From d1590f266bffc55fa76f799276c1f96fc4cef930 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Tue, 1 Aug 2023 14:35:11 +0200 Subject: [PATCH 01/31] fix: Fix query for financial statement report (cherry picked from commit bd3fc7c4342195ce22cd860cba83e287aaac15b5) --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 56d33dd111e..ebf43d13b85 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -659,7 +659,7 @@ def set_gl_entries_by_account( & (gle.posting_date <= to_date) & (account.lft >= root_lft) & (account.rgt <= root_rgt) - & (account.root_type <= root_type) + & (account.root_type == root_type) ) .orderby(gle.account, gle.posting_date) ) From 8501a1182ae8323d91438da30ddc8d93cf8c2789 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 2 Aug 2023 09:07:04 +0530 Subject: [PATCH 02/31] fix: cross connect delivery note and sales invoice (#36183) * fix: cross connect delivery note and sales invoice * chore: remove unnecessary non_standard_fieldname --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 1 + erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 0a765f3f46f..6fdcf263a55 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -15,6 +15,7 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "sales_order"], + "Delivery Note": ["items", "delivery_note"], "Timesheet": ["timesheets", "time_sheet"], }, "transactions": [ diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index b6b5ff4296f..e66c23324da 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,6 +11,7 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], + "Sales Invoice": ["items", "against_sales_invoice"], "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], }, From 8c57d56240d2c35ae09350af5d3598b4773e9ac2 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 2 Aug 2023 12:47:12 +0530 Subject: [PATCH 03/31] fix: search not working for so in the Production Plan (#36459) fix: search not working for so --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 261aa76b706..8d75c3cb60d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1731,7 +1731,7 @@ def sales_order_query( query = query.where(so_table.name.isin(filters.get("sales_orders"))) if txt: - query = query.where(table.item_code.like(f"{txt}%")) + query = query.where(table.parent.like(f"%{txt}%")) if page_len: query = query.limit(page_len) From b033b3b0d6a7a286e253a24d7432b607eaa14d09 Mon Sep 17 00:00:00 2001 From: Husam Hammad <85282854+husamhammad@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:58:05 +0300 Subject: [PATCH 04/31] fix: handle None value in payment_term_outstanding * Fix payment entry bug: Handle None value in payment_term_outstanding * fix: Handle None value in payment_term_outstanding V2 fix linting issue (cherry picked from commit 27ebf14f9d516ec555ed702a2edd78c6e65d517f) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b71e2235612..bece3f9c280 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -238,7 +238,8 @@ class PaymentEntry(AccountsController): d.payment_term and ( (flt(d.allocated_amount)) > 0 - and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + and latest.payment_term_outstanding + and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) ) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) ): From caa4f331694937a6768fa3180562e947d6996e7f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:13:22 +0530 Subject: [PATCH 05/31] fix: don't allow negative rate (backport #36027) (#36465) * fix: don't allow negative rates (#36027) * fix: don't allow negative rate * test: don't allow negative rate * fix: only check for -rate on items child table (cherry picked from commit dedf24b86db824f84dd48cf2b470272aa90ab636) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py * chore: resolve merge conflict --------- Co-authored-by: Devin Slauenwhite Co-authored-by: ruthra kumar --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ erpnext/controllers/status_updater.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 856631ee657..fd5ca8b1eba 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3316,6 +3316,13 @@ class TestSalesInvoice(unittest.TestCase): ) self.assertRaises(frappe.ValidationError, si.submit) + def test_sales_return_negative_rate(self): + si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True) + self.assertRaises(frappe.ValidationError, si.save) + + si.items[0].rate = 10 + si.save() + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 58cab147a47..a4bc4a9c69e 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -233,6 +233,9 @@ class StatusUpdater(Document): if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) + if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0: + frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code)) + if d.doctype == args["source_dt"] and d.get(args["join_field"]): args["name"] = d.get(args["join_field"]) From c1819a4b21f09b1775cb20c79a8683c734961ec3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 3 Aug 2023 12:19:25 +0530 Subject: [PATCH 06/31] fix: serial no not able to reject for the internal transfer (#36467) --- erpnext/controllers/buying_controller.py | 21 +++++- .../purchase_receipt/test_purchase_receipt.py | 64 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 2 + 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fa94a4a88d4..1de39967730 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -475,6 +475,10 @@ class BuyingController(SubcontractingController): if d.item_code not in stock_items: continue + rejected_qty = 0.0 + if flt(d.rejected_qty) != 0: + rejected_qty = flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")) + if d.warehouse: pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty")) @@ -495,6 +499,11 @@ class BuyingController(SubcontractingController): }, ) + if flt(rejected_qty) != 0: + from_warehouse_sle["actual_qty"] += -1 * rejected_qty + if d.rejected_serial_no: + from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip() + sl_entries.append(from_warehouse_sle) sle = self.get_sl_entries( @@ -520,6 +529,7 @@ class BuyingController(SubcontractingController): else 0, } ) + sl_entries.append(sle) if d.from_warehouse and ( @@ -530,23 +540,30 @@ class BuyingController(SubcontractingController): d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1} ) + if flt(rejected_qty) != 0: + from_warehouse_sle["actual_qty"] += -1 * rejected_qty + if d.rejected_serial_no: + from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip() + sl_entries.append(from_warehouse_sle) - if flt(d.rejected_qty) != 0: + if flt(rejected_qty) != 0: sl_entries.append( self.get_sl_entries( d, { "warehouse": d.rejected_warehouse, - "actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")), + "actual_qty": rejected_qty, "serial_no": cstr(d.rejected_serial_no).strip(), "incoming_rate": 0.0, + "allow_zero_valuation_rate": True, }, ) ) if self.get("is_old_subcontracting_flow"): self.make_sl_entries_for_supplier_warehouse(sl_entries) + self.make_sl_entries( sl_entries, allow_negative_stock=allow_negative_stock, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 585871cf391..c9433cf5106 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1258,6 +1258,70 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(query[0].value, 0) + def test_rejected_qty_for_internal_transfer(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + rejected_warehouse = create_warehouse( + "_Test Rejected Internal To Warehouse New", company=company + ) + item_doc = make_item( + "Test Internal Transfer Item DS", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SBNS.#####", + }, + ) + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + pr = make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=2, + rate=100, + ) + + dn1 = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + serial_no=pr.items[0].serial_no, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=2, + rate=500, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + sns = get_serial_nos(dn1.items[0].serial_no) + + self.assertEqual(len(sns), 2) + + pr1 = make_inter_company_purchase_receipt(dn1.name) + pr1.items[0].qty = 1.0 + pr1.items[0].rejected_qty = 1.0 + pr1.items[0].serial_no = sns[0] + pr1.items[0].rejected_serial_no = sns[1] + pr1.items[0].warehouse = to_warehouse + pr1.items[0].rejected_warehouse = rejected_warehouse + pr1.submit() + + rejected_serial_no_wh = frappe.get_cached_value("Serial No", sns[1], "warehouse") + + self.assertEqual(rejected_warehouse, rejected_serial_no_wh) + def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt( self, ): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 3a419195353..d52d59a0d18 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -74,6 +74,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -109,6 +110,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou "sle_id": args.get("name"), "creation": args.get("creation"), }, + allow_zero_rate=args.get("allow_zero_valuation_rate") or False, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) From dfd356a174149b98246707247a5a78b39965cc13 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:18:56 +0530 Subject: [PATCH 07/31] chore: better cost center validation for assets (backport #36477) (#36479) chore: better cost center validation for assets (#36477) (cherry picked from commit 38a612c62e2943a4a84f678bfd2804671e966b46) Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.py | 36 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b8d8ba5b486..c97f0ece73c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -148,17 +148,33 @@ class Asset(AccountsController): frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code)) def validate_cost_center(self): - if not self.cost_center: - return - - cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company") - if cost_center_company != self.company: - frappe.throw( - _("Selected Cost Center {} doesn't belongs to {}").format( - frappe.bold(self.cost_center), frappe.bold(self.company) - ), - title=_("Invalid Cost Center"), + if self.cost_center: + cost_center_company, cost_center_is_group = frappe.db.get_value( + "Cost Center", self.cost_center, ["company", "is_group"] ) + if cost_center_company != self.company: + frappe.throw( + _("Cost Center {} doesn't belong to Company {}").format( + frappe.bold(self.cost_center), frappe.bold(self.company) + ), + title=_("Invalid Cost Center"), + ) + if cost_center_is_group: + frappe.throw( + _( + "Cost Center {} is a group cost center and group cost centers cannot be used in transactions" + ).format(frappe.bold(self.cost_center)), + title=_("Invalid Cost Center"), + ) + + else: + if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"): + frappe.throw( + _( + "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}" + ).format(frappe.bold(self.company)), + title=_("Missing Cost Center"), + ) def validate_in_use_date(self): if not self.available_for_use_date: From 46bb309b8a56edf800cc4ca6b418a3ffc339d99c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 1 Aug 2023 23:22:49 +0530 Subject: [PATCH 08/31] fix: check root type only when not none (cherry picked from commit cd98be6088acc0a9d6170c88da7bdf440306f425) --- .../consolidated_financial_statement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index ebf43d13b85..cdcf73f6620 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -659,11 +659,12 @@ def set_gl_entries_by_account( & (gle.posting_date <= to_date) & (account.lft >= root_lft) & (account.rgt <= root_rgt) - & (account.root_type == root_type) ) .orderby(gle.account, gle.posting_date) ) + if root_type: + query = query.where(account.root_type == root_type) additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d) if additional_conditions: query = query.where(Criterion.all(additional_conditions)) From 47d0e7699948614f3594ac5b810265a627693f06 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 1 Aug 2023 23:24:18 +0530 Subject: [PATCH 09/31] test: balance sheet report (cherry picked from commit 002bf77314a71c02ad164e328a3a9cc9ec9714e4) --- .../balance_sheet/test_balance_sheet.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 erpnext/accounts/report/balance_sheet/test_balance_sheet.py diff --git a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py new file mode 100644 index 00000000000..3cb6efebee3 --- /dev/null +++ b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.balance_sheet.balance_sheet import execute + + +class TestBalanceSheet(FrappeTestCase): + def test_balance_sheet(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( + create_sales_invoice, + make_sales_invoice, + ) + from erpnext.accounts.utils import get_fiscal_year + + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'") + + pi = make_purchase_invoice( + company="_Test Company 6", + warehouse="Finished Goods - _TC6", + expense_account="Cost of Goods Sold - _TC6", + cost_center="Main - _TC6", + qty=10, + rate=100, + ) + si = create_sales_invoice( + company="_Test Company 6", + debit_to="Debtors - _TC6", + income_account="Sales - _TC6", + cost_center="Main - _TC6", + qty=5, + rate=110, + ) + filters = frappe._dict( + company="_Test Company 6", + period_start_date=today(), + period_end_date=today(), + periodicity="Yearly", + ) + result = execute(filters)[1] + for account_dict in result: + if account_dict.get("account") == "Current Liabilities - _TC6": + self.assertEqual(account_dict.total, 1000) + if account_dict.get("account") == "Current Assets - _TC6": + self.assertEqual(account_dict.total, 550) From 1ca9aca0d5e2002e1175581df05aa1700bd745ab Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 16:42:06 +0530 Subject: [PATCH 10/31] fix: fetch ple with party type employee in AP (cherry picked from commit c47a37c3ab1d4b4e1ebb1c27579cf2a9320db1fb) --- .../accounts_receivable.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 30f7fb38c5f..93c3fb33403 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -421,6 +421,10 @@ class ReceivablePayableReport(object): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) + if row.voucher_type == "Expense Claim": + row.party_type = "Employee" + else: + row.party_type = self.party_type if self.filters.get(scrub(self.filters.party_type)): row.currency = row.account_currency else: @@ -747,7 +751,10 @@ class ReceivablePayableReport(object): def prepare_conditions(self): self.qb_selection_filter = [] party_type_field = scrub(self.party_type) - self.qb_selection_filter.append(self.ple.party_type == self.party_type) + if self.party_type == "Supplier": + self.qb_selection_filter.append(self.ple.party_type.isin([self.party_type, "Employee"])) + else: + self.qb_selection_filter.append(self.ple.party_type == self.party_type) self.add_common_filters(party_type_field=party_type_field) @@ -901,10 +908,16 @@ class ReceivablePayableReport(object): self.columns = [] self.add_column("Posting Date", fieldtype="Date") self.add_column( - label=_(self.party_type), + label="Party Type", + fieldname="party_type", + fieldtype="Data", + width=100, + ) + self.add_column( + label="Party", fieldname="party", - fieldtype="Link", - options=self.party_type, + fieldtype="Dynamic Link", + options="party_type", width=180, ) self.add_column( From 674dba8cd7ef97103c706a19066872622f965bf2 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 11:41:03 +0530 Subject: [PATCH 11/31] fix: fetch ple for all party types (cherry picked from commit fd5c4e0a64a4a8972bf70fd1358767ab1fb86785) --- .../accounts_payable/accounts_payable.py | 2 +- .../accounts_receivable.py | 98 +++++++++++-------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.py b/erpnext/accounts/report/accounts_payable/accounts_payable.py index 7b199949113..8279afbc2bc 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.py @@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece def execute(filters=None): args = { - "party_type": "Supplier", + "account_type": "Payable", "naming_by": ["Buying Settings", "supp_master_name"], } return ReceivablePayableReport(filters).run(args) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 93c3fb33403..5b92dcd717f 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision def execute(filters=None): args = { - "party_type": "Customer", + "account_type": "Receivable", "naming_by": ["Selling Settings", "cust_master_name"], } return ReceivablePayableReport(filters).run(args) @@ -70,8 +70,11 @@ class ReceivablePayableReport(object): "Company", self.filters.get("company"), "default_currency" ) self.currency_precision = get_currency_precision() or 2 - self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" - self.party_type = self.filters.party_type + self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" + self.account_type = self.filters.account_type + self.party_type = frappe.db.get_all( + "Party Type", {"account_type": self.account_type}, pluck="name" + ) self.party_details = {} self.invoices = set() self.skip_total_row = 0 @@ -197,6 +200,7 @@ class ReceivablePayableReport(object): # no invoice, this is an invoice / stand-alone payment / credit note row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + row.party_type = ple.party_type return row def update_voucher_balance(self, ple): @@ -207,8 +211,9 @@ class ReceivablePayableReport(object): return # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get(scrub(self.party_type)): - amount = ple.amount_in_account_currency + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + amount = ple.amount_in_account_currency else: amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency @@ -362,7 +367,7 @@ class ReceivablePayableReport(object): def get_invoice_details(self): self.invoice_details = frappe._dict() - if self.party_type == "Customer": + if self.account_type == "Receivable": si_list = frappe.db.sql( """ select name, due_date, po_no @@ -390,7 +395,7 @@ class ReceivablePayableReport(object): d.sales_person ) - if self.party_type == "Supplier": + if self.account_type == "Payable": for pi in frappe.db.sql( """ select name, due_date, bill_no, bill_date @@ -421,12 +426,10 @@ class ReceivablePayableReport(object): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) - if row.voucher_type == "Expense Claim": - row.party_type = "Employee" - else: - row.party_type = self.party_type - if self.filters.get(scrub(self.filters.party_type)): - row.currency = row.account_currency + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + row.currency = row.account_currency + break else: row.currency = self.company_currency @@ -552,7 +555,7 @@ class ReceivablePayableReport(object): where payment_entry.docstatus < 2 and payment_entry.posting_date > %s - and payment_entry.party_type = %s + and payment_entry.party_type in %s """, (self.filters.report_date, self.party_type), as_dict=1, @@ -562,11 +565,11 @@ class ReceivablePayableReport(object): if self.filters.get("party"): amount_field = ( "jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.party_type == "Supplier" + if self.account_type == "Payable" else "jea.credit_in_account_currency - jea.debit_in_account_currency" ) else: - amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit" + amount_field = "jea.debit - " if self.account_type == "Payable" else "jea.credit" return frappe.db.sql( """ @@ -584,7 +587,7 @@ class ReceivablePayableReport(object): where je.docstatus < 2 and je.posting_date > %s - and jea.party_type = %s + and jea.party_type in %s and jea.reference_name is not null and jea.reference_name != '' group by je.name, jea.reference_name having future_amount > 0 @@ -623,13 +626,17 @@ class ReceivablePayableReport(object): row.future_ref = ", ".join(row.future_ref) def get_return_entries(self): - doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" + doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice" filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company} - party_field = scrub(self.filters.party_type) - if self.filters.get(party_field): - filters.update({party_field: self.filters.get(party_field)}) + or_filters = {} + for party_type in self.party_type: + party_field = scrub(party_type) + if self.filters.get(party_field): + or_filters.update({party_field: self.filters.get(party_field)}) self.return_entries = frappe._dict( - frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1) + frappe.get_all( + doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1 + ) ) def set_ageing(self, row): @@ -720,6 +727,7 @@ class ReceivablePayableReport(object): ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) + .where(Criterion.any(self.or_filters)) ) if self.filters.get("group_by_party"): @@ -750,19 +758,18 @@ class ReceivablePayableReport(object): def prepare_conditions(self): self.qb_selection_filter = [] - party_type_field = scrub(self.party_type) - if self.party_type == "Supplier": - self.qb_selection_filter.append(self.ple.party_type.isin([self.party_type, "Employee"])) - else: - self.qb_selection_filter.append(self.ple.party_type == self.party_type) + self.or_filters = [] + for party_type in self.party_type: + party_type_field = scrub(party_type) + self.or_filters.append(self.ple.party_type == party_type) - self.add_common_filters(party_type_field=party_type_field) + self.add_common_filters(party_type_field=party_type_field) - if party_type_field == "customer": - self.add_customer_filters() + if party_type_field == "customer": + self.add_customer_filters() - elif party_type_field == "supplier": - self.add_supplier_filters() + elif party_type_field == "supplier": + self.add_supplier_filters() if self.filters.cost_center: self.get_cost_center_conditions() @@ -791,11 +798,10 @@ class ReceivablePayableReport(object): self.qb_selection_filter.append(self.ple.account == self.filters.party_account) else: # get GL with "receivable" or "payable" account_type - account_type = "Receivable" if self.party_type == "Customer" else "Payable" accounts = [ d.name for d in frappe.get_all( - "Account", filters={"account_type": account_type, "company": self.filters.company} + "Account", filters={"account_type": self.account_type, "company": self.filters.company} ) ] @@ -885,7 +891,7 @@ class ReceivablePayableReport(object): def get_party_details(self, party): if not party in self.party_details: - if self.party_type == "Customer": + if self.account_type == "Receivable": fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] if self.filters.get("sales_partner"): @@ -921,7 +927,7 @@ class ReceivablePayableReport(object): width=180, ) self.add_column( - label="Receivable Account" if self.party_type == "Customer" else "Payable Account", + label=self.account_type + " Account", fieldname="party_account", fieldtype="Link", options="Account", @@ -929,13 +935,19 @@ class ReceivablePayableReport(object): ) if self.party_naming_by == "Naming Series": + if self.account_type == "Payable": + label = "Supplier Name" + fieldname = "supplier_name" + else: + label = "Customer Name" + fieldname = "customer_name" self.add_column( - _("{0} Name").format(self.party_type), - fieldname=scrub(self.party_type) + "_name", + label=label, + fieldname=fieldname, fieldtype="Data", ) - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column( _("Customer Contact"), fieldname="customer_primary_contact", @@ -955,7 +967,7 @@ class ReceivablePayableReport(object): self.add_column(label="Due Date", fieldtype="Date") - if self.party_type == "Supplier": + if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date") @@ -965,7 +977,7 @@ class ReceivablePayableReport(object): self.add_column(_("Invoiced Amount"), fieldname="invoiced") self.add_column(_("Paid Amount"), fieldname="paid") - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column(_("Credit Note"), fieldname="credit_note") else: # note: fieldname is still `credit_note` @@ -983,7 +995,7 @@ class ReceivablePayableReport(object): self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.filters.party_type == "Customer": + if self.filters.account_type == "Receivable": self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data") # comma separated list of linked delivery notes @@ -1004,7 +1016,7 @@ class ReceivablePayableReport(object): if self.filters.sales_partner: self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") - if self.filters.party_type == "Supplier": + if self.filters.account_type == "Payable": self.add_column( label=_("Supplier Group"), fieldname="supplier_group", From 769d7d7554ff808f0d14b4ed7e276ef44ea15259 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 14:51:28 +0530 Subject: [PATCH 12/31] fix: AP and AR summary (cherry picked from commit e355dea4b550fcf64450876652f852f6a6c529fd) --- erpnext/accounts/party.py | 38 ++++++++------- .../accounts_payable_summary.py | 2 +- .../accounts_receivable_summary.py | 48 ++++++++++++++----- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 56d33b758b7..b53e282bfc5 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values +from frappe.query_builder.functions import Date, Sum from frappe.utils import ( add_days, add_months, @@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None + party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None ): - cond = "1=1" + gle = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gle) + .select(gle.party) + .where( + (gle.party_type.isin(party_type)) & (gle.against_voucher == None) & (gle.is_cancelled == 0) + ) + .groupby(gle.party) + ) + if account_type == "Receivable": + query = query.select(Sum(gle.credit).as_("amount")) + else: + query = query.select(Sum(gle.debit).as_("amount")) + if posting_date: if future_payment: - cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date) + query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) else: - cond = "posting_date <= '{0}'".format(posting_date) + query = query.where(gle.posting_date <= posting_date) if company: - cond += "and company = {0}".format(frappe.db.escape(company)) + query = query.where(gle.company == company) if party: - cond += "and party = {0}".format(frappe.db.escape(party)) + query = query.where(gle.party == party) - data = frappe.db.sql( - """ SELECT party, sum({0}) as amount - FROM `tabGL Entry` - WHERE - party_type = %s and against_voucher is null - and is_cancelled = 0 - and {1} GROUP BY party""".format( - ("credit") if party_type == "Customer" else "debit", cond - ), - party_type, - ) + data = query.run(as_dict=True) if data: return frappe._dict(data) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py index 65fe1de5689..834c83c38e9 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py @@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum def execute(filters=None): args = { - "party_type": "Supplier", + "account_type": "Payable", "naming_by": ["Buying Settings", "supp_master_name"], } return AccountsReceivableSummary(filters).run(args) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 9c01b1a4980..3aa1ae71045 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece def execute(filters=None): args = { - "party_type": "Customer", + "account_type": "Receivable", "naming_by": ["Selling Settings", "cust_master_name"], } @@ -21,7 +21,10 @@ def execute(filters=None): class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): - self.party_type = args.get("party_type") + self.account_type = args.get("account_type") + self.party_type = frappe.db.get_all( + "Party Type", {"account_type": self.account_type}, pluck="name" + ) self.party_naming_by = frappe.db.get_value( args.get("naming_by")[0], None, args.get("naming_by")[1] ) @@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.get_party_total(args) + party = None + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + party = self.filters.get(scrub(party_type)) + party_advance_amount = ( get_partywise_advanced_payment_amount( self.party_type, self.filters.report_date, self.filters.show_future_payments, self.filters.company, - party=self.filters.get(scrub(self.party_type)), + party=party, + account_type=self.account_type, ) or {} ) @@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport): row.party = party if self.party_naming_by == "Naming Series": - row.party_name = frappe.get_cached_value( - self.party_type, party, scrub(self.party_type) + "_name" - ) + if self.account_type == "Payable": + doctype = "Supplier" + fieldname = "supplier_name" + else: + doctype = "Customer" + fieldname = "customer_name" + row.party_name = frappe.get_cached_value(doctype, party, fieldname) row.update(party_dict) @@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): # set territory, customer_group, sales person etc self.set_party_details(d) + self.party_total[d.party].update({"party_type": d.party_type}) def init_party_total(self, row): self.party_total.setdefault( @@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_columns(self): self.columns = [] self.add_column( - label=_(self.party_type), + label="Party Type", + fieldname="party_type", + fieldtype="Data", + width=100, + ) + self.add_column( + label="Party", fieldname="party", - fieldtype="Link", - options=self.party_type, + fieldtype="Dynamic Link", + options="party_type", width=180, ) if self.party_naming_by == "Naming Series": - self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data") + self.add_column( + label="Supplier Name" if self.account_type == "Payable" else "Customer Name", + fieldname="party_name", + fieldtype="Data", + ) - credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note" + credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note" self.add_column(_("Advance Amount"), fieldname="advance") self.add_column(_("Invoiced Amount"), fieldname="invoiced") @@ -159,7 +183,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column( label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" ) From a79b30e45f49ea90e246dd9866aee5dcbcc32f34 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 16:01:30 +0530 Subject: [PATCH 13/31] refactor: future payments query (cherry picked from commit f5761e79657f744aff6b1ed964f9ffdf6e7d5a9f) --- erpnext/accounts/party.py | 2 +- .../accounts_receivable.py | 110 +++++++++--------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b53e282bfc5..3aea40316b5 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -891,7 +891,7 @@ def get_partywise_advanced_payment_amount( frappe.qb.from_(gle) .select(gle.party) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher == None) & (gle.is_cancelled == 0) + (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) ) .groupby(gle.party) ) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 5b92dcd717f..11bbb6f1e43 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -7,7 +7,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, scrub from frappe.query_builder import Criterion -from frappe.query_builder.functions import Date +from frappe.query_builder.functions import Date, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -539,65 +539,67 @@ class ReceivablePayableReport(object): self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) def get_future_payments_from_payment_entry(self): - return frappe.db.sql( - """ - select - ref.reference_name as invoice_no, - payment_entry.party, - payment_entry.party_type, - payment_entry.posting_date as future_date, - ref.allocated_amount as future_amount, - payment_entry.reference_no as future_ref - from - `tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref - on - (ref.parent = payment_entry.name) - where - payment_entry.docstatus < 2 - and payment_entry.posting_date > %s - and payment_entry.party_type in %s - """, - (self.filters.report_date, self.party_type), - as_dict=1, - ) + pe = frappe.qb.DocType("Payment Entry") + pe_ref = frappe.qb.DocType("Payment Entry Reference") + return ( + frappe.qb.from_(pe) + .inner_join(pe_ref) + .on(pe_ref.parent == pe.name) + .select( + (pe_ref.reference_name).as_("invoice_no"), + pe.party, + pe.party_type, + (pe.posting_date).as_("future_date"), + (pe_ref.allocated_amount).as_("future_amount"), + (pe.reference_no).as_("future_ref"), + ) + .where( + (pe.docstatus < 2) + & (pe.posting_date > self.filters.report_date) + & (pe.party_type.isin(self.party_type)) + ) + ).run(as_dict=True) def get_future_payments_from_journal_entry(self): - if self.filters.get("party"): - amount_field = ( - "jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.account_type == "Payable" - else "jea.credit_in_account_currency - jea.debit_in_account_currency" - ) - else: - amount_field = "jea.debit - " if self.account_type == "Payable" else "jea.credit" - - return frappe.db.sql( - """ - select - jea.reference_name as invoice_no, + je = frappe.qb.DocType("Journal Entry") + jea = frappe.qb.DocType("Journal Entry Account") + query = ( + frappe.qb.from_(je) + .inner_join(jea) + .on(jea.parent == je.name) + .select( + jea.reference_name.as_("invoice_no"), jea.party, jea.party_type, - je.posting_date as future_date, - sum('{0}') as future_amount, - je.cheque_no as future_ref - from - `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea - on - (jea.parent = je.name) - where - je.docstatus < 2 - and je.posting_date > %s - and jea.party_type in %s - and jea.reference_name is not null and jea.reference_name != '' - group by je.name, jea.reference_name - having future_amount > 0 - """.format( - amount_field - ), - (self.filters.report_date, self.party_type), - as_dict=1, + je.posting_date.as_("future_date"), + je.cheque_no.as_("future_ref"), + ) + .where( + (je.docstatus < 2) + & (je.posting_date > self.filters.report_date) + & (jea.party_type.isin(self.party_type)) + & (jea.reference_name.isnotnull()) + & (jea.reference_name != "") + ) ) + if self.filters.get("party"): + if self.account_type == "Payable": + query = query.select( + Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount") + ) + else: + query = query.select( + Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount") + ) + else: + query = query.select( + Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount") + ) + + query = query.having(qb.Field("future_amount") > 0) + return query.run(as_dict=True) + def allocate_future_payments(self, row): # future payments are captured in additional columns # this method allocates pending future payments against a voucher to From cf2a3e2fc50d7822a2588bc01a595b00f53f75ab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:52:29 +0530 Subject: [PATCH 14/31] Contact Doctype don't have any field `job_title`. (#36156) fix: Contact Doctype doesn't have any field called `job_title` fix: Contact Doctype doesn't have any field called `job_title` (cherry picked from commit 49be7407369f33419513475f00b1ca8da9efea17) Co-authored-by: Sumit Jain <59503001+sumitjain236@users.noreply.github.com> --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 08ea4b06e7c..460974972c5 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -185,7 +185,7 @@ class Lead(SellingController, CRMNote): "last_name": self.last_name, "salutation": self.salutation, "gender": self.gender, - "job_title": self.job_title, + "designation": self.job_title, "company_name": self.company_name, } ) From 0d7a4b6ff64dbe5cca0cc6999557c2a9fdbd54fb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 19:31:55 +0530 Subject: [PATCH 15/31] fix(ux): add `Ordered Qty` column in Get Items From > MR (backport #36486) (#36505) fix(ux): add `Ordered Qty` column in Get Items From > MR (#36486) (cherry picked from commit e17949976442f921335ab1961928ddec37aeffef) Co-authored-by: s-aga-r --- erpnext/buying/doctype/purchase_order/purchase_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 8fa8f305549..52a0f4ac2a2 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -369,7 +369,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e }, allow_child_item_selection: true, child_fieldname: "items", - child_columns: ["item_code", "qty"] + child_columns: ["item_code", "qty", "ordered_qty"] }) }, __("Get Items From")); From 67393694de3ee4f37e40c8fe1e3a9963acc0ed8b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:43:44 +0530 Subject: [PATCH 16/31] fix(accounts): Translate columns in AP/AR report (#36503) fix(accounts): Translate columns in AP/AR report (#36503) (cherry picked from commit 559d914c0bffb615b9b53083f6c9ca9fd2ca9a3d) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> --- .../accounts_receivable_summary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 3aa1ae71045..da4c9dabbf6 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -145,13 +145,13 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_columns(self): self.columns = [] self.add_column( - label="Party Type", + label=_("Party Type"), fieldname="party_type", fieldtype="Data", width=100, ) self.add_column( - label="Party", + label=_("Party"), fieldname="party", fieldtype="Dynamic Link", options="party_type", @@ -160,7 +160,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): if self.party_naming_by == "Naming Series": self.add_column( - label="Supplier Name" if self.account_type == "Payable" else "Customer Name", + label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"), fieldname="party_name", fieldtype="Data", ) From 2216875bd6f22f695e19e9af6219628760b42a88 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:44:10 +0530 Subject: [PATCH 17/31] fix: Lower deduction certificate for multi-company (#36491) fix: Lower deduction certificate for multi-company (#36491) (cherry picked from commit 96035b87d566948ac56bec2a49357fed199a59ba) Co-authored-by: Deepesh Garg --- .../lower_deduction_certificate/lower_deduction_certificate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index cc223e91bc8..6ae04c165c4 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -34,6 +34,7 @@ class LowerDeductionCertificate(Document): "supplier": self.supplier, "tax_withholding_category": self.tax_withholding_category, "name": ("!=", self.name), + "company": self.company, }, ["name", "valid_from", "valid_upto"], as_dict=True, From a234b8932e34b7f726c54a720c3199ed1e3bfb64 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:58:55 +0530 Subject: [PATCH 18/31] fix: Tax withholding against order via Payment Entry (#36493) fix: Tax withholding against order via Payment Entry (#36493) * fix: Tax withholding against order via Payment Entry * test: Add test case * fix: Nonetype exceptions (cherry picked from commit 93767eb7fcaadd5d7e921c3e255b495ea02b094c) Co-authored-by: Deepesh Garg --- .../doctype/payment_entry/payment_entry.py | 18 ++++++++- .../test_tax_withholding_category.py | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bece3f9c280..580608d5a37 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -636,7 +636,9 @@ class PaymentEntry(AccountsController): if not self.apply_tax_withholding_amount: return - net_total = self.paid_amount + order_amount = self.get_order_net_total() + + net_total = flt(order_amount) + flt(self.unallocated_amount) # Adding args as purchase invoice to get TDS amount args = frappe._dict( @@ -681,6 +683,20 @@ class PaymentEntry(AccountsController): for d in to_remove: self.remove(d) + def get_order_net_total(self): + if self.party_type == "Supplier": + doctype = "Purchase Order" + else: + doctype = "Sales Order" + + docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype] + + tax_withholding_net_total = frappe.db.get_value( + doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"] + ) + + return tax_withholding_net_total + def apply_taxes(self): self.initialize_taxes() self.determine_exclusive_rate() diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ac84217e6f9..f8e0e2992f7 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -321,6 +321,42 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in reversed(orders): d.cancel() + def test_tds_deduction_for_po_via_payment_entry(self): + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + frappe.db.set_value( + "Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS" + ) + order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True) + + # Add some tax on the order + order.append( + "taxes", + { + "category": "Total", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "tax_amount": 8000, + "description": "Test", + "add_deduct_tax": "Add", + }, + ) + + order.save() + + order.apply_tds = 1 + order.tax_withholding_category = "Cumulative Threshold TDS" + order.submit() + + self.assertEqual(order.taxes[0].tax_amount, 4000) + + payment = get_payment_entry(order.doctype, order.name) + payment.apply_tax_withholding_amount = 1 + payment.tax_withholding_category = "Cumulative Threshold TDS" + payment.submit() + self.assertEqual(payment.taxes[0].tax_amount, 4000) + def test_multi_category_single_supplier(self): frappe.db.set_value( "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category" @@ -578,6 +614,7 @@ def create_records(): "Test TDS Supplier5", "Test TDS Supplier6", "Test TDS Supplier7", + "Test TDS Supplier8", ]: if frappe.db.exists("Supplier", name): continue From bdfbccd38eefd5c7a3385f3e25bcb3fcb442f2ae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:51:23 +0530 Subject: [PATCH 19/31] fix: get incoming rate instead of BOM rate (backport #36496) (#36506) * fix: get incoming rate instead of BOM rate (#36496) * fix: get incoming rate instead of BOM rate * test: add test case for SCR rm rate (cherry picked from commit 758b31d895f77ae04b075b65dce3bb427b70975a) # Conflicts: # erpnext/controllers/subcontracting_controller.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../controllers/subcontracting_controller.py | 2 +- .../test_subcontracting_receipt.py | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 566135d75b9..b01b76d1ec9 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -460,7 +460,7 @@ class SubcontractingController(StockController): "allow_zero_valuation": 1, } ) - rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args) + rm_obj.rate = get_incoming_rate(args) if self.doctype == self.subcontract_data.order_doctype: rm_obj.required_qty = qty diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index dfb72c33567..6c962531dfa 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -594,6 +594,67 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertNotEqual(scr.supplied_items[0].rate, prev_cost) self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) + def test_subcontracting_receipt_raw_material_rate(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + # Step - 1: Set Backflush Based On as "BOM" + set_backflush_based_on("BOM") + + # Step - 2: Create FG and RM Items + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item(properties={"is_stock_item": 1}).name + rm_item2 = make_item(properties={"is_stock_item": 1}).name + + # Step - 3: Create BOM for FG Item + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]) + for rm_item in bom.items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + bom = bom.name + + # Step - 4: Create PO and SCO + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 100, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 100, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + for rm_item in sco.supplied_items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + + # Step - 5: Inward Raw Materials + rm_items = get_rm_items(sco.supplied_items) + for rm_item in rm_items: + rm_item["rate"] = 100 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + # Step - 6: Transfer RM's to Subcontractor + se = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + for item in se.items: + self.assertEqual(item.qty, 100) + self.assertEqual(item.basic_rate, 100) + self.assertEqual(item.amount, item.qty * item.basic_rate) + + # Step - 7: Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + scr.load_from_db() + for rm_item in scr.supplied_items: + self.assertEqual(rm_item.consumed_qty, 100) + self.assertEqual(rm_item.rate, 100) + self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) From 2d7d86039aa5487f841aa63247d2305a1f6d3b0b Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Sun, 6 Aug 2023 23:34:02 +0530 Subject: [PATCH 20/31] chore: don't merge asset capitalization gl entries (#36514) --- .../doctype/asset_capitalization/asset_capitalization.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 5625fbb523b..fb480420b97 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -325,7 +325,7 @@ class AssetCapitalization(StockController): gl_entries = self.get_gl_entries() if gl_entries: - make_gl_entries(gl_entries, from_repost=from_repost) + make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -355,9 +355,6 @@ class AssetCapitalization(StockController): gl_entries, target_account, target_against, precision ) - if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable: - return [] - self.get_gl_entries_for_target_item(gl_entries, target_against, precision) return gl_entries From f9981d1ff3527057cebc132eede74638b88ebfeb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:08:59 +0530 Subject: [PATCH 21/31] fix: use correct lang separator for frappe (backport #36519) (#36520) * fix: use correct lang separator for frappe (cherry picked from commit 0218ca538f8dd4342d1c9a989607441dc89fed22) * perf: defer babel import Only required when configuring but will get loaded everywhere (cherry picked from commit f574ac11ea8ee2f7f46916d93c6f5877350bc069) --------- Co-authored-by: Ankush Menat --- erpnext/setup/doctype/holiday_list/holiday_list.py | 5 +++-- .../setup/doctype/holiday_list/test_holiday_list.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 2ef4e655b2d..6b6c8ba5be8 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -6,7 +6,6 @@ import json from datetime import date import frappe -from babel import Locale from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today @@ -169,4 +168,6 @@ def is_holiday(holiday_list, date=None): def local_country_name(country_code: str) -> str: """Return the localized country name for the given country code.""" - return Locale.parse(frappe.local.lang).territories.get(country_code, country_code) + from babel import Locale + + return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code) diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index 23b08fd1170..7eeb27d864e 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -8,6 +8,8 @@ from datetime import date, timedelta import frappe from frappe.utils import getdate +from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name + class TestHolidayList(unittest.TestCase): def test_holiday_list(self): @@ -58,6 +60,16 @@ class TestHolidayList(unittest.TestCase): self.assertIn(date(2023, 4, 10), holidays) self.assertNotIn(date(2023, 5, 1), holidays) + def test_localized_country_names(self): + lang = frappe.local.lang + frappe.local.lang = "en-gb" + self.assertEqual(local_country_name("IN"), "India") + self.assertEqual(local_country_name("DE"), "Germany") + + frappe.local.lang = "de" + self.assertEqual(local_country_name("DE"), "Deutschland") + frappe.local.lang = lang + def make_holiday_list( name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None From 7adad4272af06ac3bf50b9dd27affc507f4b931b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 7 Aug 2023 10:08:03 +0530 Subject: [PATCH 22/31] perf: defer holiday list imports Only used for configuring but loaded whenever get_doc("holiday list", ...) is done (cherry picked from commit 2eea90a873ca48a351dfda4bcc52a4e1fd57bff6) --- erpnext/setup/doctype/holiday_list/holiday_list.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 6b6c8ba5be8..526bc2ba4ac 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -9,8 +9,6 @@ import frappe from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today -from holidays import country_holidays -from holidays.utils import list_supported_countries class OverlapError(frappe.ValidationError): @@ -39,6 +37,8 @@ class HolidayList(Document): @frappe.whitelist() def get_supported_countries(self): + from holidays.utils import list_supported_countries + subdivisions_by_country = list_supported_countries() countries = [ {"value": country, "label": local_country_name(country)} @@ -51,6 +51,8 @@ class HolidayList(Document): @frappe.whitelist() def get_local_holidays(self): + from holidays import country_holidays + if not self.country: throw(_("Please select a country")) From 07f235cf7d251573a789f014becd0c5956cefc54 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 7 Aug 2023 11:28:07 +0530 Subject: [PATCH 23/31] feat: ledger comparison report (#36485) * feat: Accounting Ledger comparison report * chore: barebones methods * chore: working state * chore: refactor internal logic * chore: working multi select filter on Account * chore: working voucher no filter * chore: remove debugging statements * chore: report with currency symbol * chore: working start and end date filter * test: basic report function * refactor(test): test all filters (cherry picked from commit b86747c9d4595d37ffeca44c0915a117730ed078) --- .../__init__.py | 0 .../general_and_payment_ledger_comparison.js | 52 +++++ ...general_and_payment_ledger_comparison.json | 32 +++ .../general_and_payment_ledger_comparison.py | 221 ++++++++++++++++++ ...t_general_and_payment_ledger_comparison.py | 100 ++++++++ 5 files changed, 405 insertions(+) create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js new file mode 100644 index 00000000000..7e6b0537e87 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js @@ -0,0 +1,52 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"account", + "label": __("Account"), + "fieldtype": "MultiSelectList", + "options": "Account", + get_data: function(txt) { + return frappe.db.get_link_options('Account', txt, { + company: frappe.query_report.get_filter_value("company"), + account_type: ['in', ["Receivable", "Payable"]] + }); + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "Data", + "width": 100, + }, + ] + return filters; +} + +frappe.query_reports["General and Payment Ledger Comparison"] = { + "filters": get_filters() +}; diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json new file mode 100644 index 00000000000..1d0d9d134da --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-08-02 17:30:29.494907", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2023-08-02 17:30:29.494907", + "modified_by": "Administrator", + "module": "Accounts", + "name": "General and Payment Ledger Comparison", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "General and Payment Ledger Comparison", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py new file mode 100644 index 00000000000..553c137f024 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -0,0 +1,221 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Sum + + +class General_Payment_Ledger_Comparison(object): + """ + A Utility report to compare Voucher-wise balance between General and Payment Ledger + """ + + def __init__(self, filters=None): + self.filters = filters + self.gle = [] + self.ple = [] + + def get_accounts(self): + receivable_accounts = [ + x[0] + for x in frappe.db.get_all( + "Account", + filters={"company": self.filters.company, "account_type": "Receivable"}, + as_list=True, + ) + ] + payable_accounts = [ + x[0] + for x in frappe.db.get_all( + "Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True + ) + ] + + self.account_types = frappe._dict( + { + "receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}), + "payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}), + } + ) + + def generate_filters(self): + if self.filters.account: + self.account_types.receivable.accounts = [] + self.account_types.payable.accounts = [] + + for acc in frappe.db.get_all( + "Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"] + ): + if acc.account_type == "Receivable": + self.account_types.receivable.accounts.append(acc.name) + else: + self.account_types.payable.accounts.append(acc.name) + + def get_gle(self): + gle = qb.DocType("GL Entry") + + for acc_type, val in self.account_types.items(): + if val.accounts: + + filter_criterion = [] + if self.filters.voucher_no: + filter_criterion.append((gle.voucher_no == self.filters.voucher_no)) + + if self.filters.period_start_date: + filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date)) + + if self.filters.period_end_date: + filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date)) + + if acc_type == "receivable": + outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding") + else: + outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding") + + self.account_types[acc_type].gle = ( + qb.from_(gle) + .select( + gle.company, + gle.account, + gle.voucher_no, + gle.party, + outstanding, + ) + .where( + (gle.company == self.filters.company) + & (gle.is_cancelled == 0) + & (gle.account.isin(val.accounts)) + ) + .where(Criterion.all(filter_criterion)) + .groupby(gle.company, gle.account, gle.voucher_no, gle.party) + .run() + ) + + def get_ple(self): + ple = qb.DocType("Payment Ledger Entry") + + for acc_type, val in self.account_types.items(): + if val.accounts: + + filter_criterion = [] + if self.filters.voucher_no: + filter_criterion.append((ple.voucher_no == self.filters.voucher_no)) + + if self.filters.period_start_date: + filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date)) + + if self.filters.period_end_date: + filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date)) + + self.account_types[acc_type].ple = ( + qb.from_(ple) + .select( + ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding") + ) + .where( + (ple.company == self.filters.company) + & (ple.delinked == 0) + & (ple.account.isin(val.accounts)) + ) + .where(Criterion.all(filter_criterion)) + .groupby(ple.company, ple.account, ple.voucher_no, ple.party) + .run() + ) + + def compare(self): + self.gle_balances = set() + self.ple_balances = set() + + # consolidate both receivable and payable balances in one set + for acc_type, val in self.account_types.items(): + self.gle_balances = set(val.gle) | self.gle_balances + self.ple_balances = set(val.ple) | self.ple_balances + + self.diff1 = self.gle_balances.difference(self.ple_balances) + self.diff2 = self.ple_balances.difference(self.gle_balances) + self.diff = frappe._dict({}) + + for x in self.diff1: + self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) + + for x in self.diff2: + self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]})) + + def generate_data(self): + self.data = [] + for key, val in self.diff.items(): + self.data.append( + frappe._dict( + { + "voucher_no": key[2], + "party": key[3], + "gl_balance": val.gl_balance, + "pl_balance": val.pl_balance, + } + ) + ) + + def get_columns(self): + self.columns = [] + options = None + self.columns.append( + dict( + label=_("Voucher No"), + fieldname="voucher_no", + fieldtype="Data", + options=options, + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Party"), + fieldname="party", + fieldtype="Data", + options=options, + width="100", + ) + ) + + self.columns.append( + dict( + label=_("GL Balance"), + fieldname="gl_balance", + fieldtype="Currency", + options="Company:company:default_currency", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Payment Ledger Balance"), + fieldname="pl_balance", + fieldtype="Currency", + options="Company:company:default_currency", + width="100", + ) + ) + + def run(self): + self.get_accounts() + self.generate_filters() + self.get_gle() + self.get_ple() + self.compare() + self.generate_data() + self.get_columns() + + return self.columns, self.data + + +def execute(filters=None): + columns, data = [], [] + + rpt = General_Payment_Ledger_Comparison(filters) + columns, data = rpt.run() + + return columns, data diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py new file mode 100644 index 00000000000..4b0e99d7125 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py @@ -0,0 +1,100 @@ +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import ( + execute, +) +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin): + def setUp(self): + self.create_company() + self.cleanup() + + def tearDown(self): + frappe.db.rollback() + + def cleanup(self): + doctypes = [] + doctypes.append(qb.DocType("GL Entry")) + doctypes.append(qb.DocType("Payment Ledger Entry")) + doctypes.append(qb.DocType("Sales Invoice")) + + for doctype in doctypes: + qb.from_(doctype).delete().where(doctype.company == self.company).run() + + def test_01_basic_report_functionality(self): + sinv = create_sales_invoice( + company=self.company, + debit_to=self.debit_to, + expense_account=self.expense_account, + cost_center=self.cost_center, + income_account=self.income_account, + warehouse=self.warehouse, + ) + + # manually edit the payment ledger entry + ple = frappe.db.get_all( + "Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0} + )[0] + frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1) + + filters = frappe._dict({"company": self.company}) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + + expected = { + "voucher_no": sinv.name, + "party": sinv.customer, + "gl_balance": sinv.grand_total, + "pl_balance": sinv.grand_total - 1, + } + self.assertEqual(expected, data[0]) + + # account filter + filters = frappe._dict({"company": self.company, "account": self.debit_to}) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + + filters = frappe._dict({"company": self.company, "account": self.creditors}) + columns, data = execute(filters=filters) + self.assertEqual([], data) + + # voucher_no filter + filters = frappe._dict({"company": self.company, "voucher_no": sinv.name}) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + + filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"}) + columns, data = execute(filters=filters) + self.assertEqual([], data) + + # date range filter + filters = frappe._dict( + { + "company": self.company, + "period_start_date": sinv.posting_date, + "period_end_date": sinv.posting_date, + } + ) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + + filters = frappe._dict( + { + "company": self.company, + "period_start_date": add_days(sinv.posting_date, -1), + "period_end_date": add_days(sinv.posting_date, -1), + } + ) + columns, data = execute(filters=filters) + self.assertEqual([], data) From 5b047081646ad9131ffbe925d57904597aa454ea Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:11:19 +0530 Subject: [PATCH 24/31] fix: stock entry decimal issue (backport #36530) (#36533) fix: stock entry decimal issue (#36530) (cherry picked from commit 28dfc88789e71edac74556e01e8d49d46e42a35f) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index b430d03d92b..cf61f9657f4 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -228,7 +228,8 @@ class MaterialRequest(BuyingController): d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) if mr_qty_allowance: - allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) + allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")) + if d.ordered_qty and d.ordered_qty > allowed_qty: frappe.throw( _( From 9c108a8ef70ec6f440cddeccc640c363bcf02196 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 21:03:36 +0530 Subject: [PATCH 25/31] fix: enqueue submit/cancel action for stock entry having more than 50 line items (backport #36532) (#36536) fix: enqueue submit/cancel action for stock entry having more than 50 line items (#36532) (cherry picked from commit ecba6ee1833343d57de15ca546da87e140ab8a55) Co-authored-by: s-aga-r --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b93ffc437cb..9560b52f59c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -194,7 +194,7 @@ class StockEntry(StockController): return False # If line items are more than 100 or record is older than 6 months - if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6: + if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6: return True return False From 5881960001fa55e3d24ddd009e6b897fd4109936 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 21:17:12 +0530 Subject: [PATCH 26/31] feat(RFQ): make sending attachments configurable (backport #36359) (#36535) * feat(RFQ): make sending attachments configurable (#36359) (cherry picked from commit 8cc3df7c2c77d55ee6cea85b67b045f8c35c9668) # Conflicts: # erpnext/buying/doctype/request_for_quotation/request_for_quotation.json * chore: resolve conflicts --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../request_for_quotation/request_for_quotation.json | 10 +++++++++- .../request_for_quotation/request_for_quotation.py | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index bd65b0c805e..c16abb2f41b 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -25,6 +25,7 @@ "col_break_email_1", "email_template", "preview", + "send_attached_files", "sec_break_email_2", "message_for_supplier", "terms_section_break", @@ -285,13 +286,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "1", + "description": "If enabled, all files attached to this document will be attached to each email", + "fieldname": "send_attached_files", + "fieldtype": "Check", + "label": "Send Attached Files" } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-31 23:22:06.684694", + "modified": "2023-07-27 16:41:48.468873", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 4590f8c3d93..57bd6bd5705 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -209,7 +209,9 @@ class RequestforQuotation(BuyingController): if preview: return message - attachments = self.get_attachments() + attachments = None + if self.send_attached_files: + attachments = self.get_attachments() self.send_email(data, sender, subject, message, attachments) From c26a52d7918f1f1db70ab7958633e6b65c39ffab Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Aug 2023 14:14:30 +0530 Subject: [PATCH 27/31] refactor: use base_tax_withholding_net_total for treshold validation (#36528) * refactor: use base_tax_withholding_net_total for treshold validation * fix: only for non payment entry doctypes (cherry picked from commit 11d5327d1b5fc1b9198bad77365d5ca269593e76) --- .../tax_withholding_category/tax_withholding_category.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 58792d1d8ad..e66a886bf9a 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -476,7 +476,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) - if (threshold and inv.tax_withholding_net_total >= threshold) or ( + if inv.doctype != "Payment Entry": + tax_withholding_net_total = inv.base_tax_withholding_net_total + else: + tax_withholding_net_total = inv.tax_withholding_net_total + + if (threshold and tax_withholding_net_total >= threshold) or ( cumulative_threshold and supp_credit_amt >= cumulative_threshold ): if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( From cd1c175439dc58ba78224f8a2abd4d9bd8c1a39b Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 8 Aug 2023 15:09:55 +0530 Subject: [PATCH 28/31] perf: asset depreciation entry posting (#36461) * perf: make post depr entries job daily_long * perf: optimise post_depreciation_entries and make_depreciation_entry * chore: more optimisation and dont fail all schedule dates if one date fails * chore: don't post entries before acc_frozen_upto * chore: using get_single_value * refactor: destructure asset object --- erpnext/assets/doctype/asset/asset.py | 8 +- erpnext/assets/doctype/asset/depreciation.py | 322 +++++++++++++----- .../asset_value_adjustment.py | 4 +- erpnext/hooks.py | 2 +- 4 files changed, 241 insertions(+), 95 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c97f0ece73c..e6bac31d7d2 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -962,7 +962,9 @@ class Asset(AccountsController): @frappe.whitelist() def get_manual_depreciation_entries(self): - (_, _, depreciation_expense_account) = get_depreciation_accounts(self) + (_, _, depreciation_expense_account) = get_depreciation_accounts( + self.asset_category, self.company + ) gle = frappe.qb.DocType("GL Entry") @@ -1201,10 +1203,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non def make_journal_entry(asset_name): asset = frappe.get_doc("Asset", asset_name) ( - fixed_asset_account, + _, accumulated_depreciation_account, depreciation_expense_account, - ) = get_depreciation_accounts(asset) + ) = get_depreciation_accounts(asset.asset_category, asset.company) depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 80262c04d4a..f5fd5d60221 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder import Order +from frappe.query_builder.functions import Max, Min from frappe.utils import ( add_months, cint, @@ -36,9 +38,40 @@ def post_depreciation_entries(date=None): failed_asset_names = [] error_log_names = [] - for asset_name in get_depreciable_assets(date): + depreciable_assets = get_depreciable_assets(date) + + credit_and_debit_accounts_for_asset_category_and_company = {} + depreciation_cost_center_and_depreciation_series_for_company = ( + get_depreciation_cost_center_and_depreciation_series_for_company() + ) + + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + + for asset in depreciable_assets: + asset_name, asset_category, asset_company, sch_start_idx, sch_end_idx = asset + + if ( + asset_category, + asset_company, + ) not in credit_and_debit_accounts_for_asset_category_and_company: + credit_and_debit_accounts_for_asset_category_and_company.update( + { + (asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company( + asset_category, asset_company + ), + } + ) + try: - make_depreciation_entry(asset_name, date) + make_depreciation_entry( + asset_name, + date, + sch_start_idx, + sch_end_idx, + credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)], + depreciation_cost_center_and_depreciation_series_for_company[asset_company], + accounting_dimensions, + ) frappe.db.commit() except Exception as e: frappe.db.rollback() @@ -54,115 +87,226 @@ def post_depreciation_entries(date=None): def get_depreciable_assets(date): - return frappe.db.sql_list( - """select distinct a.name - from tabAsset a, `tabDepreciation Schedule` ds - where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 - and a.status in ('Submitted', 'Partially Depreciated') - and ifnull(ds.journal_entry, '')=''""", - date, + a = frappe.qb.DocType("Asset") + ds = frappe.qb.DocType("Depreciation Schedule") + + res = ( + frappe.qb.from_(a) + .join(ds) + .on(a.name == ds.parent) + .select(a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx)) + .where(a.calculate_depreciation == 1) + .where(a.docstatus == 1) + .where(a.status.isin(["Submitted", "Partially Depreciated"])) + .where(ds.journal_entry.isnull()) + .where(ds.schedule_date <= date) + .groupby(a.name) + .orderby(a.creation, order=Order.desc) ) + acc_frozen_upto = get_acc_frozen_upto() + if acc_frozen_upto: + res = res.where(ds.schedule_date > acc_frozen_upto) + + res = res.run() + + return res + + +def get_acc_frozen_upto(): + acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto") + + if not acc_frozen_upto: + return + + frozen_accounts_modifier = frappe.db.get_single_value( + "Accounts Settings", "frozen_accounts_modifier" + ) + + if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator": + return getdate(acc_frozen_upto) + + return + + +def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company): + ( + _, + accumulated_depreciation_account, + depreciation_expense_account, + ) = get_depreciation_accounts(asset_category, company) + + credit_account, debit_account = get_credit_and_debit_accounts( + accumulated_depreciation_account, depreciation_expense_account + ) + + return (credit_account, debit_account) + + +def get_depreciation_cost_center_and_depreciation_series_for_company(): + company_names = frappe.db.get_all("Company", pluck="name") + + res = {} + + for company_name in company_names: + depreciation_cost_center, depreciation_series = frappe.get_cached_value( + "Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) + res.update({company_name: (depreciation_cost_center, depreciation_series)}) + + return res + @frappe.whitelist() -def make_depreciation_entry(asset_name, date=None): +def make_depreciation_entry( + asset_name, + date=None, + sch_start_idx=None, + sch_end_idx=None, + credit_and_debit_accounts=None, + depreciation_cost_center_and_depreciation_series=None, + accounting_dimensions=None, +): frappe.has_permission("Journal Entry", throw=True) if not date: date = today() asset = frappe.get_doc("Asset", asset_name) - ( - fixed_asset_account, - accumulated_depreciation_account, - depreciation_expense_account, - ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.get_cached_value( - "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] - ) + if credit_and_debit_accounts: + credit_account, debit_account = credit_and_debit_accounts + else: + credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company( + asset.asset_category, asset.company + ) + + if depreciation_cost_center_and_depreciation_series: + depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series + else: + depreciation_cost_center, depreciation_series = frappe.get_cached_value( + "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) depreciation_cost_center = asset.cost_center or depreciation_cost_center - accounting_dimensions = get_checks_for_pl_and_bs_accounts() + if not accounting_dimensions: + accounting_dimensions = get_checks_for_pl_and_bs_accounts() - for d in asset.get("schedules"): - if not d.journal_entry and getdate(d.schedule_date) <= getdate(date): - je = frappe.new_doc("Journal Entry") - je.voucher_type = "Depreciation Entry" - je.naming_series = depreciation_series - je.posting_date = d.schedule_date - je.company = asset.company - je.finance_book = d.finance_book - je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) + depreciation_posting_error = None - credit_account, debit_account = get_credit_and_debit_accounts( - accumulated_depreciation_account, depreciation_expense_account + for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]: + try: + _make_journal_entry_for_depreciation( + asset, + date, + d, + sch_start_idx, + sch_end_idx, + depreciation_cost_center, + depreciation_series, + credit_account, + debit_account, + accounting_dimensions, ) - - credit_entry = { - "account": credit_account, - "credit_in_account_currency": d.depreciation_amount, - "reference_type": "Asset", - "reference_name": asset.name, - "cost_center": depreciation_cost_center, - } - - debit_entry = { - "account": debit_account, - "debit_in_account_currency": d.depreciation_amount, - "reference_type": "Asset", - "reference_name": asset.name, - "cost_center": depreciation_cost_center, - } - - for dimension in accounting_dimensions: - if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"): - credit_entry.update( - { - dimension["fieldname"]: asset.get(dimension["fieldname"]) - or dimension.get("default_dimension") - } - ) - - if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"): - debit_entry.update( - { - dimension["fieldname"]: asset.get(dimension["fieldname"]) - or dimension.get("default_dimension") - } - ) - - je.append("accounts", credit_entry) - - je.append("accounts", debit_entry) - - je.flags.ignore_permissions = True - je.flags.planned_depr_entry = True - je.save() - - d.db_set("journal_entry", je.name) - - if not je.meta.get_workflow(): - je.submit() - idx = cint(d.finance_book_id) - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation -= d.depreciation_amount - finance_books.db_update() - - asset.db_set("depr_entry_posting_status", "Successful") + frappe.db.commit() + except Exception as e: + frappe.db.rollback() + depreciation_posting_error = e asset.set_status() - return asset + if not depreciation_posting_error: + asset.db_set("depr_entry_posting_status", "Successful") + return asset + + raise depreciation_posting_error -def get_depreciation_accounts(asset): +def _make_journal_entry_for_depreciation( + asset, + date, + depr_schedule, + sch_start_idx, + sch_end_idx, + depreciation_cost_center, + depreciation_series, + credit_account, + debit_account, + accounting_dimensions, +): + if not (sch_start_idx and sch_end_idx) and not ( + not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date) + ): + return + + je = frappe.new_doc("Journal Entry") + je.voucher_type = "Depreciation Entry" + je.naming_series = depreciation_series + je.posting_date = depr_schedule.schedule_date + je.company = asset.company + je.finance_book = depr_schedule.finance_book + je.remark = "Depreciation Entry against {0} worth {1}".format( + asset.name, depr_schedule.depreciation_amount + ) + + credit_entry = { + "account": credit_account, + "credit_in_account_currency": depr_schedule.depreciation_amount, + "reference_type": "Asset", + "reference_name": asset.name, + "cost_center": depreciation_cost_center, + } + + debit_entry = { + "account": debit_account, + "debit_in_account_currency": depr_schedule.depreciation_amount, + "reference_type": "Asset", + "reference_name": asset.name, + "cost_center": depreciation_cost_center, + } + + for dimension in accounting_dimensions: + if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"): + credit_entry.update( + { + dimension["fieldname"]: asset.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) + + if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"): + debit_entry.update( + { + dimension["fieldname"]: asset.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) + + je.append("accounts", credit_entry) + + je.append("accounts", debit_entry) + + je.flags.ignore_permissions = True + je.flags.planned_depr_entry = True + je.save() + + depr_schedule.db_set("journal_entry", je.name) + + if not je.meta.get_workflow(): + je.submit() + idx = cint(depr_schedule.finance_book_id) + finance_books = asset.get("finance_books")[idx - 1] + finance_books.value_after_depreciation -= depr_schedule.depreciation_amount + finance_books.db_update() + + +def get_depreciation_accounts(asset_category, company): fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None accounts = frappe.db.get_value( "Asset Category Account", - filters={"parent": asset.asset_category, "company_name": asset.company}, + filters={"parent": asset_category, "company_name": company}, fieldname=[ "fixed_asset_account", "accumulated_depreciation_account", @@ -178,7 +322,7 @@ def get_depreciation_accounts(asset): if not accumulated_depreciation_account or not depreciation_expense_account: accounts = frappe.get_cached_value( - "Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"] + "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] ) if not accumulated_depreciation_account: @@ -193,7 +337,7 @@ def get_depreciation_accounts(asset): ): frappe.throw( _("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format( - asset.asset_category, asset.company + asset_category, company ) ) @@ -533,8 +677,8 @@ def get_gl_entries_on_asset_disposal( def get_asset_details(asset, finance_book=None): - fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts( - asset + fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts( + asset.asset_category, asset.company ) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index ee75b83af39..9928b2f5f38 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -50,10 +50,10 @@ class AssetValueAdjustment(Document): def make_depreciation_entry(self): asset = frappe.get_doc("Asset", self.asset) ( - fixed_asset_account, + _, accumulated_depreciation_account, depreciation_expense_account, - ) = get_depreciation_accounts(asset) + ) = get_depreciation_accounts(asset.asset_category, asset.company) depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6b7d2dc26a2..b2a76f2038c 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -432,7 +432,6 @@ scheduler_events = { "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", "erpnext.projects.doctype.task.task.set_tasks_as_overdue", - "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", "erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status", "erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards", "erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history", @@ -459,6 +458,7 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", + "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", From 00b9df0bc5199d6c08d14cf7dd4f9f4d01ec93c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:04:52 +0530 Subject: [PATCH 29/31] fix: stock reconciliation negative stock error (backport #36544) (#36549) fix: stock reconciliation negative stock error (#36544) fix: stock reco negative stock error (cherry picked from commit 0b36e7d10ec42a4baac2af8f41a2817ed06b711d) Co-authored-by: rohitwaghchaure --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3fd4cec5d88..c6c8571b9ef 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -607,7 +607,7 @@ class StockReconciliation(StockController): ) if sl_entries: - self.make_sl_entries(sl_entries) + self.make_sl_entries(sl_entries, allow_negative_stock=True) def get_batch_qty_for_stock_reco( From 0e87c86aab93255f57d7b691f7c4843c0274ba70 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:04:08 +0530 Subject: [PATCH 30/31] fix: payment allocation in invoice payment schedule (#36440) * fix: payment allocation in invoice payment schedule (#36440) * fix: payment allocation in invoice payment schedule * test: payment allocation for payment terms * chore: linting issues (cherry picked from commit edbefee10ca779f1d81153110c6085dd04d9c769) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py * chore: resolve conflicts --------- Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Co-authored-by: Deepesh Garg --- .../purchase_invoice/test_purchase_invoice.py | 46 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 5 ++ 2 files changed, 51 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e8766275f0b..ab2e3cf103c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1670,6 +1670,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(returned_inv.items[0].rate, rate) + def test_payment_allocation_for_payment_terms(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, + ) + from erpnext.selling.doctype.sales_order.test_sales_order import ( + automatically_fetch_payment_terms, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_pi_from_pr, + ) + + automatically_fetch_payment_terms() + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + + po = create_purchase_order(do_not_save=1) + po.payment_terms_template = "_Test Payment Term Template" + po.save() + po.submit() + + pr = create_pr_against_po(po.name, received_qty=4) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) + + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 1, + ) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 2500) + + automatically_fetch_payment_terms(enable=0) + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f4b6e01cc4b..4eb2597c668 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1672,8 +1672,13 @@ class AccountsController(TransactionBase): ) self.append("payment_schedule", data) + allocate_payment_based_on_payment_terms = frappe.db.get_value( + "Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms" + ) + if not ( automatically_fetch_payment_terms + and allocate_payment_based_on_payment_terms and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) ): for d in self.get("payment_schedule"): From 240d866ef495e09e4da8a3064c33bcccd319cb27 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:04:47 +0530 Subject: [PATCH 31/31] fix: Debit credit difference while submitting Sales Invoice (#36523) * fix: Debit credit difference while submitting Sales Invoice (#36523) * fix: Debit credit difference while submitting Sales Invoice * test(fix): Update gl entry comparison * test(fix): Update gl entry comparison (cherry picked from commit 492ea3bcc820fe14dfde3a0026397d3b5e9e4179) # Conflicts: # erpnext/controllers/accounts_controller.py * chore: resolve conflicts --------- Co-authored-by: Deepesh Garg --- .../purchase_invoice/purchase_invoice.py | 24 -------------- .../doctype/sales_invoice/sales_invoice.py | 1 + .../sales_invoice/test_sales_invoice.py | 32 +++++++++---------- erpnext/controllers/accounts_controller.py | 28 ++++++++++++++++ 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d791232c0af..5a7ff1c0d1c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -976,30 +976,6 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount, item.precision("item_tax_amount") ) - def make_precision_loss_gl_entry(self, gl_entries): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center - ) - - precision_loss = self.get("base_net_total") - flt( - self.get("net_total") * self.conversion_rate, self.precision("net_total") - ) - - if precision_loss: - gl_entries.append( - self.get_gl_dict( - { - "account": round_off_account, - "against": self.supplier, - "credit": precision_loss, - "cost_center": round_off_cost_center - if self.use_company_roundoff_cost_center - else self.cost_center or round_off_cost_center, - "remarks": _("Net total calculation precision loss"), - } - ) - ) - def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 15774331272..03c0712d632 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1075,6 +1075,7 @@ class SalesInvoice(SellingController): self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_precision_loss_gl_entry(gl_entries) self.make_discount_gl_entries(gl_entries) # merge gl entries before adding pos entries diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index fd5ca8b1eba..277e584aeaf 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2049,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 228.82) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = dict( - (d[0], d) - for d in [ - [si.debit_to, 1500, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 114.41], - ["_Test Account VAT - _TC", 0.0, 114.41], - ["Sales - _TC", 0.0, 1271.18], - ] - ) + expected_values = [ + ["_Test Account Service Tax - _TC", 0.0, 114.41], + ["_Test Account VAT - _TC", 0.0, 114.41], + [si.debit_to, 1500, 0.0], + ["Round Off - _TC", 0.01, 0.01], + ["Sales - _TC", 0.0, 1271.18], + ] gl_entries = frappe.db.sql( - """select account, debit, credit + """select account, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + group by account order by account asc""", si.name, as_dict=1, ) - for gle in gl_entries: - self.assertEqual(expected_values[gle.account][0], gle.account) - self.assertEqual(expected_values[gle.account][1], gle.debit) - self.assertEqual(expected_values[gle.account][2], gle.credit) + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_values[i][0], gle.account) + self.assertEqual(expected_values[i][1], gle.debit) + self.assertEqual(expected_values[i][2], gle.credit) def test_rounding_adjustment_3(self): from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( @@ -2125,13 +2124,14 @@ class TestSalesInvoice(unittest.TestCase): ["_Test Account Service Tax - _TC", 0.0, 240.43], ["_Test Account VAT - _TC", 0.0, 240.43], ["Sales - _TC", 0.0, 4007.15], - ["Round Off - _TC", 0.01, 0], + ["Round Off - _TC", 0.02, 0.01], ] ) gl_entries = frappe.db.sql( - """select account, debit, credit + """select account, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + group by account order by account asc""", si.name, as_dict=1, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4eb2597c668..9912dd47f8b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -31,6 +31,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import ( apply_pricing_rule_on_transaction, get_applied_pricing_rules, ) +from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import ( get_party_account, get_party_account_currency, @@ -1023,6 +1024,33 @@ class AccountsController(TransactionBase): ) ) + def make_precision_loss_gl_entry(self, gl_entries): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center + ) + + precision_loss = self.get("base_net_total") - flt( + self.get("net_total") * self.conversion_rate, self.precision("net_total") + ) + + credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit" + against = self.supplier if self.doctype == "Purchase Invoice" else self.customer + + if precision_loss: + gl_entries.append( + self.get_gl_dict( + { + "account": round_off_account, + "against": against, + credit_or_debit: precision_loss, + "cost_center": round_off_cost_center + if self.use_company_roundoff_cost_center + else self.cost_center or round_off_cost_center, + "remarks": _("Net total calculation precision loss"), + } + ) + ) + def update_against_document_in_jv(self): """ Links invoice and advance voucher: