From cc07402b5e5f2acdb5d4626a38108799cdd09f68 Mon Sep 17 00:00:00 2001 From: Varun Verma Date: Mon, 14 Oct 2024 11:09:26 +0000 Subject: [PATCH 01/46] fix: bulk update invoice remarks during site upgrade fixes issue #43634 --- .../patches/v15_0/update_invoice_remarks.py | 164 ++++++++++++++---- 1 file changed, 131 insertions(+), 33 deletions(-) diff --git a/erpnext/patches/v15_0/update_invoice_remarks.py b/erpnext/patches/v15_0/update_invoice_remarks.py index 7060fe57e31..9146713815f 100644 --- a/erpnext/patches/v15_0/update_invoice_remarks.py +++ b/erpnext/patches/v15_0/update_invoice_remarks.py @@ -8,44 +8,142 @@ def execute(): def update_sales_invoice_remarks(): - si_list = frappe.db.get_all( - "Sales Invoice", - filters={ - "docstatus": 1, - "remarks": "No Remarks", - "po_no": ["!=", ""], - }, - fields=["name", "po_no"], - ) + """ + Update remarks in Sales Invoice. + Some sites may have very large volume of sales invoices. + In such cases, updating documents one by one won't be successful, especially during site migration step. + Refer to the bug report: https://github.com/frappe/erpnext/issues/43634 + In this case, a bulk update must be done. - for doc in si_list: - remarks = _("Against Customer Order {0}").format(doc.po_no) - update_remarks("Sales Invoice", doc.name, remarks) + Step 1: Update remarks in GL Entries + Step 2: Update remarks in Payment Ledger Entries + Step 3: Update remarks in Sales Invoice - Should be last step + """ + + ### Step 1: Update remarks in GL Entries + update_sales_invoice_gle_remarks() + + ### Step 2: Update remarks in Payment Ledger Entries + update_sales_invoice_ple_remarks() + + ### Step 3: Update remarks in Sales Invoice + update_query = """ + UPDATE `tabSales Invoice` + SET remarks = concat('Against Customer Order ', po_no) + WHERE po_no <> '' AND docstatus = %(docstatus)s and remarks = %(remarks)s + """ + + # Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + # Execute query + frappe.db.sql(update_query, values=values, as_dict=0) def update_purchase_invoice_remarks(): - pi_list = frappe.db.get_all( - "Purchase Invoice", - filters={ - "docstatus": 1, - "remarks": "No Remarks", - "bill_no": ["!=", ""], - }, - fields=["name", "bill_no"], - ) + """ + Update remarks in Purchase Invoice. + Some sites may have very large volume of purchase invoices. + In such cases, updating documents one by one wont be successful, especially during site migration step. + Refer to the bug report: https://github.com/frappe/erpnext/issues/43634 + In this case, a bulk update must be done. - for doc in pi_list: - remarks = _("Against Supplier Invoice {0}").format(doc.bill_no) - update_remarks("Purchase Invoice", doc.name, remarks) + Step 1: Update remarks in GL Entries + Step 2: Update remarks in Payment Ledger Entries + Step 3: Update remarks in Purchase Invoice - Should be last step + """ + + ### Step 1: Update remarks in GL Entries + update_purchase_invoice_gle_remarks() + + ### Step 2: Update remarks in Payment Ledger Entries + update_purchase_invoice_ple_remarks() + + ### Step 3: Update remarks in Purchase Invoice + update_query = """ + UPDATE `tabPurchase Invoice` + SET remarks = concat('Against Supplier Invoice ', bill_no) + WHERE bill_no <> '' AND docstatus = %(docstatus)s and remarks = %(remarks)s + """ + + # Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + # Execute query + frappe.db.sql(update_query, values=values, as_dict=0) -def update_remarks(doctype, docname, remarks): - filters = { - "voucher_type": doctype, - "remarks": "No Remarks", - "voucher_no": docname, - } +def update_sales_invoice_gle_remarks(): + ## Update query to update GL Entry - Updates all entries which are for Sales Invoice with No Remarks + update_query = """ + UPDATE + `tabGL Entry` as gle + INNER JOIN `tabSales Invoice` as si + ON gle.voucher_type = 'Sales Invoice' AND gle.voucher_no = si.name AND gle.remarks = %(remarks)s + SET + gle.remarks = concat('Against Customer Order ', si.po_no) + WHERE si.po_no <> '' AND si.docstatus = %(docstatus)s and si.remarks = %(remarks)s + """ - frappe.db.set_value(doctype, docname, "remarks", remarks) - frappe.db.set_value("GL Entry", filters, "remarks", remarks) - frappe.db.set_value("Payment Ledger Entry", filters, "remarks", remarks) + # Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + # Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_sales_invoice_ple_remarks(): + ## Update query to update Payment Ledger Entry - Updates all entries which are for Sales Invoice with No Remarks + update_query = """ + UPDATE + `tabPayment Ledger Entry` as ple + INNER JOIN `tabSales Invoice` as si + ON ple.voucher_type = 'Sales Invoice' AND ple.voucher_no = si.name AND ple.remarks = %(remarks)s + SET + ple.remarks = concat('Against Customer Order ', si.po_no) + WHERE si.po_no <> '' AND si.docstatus = %(docstatus)s and si.remarks = %(remarks)s + """ + + ### Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + ### Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_purchase_invoice_gle_remarks(): + ### Query to update GL Entry - Updates all entries which are for Purchase Invoice with No Remarks + update_query = """ + UPDATE + `tabGL Entry` as gle + INNER JOIN `tabPurchase Invoice` as pi + ON gle.voucher_type = 'Purchase Invoice' AND gle.voucher_no = pi.name AND gle.remarks = %(remarks)s + SET + gle.remarks = concat('Against Supplier Invoice ', pi.bill_no) + WHERE pi.bill_no <> '' AND pi.docstatus = %(docstatus)s and pi.remarks = %(remarks)s + """ + + ### Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + ### Execute query + frappe.db.sql(update_query, values=values, as_dict=0) + + +def update_purchase_invoice_ple_remarks(): + ### Query to update Payment Ledger Entry - Updates all entries which are for Purchase Invoice with No Remarks + update_query = """ + UPDATE + `tabPayment Ledger Entry` as ple + INNER JOIN `tabPurchase Invoice` as pi + ON ple.voucher_type = 'Purchase Invoice' AND ple.voucher_no = pi.name AND ple.remarks = %(remarks)s + SET + ple.remarks = concat('Against Supplier Invoice ', pi.bill_no) + WHERE pi.bill_no <> '' AND pi.docstatus = %(docstatus)s and pi.remarks = %(remarks)s + """ + + ### Data for update query + values = {"remarks": "No Remarks", "docstatus": 1} + + ### Execute query + frappe.db.sql(update_query, values=values, as_dict=0) From 8e6249d361bb36a4fd1ef0cfa7333265bb8933d5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 Aug 2024 11:35:15 +0530 Subject: [PATCH 02/46] feat: round off for opening entries (cherry picked from commit a5b228549c0c03517a53db8609f34fb6ab308445) --- erpnext/setup/doctype/company/company.json | 9 ++++++++- erpnext/setup/doctype/company/company.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 284bd2b7f22..4b07037ad3e 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -49,6 +49,7 @@ "default_cash_account", "default_receivable_account", "round_off_account", + "round_off_for_opening", "round_off_cost_center", "write_off_account", "exchange_gain_loss_account", @@ -801,6 +802,12 @@ "fieldtype": "Link", "label": "Default Operating Cost Account", "options": "Account" + }, + { + "fieldname": "round_off_for_opening", + "fieldtype": "Link", + "label": "Round Off for Opening", + "options": "Account" } ], "icon": "fa fa-building", @@ -808,7 +815,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2024-07-24 18:17:56.413971", + "modified": "2024-08-02 11:34:46.785377", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 8028b8e6af4..d781288c8bd 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -91,6 +91,7 @@ class Company(NestedSet): rgt: DF.Int round_off_account: DF.Link | None round_off_cost_center: DF.Link | None + round_off_for_opening: DF.Link | None sales_monthly_history: DF.SmallText | None series_for_depreciation_entry: DF.Data | None stock_adjustment_account: DF.Link | None From 9a3e9c4c9a627cac552a95ff880469ef3888c56f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 Aug 2024 11:49:50 +0530 Subject: [PATCH 03/46] refactor: use separate round off for opening entries (cherry picked from commit 88e68168e36624d9ec31993c8f7a9b29add8c1ce) # Conflicts: # erpnext/accounts/general_ledger.py --- erpnext/accounts/doctype/account/account.json | 4 +- erpnext/accounts/doctype/account/account.py | 1 + .../purchase_invoice/purchase_invoice.py | 6 ++- .../doctype/sales_invoice/sales_invoice.py | 6 ++- erpnext/accounts/general_ledger.py | 52 ++++++++++++++++--- erpnext/controllers/accounts_controller.py | 6 ++- erpnext/setup/doctype/company/company.js | 1 + 7 files changed, 63 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index e87b59ea9cb..7b56444e635 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -121,7 +121,7 @@ "label": "Account Type", "oldfieldname": "account_type", "oldfieldtype": "Select", - "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", + "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", "search_index": 1 }, { @@ -191,7 +191,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2024-06-27 16:23:04.444354", + "modified": "2024-08-19 15:19:11.095045", "modified_by": "Administrator", "module": "Accounts", "name": "Account", diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 2c876e09725..b510651e68f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -60,6 +60,7 @@ class Account(NestedSet): "Payable", "Receivable", "Round Off", + "Round Off for Opening", "Stock", "Stock Adjustment", "Stock Received But Not Billed", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index b47e90eb77d..dc4051eecf4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1537,7 +1537,11 @@ class PurchaseInvoice(BuyingController): # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 # then base_rounding_adjustment becomes zero and error is thrown in GL Entry if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment: - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + ( + round_off_account, + round_off_cost_center, + round_off_for_opening, + ) = get_round_off_account_and_cost_center( self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index eb43de47a54..d24717a614d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1633,7 +1633,11 @@ class SalesInvoice(SellingController): and self.base_rounding_adjustment and not self.is_internal_transfer() ): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + ( + round_off_account, + round_off_cost_center, + round_off_for_opening, + ) = get_round_off_account_and_cost_center( self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center ) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f9b503675aa..b3d78284616 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -7,7 +7,12 @@ import copy import frappe from frappe import _ from frappe.model.meta import get_field_precision +<<<<<<< HEAD from frappe.utils import cint, flt, formatdate, getdate, now +======= +from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now +from frappe.utils.dashboard import cache_source +>>>>>>> 88e68168e3 (refactor: use separate round off for opening entries) import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -496,16 +501,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_ ) +def has_opening_entries(gl_map: list) -> bool: + for x in gl_map: + if x.is_opening == "Yes": + return True + return False + + def make_round_off_gle(gl_map, debit_credit_diff, precision): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center( gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no ) round_off_gle = frappe._dict() round_off_account_exists = False + has_opening_entry = has_opening_entries(gl_map) + + if has_opening_entry: + if not round_off_for_opening: + frappe.throw( + _("Please set '{0}' in Company: {1}").format( + frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company) + ) + ) + + account = round_off_for_opening + else: + account = round_off_account if gl_map[0].voucher_type != "Period Closing Voucher": for d in gl_map: - if d.account == round_off_account: + if d.account == account: round_off_gle = d if d.debit: debit_credit_diff -= flt(d.debit) - flt(d.credit) @@ -523,7 +548,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): round_off_gle.update( { - "account": round_off_account, + "account": account, "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0, "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, @@ -537,6 +562,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): } ) + if has_opening_entry: + round_off_gle.update({"is_opening": "Yes"}) + update_accounting_dimensions(round_off_gle) if not round_off_account_exists: gl_map.append(round_off_gle) @@ -561,8 +589,8 @@ def update_accounting_dimensions(round_off_gle): def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): - round_off_account, round_off_cost_center = frappe.get_cached_value( - "Company", company, ["round_off_account", "round_off_cost_center"] + round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value( + "Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"] ) or [None, None] # Use expense account as fallback @@ -578,12 +606,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use round_off_cost_center = parent_cost_center if not round_off_account: - frappe.throw(_("Please mention Round Off Account in Company")) + frappe.throw( + _("Please mention '{0}' in Company: {1}").format( + frappe.bold("Round Off Account"), get_link_to_form("Company", company) + ) + ) if not round_off_cost_center: - frappe.throw(_("Please mention Round Off Cost Center in Company")) + frappe.throw( + _("Please mention '{0}' in Company: {1}").format( + frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company) + ) + ) - return round_off_account, round_off_cost_center + return round_off_account, round_off_cost_center, round_off_for_opening def make_reverse_gl_entries( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b14cf428c53..b4b23dd5f4c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1298,7 +1298,11 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference def make_precision_loss_gl_entry(self, gl_entries): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + ( + round_off_account, + round_off_cost_center, + round_off_for_opening, + ) = get_round_off_account_and_cost_center( self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index f14057a272e..52ff21dc407 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -252,6 +252,7 @@ erpnext.company.setup_queries = function (frm) { ["default_expense_account", { root_type: "Expense" }], ["default_income_account", { root_type: "Income" }], ["round_off_account", { root_type: "Expense" }], + ["round_off_for_opening", { root_type: "Liability" }], ["write_off_account", { root_type: "Expense" }], ["default_deferred_expense_account", {}], ["default_deferred_revenue_account", {}], From b28ff25180ae95fc737708c50b4917f18f82cb1f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 29 Aug 2024 17:37:32 +0530 Subject: [PATCH 04/46] chore: default should return 3 elements (cherry picked from commit fc46ebcd7c9a3628ddd7b7a1f284d2c2d9d6b7f9) --- erpnext/accounts/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index b3d78284616..c2fbdcc95f1 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -591,7 +591,7 @@ def update_accounting_dimensions(round_off_gle): def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False): round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"] - ) or [None, None] + ) or [None, None, None] # Use expense account as fallback if not round_off_account: From 186b646dee3038cb78311ec99e6f5f292c50a21d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 30 Aug 2024 16:08:43 +0530 Subject: [PATCH 05/46] refactor: handle opening round off from sales invoice (cherry picked from commit 96e3c2ad1061af44b044fa64b0af16004d0d732e) --- .../doctype/sales_invoice/sales_invoice.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d24717a614d..8baa36475da 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1641,6 +1641,21 @@ class SalesInvoice(SellingController): self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center ) + if self.is_opening == "Yes" and self.rounding_adjustment: + if not round_off_for_opening: + frappe.throw( + _( + "Opening Invoice has rounding adjustment of {0}.

'{1}' account is required to post these values. Please set it in Company: {2}.

Or, '{3}' can be enabled to not post any rounding adjustment." + ).format( + frappe.bold(self.rounding_adjustment), + frappe.bold("Round Off for Opening"), + get_link_to_form("Company", self.company), + frappe.bold("Disable Rounded Total"), + ) + ) + else: + round_off_account = round_off_for_opening + gl_entries.append( self.get_gl_dict( { From 820692f24647ca6f47370594160cb1e204c66aa7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 30 Aug 2024 17:55:02 +0530 Subject: [PATCH 06/46] test: rounding adjustment validation and posting (cherry picked from commit 5021c7ca2c3bb5f758d00c65eab1dcfec692d2a4) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 56f90ae8cd4..d9d7d5fa3ba 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3924,6 +3924,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(len(res), 1) self.assertEqual(res[0][0], pos_return.return_against) +<<<<<<< HEAD @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) def test_common_party_with_foreign_currency_jv(self): from erpnext.accounts.doctype.account.test_account import create_account @@ -4032,6 +4033,57 @@ class TestSalesInvoice(FrappeTestCase): ) self.assertTrue(all([x == "Credit Note" for x in gl_entries])) +======= + def test_validation_on_opening_invoice_with_rounding(self): + si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + si.save() + self.assertRaises(frappe.ValidationError, si.submit) + + def test_opening_invoice_with_rounding_adjustment(self): + si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + si.save() + + liability_root = frappe.db.get_all( + "Account", + filters={"company": si.company, "root_type": "Liability", "disabled": 0}, + order_by="lft", + limit=1, + )[0] + + # setup round off account + company = frappe.get_doc("Company", si.company) + if acc := frappe.db.exists( + "Account", + { + "account_name": "Round Off for Opening", + "account_type": "Round Off for Opening", + "company": si.company, + }, + ): + company.round_off_for_opening = acc + else: + acc = frappe.new_doc("Account") + acc.company = si.company + acc.parent_account = liability_root.name + acc.account_name = "Round Off for Opening" + acc.account_type = "Round Off for Opening" + acc.save() + company.round_off_for_opening = acc.name + company.save() + + si.reload() + si.submit() + res = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": si.name, "is_opening": "Yes"}, + fields=["account", "debit", "credit", "is_opening"], + ) + self.assertEqual(len(res), 3) +>>>>>>> 5021c7ca2c (test: rounding adjustment validation and posting) def set_advance_flag(company, flag, default_account): From da2f6a045aa637514754bbd342bac48ce5847521 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Sep 2024 16:33:55 +0530 Subject: [PATCH 07/46] test: opening round off with inclusive tax (cherry picked from commit 79267358d0fc95a10c9bcd03307ba1284e561526) --- .../sales_invoice/test_sales_invoice.py | 106 +++++++++++++----- 1 file changed, 79 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d9d7d5fa3ba..15bc234fec1 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4041,39 +4041,40 @@ class TestSalesInvoice(FrappeTestCase): si.save() self.assertRaises(frappe.ValidationError, si.submit) + def _create_opening_roundoff_account(self, company_name): + liability_root = frappe.db.get_all( + "Account", + filters={"company": company_name, "root_type": "Liability", "disabled": 0}, + order_by="lft", + limit=1, + )[0] + + # setup round off account + if acc := frappe.db.exists( + "Account", + { + "account_name": "Round Off for Opening", + "account_type": "Round Off for Opening", + "company": company_name, + }, + ): + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc) + else: + acc = frappe.new_doc("Account") + acc.company = company_name + acc.parent_account = liability_root.name + acc.account_name = "Round Off for Opening" + acc.account_type = "Round Off for Opening" + acc.save() + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name) + def test_opening_invoice_with_rounding_adjustment(self): si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) si.is_opening = "Yes" si.items[0].income_account = "Temporary Opening - _TC" si.save() - liability_root = frappe.db.get_all( - "Account", - filters={"company": si.company, "root_type": "Liability", "disabled": 0}, - order_by="lft", - limit=1, - )[0] - - # setup round off account - company = frappe.get_doc("Company", si.company) - if acc := frappe.db.exists( - "Account", - { - "account_name": "Round Off for Opening", - "account_type": "Round Off for Opening", - "company": si.company, - }, - ): - company.round_off_for_opening = acc - else: - acc = frappe.new_doc("Account") - acc.company = si.company - acc.parent_account = liability_root.name - acc.account_name = "Round Off for Opening" - acc.account_type = "Round Off for Opening" - acc.save() - company.round_off_for_opening = acc.name - company.save() + self._create_opening_roundoff_account(si.company) si.reload() si.submit() @@ -4085,6 +4086,57 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(len(res), 3) >>>>>>> 5021c7ca2c (test: rounding adjustment validation and posting) + def _create_opening_invoice_with_inclusive_tax(self): + si = create_sales_invoice(qty=1, rate=90, do_not_submit=True) + si.is_opening = "Yes" + si.items[0].income_account = "Temporary Opening - _TC" + item_template = si.items[0].as_dict() + item_template.name = None + item_template.rate = 55 + si.append("items", item_template) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Testing...", + "rate": 5, + "included_in_print_rate": True, + }, + ) + # there will be 0.01 precision loss between Dr and Cr + # caused by 'included_in_print_tax' option + si.save() + return si + + def test_rounding_validation_for_opening_with_inclusive_tax(self): + si = self._create_opening_invoice_with_inclusive_tax() + # 'Round Off for Opening' not set in Company master + # Ledger level validation must be thrown + self.assertRaises(frappe.ValidationError, si.submit) + + def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self): + si = self._create_opening_invoice_with_inclusive_tax() + # 'Round Off for Opening' is set in Company master + self._create_opening_roundoff_account(si.company) + + si.submit() + actual = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": si.name, "is_opening": "Yes"}, + fields=["account", "debit", "credit", "is_opening"], + order_by="account,debit", + ) + expected = [ + {"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"}, + {"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"}, + {"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"}, + {"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"}, + ] + self.assertEqual(len(actual), 4) + self.assertEqual(expected, actual) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From 7eb4b422808e5df5353e169279a2356a454cd69f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Sep 2024 17:52:28 +0530 Subject: [PATCH 08/46] refactor: filter on account_type (cherry picked from commit 193ea9ad8f29358820fff45698772ea5adeffcfa) --- erpnext/setup/doctype/company/company.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 52ff21dc407..72d28a705ad 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -252,7 +252,7 @@ erpnext.company.setup_queries = function (frm) { ["default_expense_account", { root_type: "Expense" }], ["default_income_account", { root_type: "Income" }], ["round_off_account", { root_type: "Expense" }], - ["round_off_for_opening", { root_type: "Liability" }], + ["round_off_for_opening", { root_type: "Liability", account_type: "Round Off for Opening" }], ["write_off_account", { root_type: "Expense" }], ["default_deferred_expense_account", {}], ["default_deferred_revenue_account", {}], From 8bec67cbcfb514dda6ba66f6bf0df07841bb8026 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Sep 2024 17:53:02 +0530 Subject: [PATCH 09/46] refactor: handle opening round off on purchase invoice (cherry picked from commit a5d6a25a9654ebe7f91285b9266e9b7cd9797545) --- .../doctype/purchase_invoice/purchase_invoice.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index dc4051eecf4..ebc4efc08a0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1545,6 +1545,21 @@ class PurchaseInvoice(BuyingController): self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) + if self.is_opening == "Yes" and self.rounding_adjustment: + if not round_off_for_opening: + frappe.throw( + _( + "Opening Invoice has rounding adjustment of {0}.

'{1}' account is required to post these values. Please set it in Company: {2}.

Or, '{3}' can be enabled to not post any rounding adjustment." + ).format( + frappe.bold(self.rounding_adjustment), + frappe.bold("Round Off for Opening"), + get_link_to_form("Company", self.company), + frappe.bold("Disable Rounded Total"), + ) + ) + else: + round_off_account = round_off_for_opening + gl_entries.append( self.get_gl_dict( { From 9bfd5cdb2b8999fd5e2140c79338542f135f77ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Sep 2024 11:04:26 +0530 Subject: [PATCH 10/46] test: opening purchase invoice with rounding adjustment (cherry picked from commit b7edc6dea908b887041998801a3d7724fc8777f1) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py --- .../purchase_invoice/test_purchase_invoice.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f5835deb0d0..f234157d949 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2347,6 +2347,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) +<<<<<<< HEAD def test_last_purchase_rate(self): item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1) pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100) @@ -2364,6 +2365,66 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi1.cancel() item.reload() self.assertEqual(item.last_purchase_rate, 0) +======= + def test_opening_invoice_rounding_adjustment_validation(self): + pi = make_purchase_invoice(do_not_save=1) + pi.items[0].rate = 99.98 + pi.items[0].qty = 1 + pi.items[0].expense_account = "Temporary Opening - _TC" + pi.is_opening = "Yes" + pi.save() + self.assertRaises(frappe.ValidationError, pi.submit) + + def _create_opening_roundoff_account(self, company_name): + liability_root = frappe.db.get_all( + "Account", + filters={"company": company_name, "root_type": "Liability", "disabled": 0}, + order_by="lft", + limit=1, + )[0] + + # setup round off account + if acc := frappe.db.exists( + "Account", + { + "account_name": "Round Off for Opening", + "account_type": "Round Off for Opening", + "company": company_name, + }, + ): + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc) + else: + acc = frappe.new_doc("Account") + acc.company = company_name + acc.parent_account = liability_root.name + acc.account_name = "Round Off for Opening" + acc.account_type = "Round Off for Opening" + acc.save() + frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name) + + def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self): + pi = make_purchase_invoice(do_not_save=1) + pi.items[0].rate = 99.98 + pi.items[0].qty = 1 + pi.items[0].expense_account = "Temporary Opening - _TC" + pi.is_opening = "Yes" + pi.save() + self._create_opening_roundoff_account(pi.company) + pi.submit() + actual = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False}, + fields=["account", "debit", "credit", "is_opening"], + order_by="account,debit", + ) + expected = [ + {"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"}, + {"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"}, + {"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"}, + ] + self.assertEqual(len(actual), 3) + self.assertEqual(expected, actual) +>>>>>>> b7edc6dea9 (test: opening purchase invoice with rounding adjustment) def set_advance_flag(company, flag, default_account): From ba79560c0c5f4b214c9b52061fac9762c3f6b363 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Sep 2024 11:05:34 +0530 Subject: [PATCH 11/46] refactor(test): filter for active ledger entries (cherry picked from commit cf11ac87fb7a9bb15518204900d776e06af60aaf) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 15bc234fec1..adbab43001b 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4124,7 +4124,7 @@ class TestSalesInvoice(FrappeTestCase): si.submit() actual = frappe.db.get_all( "GL Entry", - filters={"voucher_no": si.name, "is_opening": "Yes"}, + filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False}, fields=["account", "debit", "credit", "is_opening"], order_by="account,debit", ) From 9598b1fc0fdf4c8be6145d0a1d4a9130635b595e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Nov 2024 12:35:41 +0530 Subject: [PATCH 12/46] chore: resolve conflicts --- .../doctype/purchase_invoice/test_purchase_invoice.py | 4 +--- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 +--- erpnext/accounts/general_ledger.py | 5 ----- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f234157d949..f0b51c32c05 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2347,7 +2347,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) -<<<<<<< HEAD def test_last_purchase_rate(self): item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1) pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100) @@ -2365,7 +2364,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): pi1.cancel() item.reload() self.assertEqual(item.last_purchase_rate, 0) -======= + def test_opening_invoice_rounding_adjustment_validation(self): pi = make_purchase_invoice(do_not_save=1) pi.items[0].rate = 99.98 @@ -2424,7 +2423,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): ] self.assertEqual(len(actual), 3) self.assertEqual(expected, actual) ->>>>>>> b7edc6dea9 (test: opening purchase invoice with rounding adjustment) def set_advance_flag(company, flag, default_account): diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index adbab43001b..90bec018257 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3924,7 +3924,6 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(len(res), 1) self.assertEqual(res[0][0], pos_return.return_against) -<<<<<<< HEAD @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) def test_common_party_with_foreign_currency_jv(self): from erpnext.accounts.doctype.account.test_account import create_account @@ -4033,7 +4032,7 @@ class TestSalesInvoice(FrappeTestCase): ) self.assertTrue(all([x == "Credit Note" for x in gl_entries])) -======= + def test_validation_on_opening_invoice_with_rounding(self): si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True) si.is_opening = "Yes" @@ -4084,7 +4083,6 @@ class TestSalesInvoice(FrappeTestCase): fields=["account", "debit", "credit", "is_opening"], ) self.assertEqual(len(res), 3) ->>>>>>> 5021c7ca2c (test: rounding adjustment validation and posting) def _create_opening_invoice_with_inclusive_tax(self): si = create_sales_invoice(qty=1, rate=90, do_not_submit=True) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index c2fbdcc95f1..7d7c6f49e12 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -7,12 +7,7 @@ import copy import frappe from frappe import _ from frappe.model.meta import get_field_precision -<<<<<<< HEAD -from frappe.utils import cint, flt, formatdate, getdate, now -======= from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now -from frappe.utils.dashboard import cache_source ->>>>>>> 88e68168e3 (refactor: use separate round off for opening entries) import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( From 5d6451fca7de3a9deba0df0a8568dc98f8c1936f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Sep 2024 15:25:32 +0530 Subject: [PATCH 13/46] fix: broken apply on other item pricing rule (cherry picked from commit e5119a749cb0132ac389c416b1b285d3763486b9) --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 1a1ff78a217..72ad0d096bc 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -446,7 +446,10 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): if isinstance(pricing_rule, str): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) update_pricing_rule_uom(pricing_rule, args) - pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or [] + fetch_other_item = True if pricing_rule.apply_rule_on_other else False + pricing_rule.apply_rule_on_other_items = ( + get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or [] + ) if pricing_rule.get("suggestion"): continue From c59a7785039cdedc62c3321706799a35892ee7b3 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Thu, 14 Nov 2024 20:15:20 +0530 Subject: [PATCH 14/46] fix: correctly set 'cannot_add_rows' property on allocations table field (cherry picked from commit 13ca2700f83892dd2074f31907dc08c013fde837) --- erpnext/public/js/utils/unreconcile.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index de20f468ccb..4854bf6b452 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -119,11 +119,18 @@ erpnext.accounts.unreconcile_payment = { return r.message; }; + const allocationsTableField = unreconcile_dialog_fields.find( + (field) => field.fieldname === "allocations" + ); + + if (allocationsTableField) { + allocationsTableField.cannot_add_rows = true; + } + let d = new frappe.ui.Dialog({ title: "UnReconcile Allocations", fields: unreconcile_dialog_fields, size: "large", - cannot_add_rows: true, primary_action_label: "UnReconcile", primary_action(values) { let selected_allocations = values.allocations.filter((x) => x.__checked); From 41c8cfac733104c69c1f30b921c667031c146024 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Mon, 11 Nov 2024 09:18:09 +0000 Subject: [PATCH 15/46] fix: apply posting date sorting to invoices in Payment Reconciliation similar to payments (cherry picked from commit 0bd83d920d42f461ced04a1ca3a35307b9796d16) --- .../payment_reconciliation/payment_reconciliation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 35d268accac..b08afc02c01 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -370,7 +370,11 @@ class PaymentReconciliation(Document): if self.invoice_limit: non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] - + + non_reconciled_invoices = sorted( + non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate()) + ) + self.add_invoice_entries(non_reconciled_invoices) def add_invoice_entries(self, non_reconciled_invoices): From 5bd633b40f86d46ebc3bce68e0dc65c88489d81e Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Mon, 11 Nov 2024 18:06:53 +0530 Subject: [PATCH 16/46] fix: remove trailing whitespace (cherry picked from commit d6703eb88b08e358d66bf43a24225db318e1ddc7) --- .../doctype/payment_reconciliation/payment_reconciliation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b08afc02c01..bbbb3c978ff 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -370,11 +370,11 @@ class PaymentReconciliation(Document): if self.invoice_limit: non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] - + non_reconciled_invoices = sorted( non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate()) ) - + self.add_invoice_entries(non_reconciled_invoices) def add_invoice_entries(self, non_reconciled_invoices): From f4603910e4d4c11ca7cc48d8a44af5a35c98395d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Nov 2024 11:05:01 +0530 Subject: [PATCH 17/46] fix: broken UI on currency exchange (cherry picked from commit e91b65e7bd5bc23788d6329798d3ab8e1b4ab187) --- .../currency_exchange/currency_exchange.js | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.js b/erpnext/setup/doctype/currency_exchange/currency_exchange.js index 82f0e22ee61..d4501e5d0da 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.js +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.js @@ -1,30 +1,32 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -extend_cscript(cur_frm.cscript, { - onload: function () { - if (cur_frm.doc.__islocal) { - cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency")); +frappe.ui.form.on("Currency Exchange", { + onload: function (frm) { + if (frm.doc.__islocal) { + frm.set_value("to_currency", frappe.defaults.get_global_default("currency")); } }, - refresh: function () { - cur_frm.cscript.set_exchange_rate_label(); + refresh: function (frm) { + // Don't trigger on Quick Entry form + if (typeof frm.is_dialog === "undefined") { + frm.trigger("set_exchange_rate_label"); + } }, - from_currency: function () { - cur_frm.cscript.set_exchange_rate_label(); + from_currency: function (frm) { + frm.trigger("set_exchange_rate_label"); }, - to_currency: function () { - cur_frm.cscript.set_exchange_rate_label(); + to_currency: function (frm) { + frm.trigger("set_exchange_rate_label"); }, - - set_exchange_rate_label: function () { - if (cur_frm.doc.from_currency && cur_frm.doc.to_currency) { - var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label); - cur_frm.fields_dict.exchange_rate.set_label( - default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc) + set_exchange_rate_label: function (frm) { + if (frm.doc.from_currency && frm.doc.to_currency) { + var default_label = __(frappe.meta.docfield_map[frm.doctype]["exchange_rate"].label); + frm.fields_dict.exchange_rate.set_label( + default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", frm.doc) ); } }, From 1fe534290dcea0c689c48530b8a765c9111b71f6 Mon Sep 17 00:00:00 2001 From: Vishakh Desai Date: Sat, 28 Sep 2024 12:48:14 +0530 Subject: [PATCH 18/46] fix: Get Entries not showing accounts with no gain or loss in Exchange Rate Revaluation issue (cherry picked from commit 6de6f55b390c1e2180a4698ce25ceba920578b2c) --- .../exchange_rate_revaluation.py | 82 +++++++++++-------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 8607d1ed71f..cd344214736 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -74,6 +74,21 @@ class ExchangeRateRevaluation(Document): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) + def on_submit(self): + self.remove_accounts_without_gain_loss() + + def remove_accounts_without_gain_loss(self): + self.accounts = [account for account in self.accounts if account.gain_loss] + + if not self.accounts: + frappe.throw(_("At least one account with exchange gain or loss is required")) + + frappe.msgprint( + _("Removing rows without exchange gain or loss"), + alert=True, + indicator="yellow", + ) + def on_cancel(self): self.ignore_linked_doctypes = "GL Entry" @@ -248,23 +263,23 @@ class ExchangeRateRevaluation(Document): new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date) new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) - if gain_loss: - accounts.append( - { - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "zero_balance": d.zero_balance, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - "new_balance_in_account_currency": d.balance_in_account_currency, - "gain_loss": gain_loss, - } - ) + + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": d.balance_in_account_currency, + "gain_loss": gain_loss, + } + ) # Handle Accounts with '0' balance in Account/Base Currency for d in [x for x in account_details if x.zero_balance]: @@ -288,23 +303,22 @@ class ExchangeRateRevaluation(Document): current_exchange_rate * d.balance_in_account_currency ) - if gain_loss: - accounts.append( - { - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "zero_balance": d.zero_balance, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - "new_balance_in_account_currency": new_balance_in_account_currency, - "gain_loss": gain_loss, - } - ) + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": new_balance_in_account_currency, + "gain_loss": gain_loss, + } + ) return accounts From 381101f55235e2aa49e2b8229b4250eb3ea2474a Mon Sep 17 00:00:00 2001 From: Vishakh Desai Date: Sat, 28 Sep 2024 12:53:43 +0530 Subject: [PATCH 19/46] fix: linters (cherry picked from commit 9cc22b4cacaf958dad6aadf7a8b9b663326f5a05) --- .../exchange_rate_revaluation/exchange_rate_revaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index cd344214736..c08bd3878d5 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -74,7 +74,7 @@ class ExchangeRateRevaluation(Document): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) - def on_submit(self): + def before_submit(self): self.remove_accounts_without_gain_loss() def remove_accounts_without_gain_loss(self): From b6fe1f5842671421de52e015c968d2740aaad79e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 15:04:48 +0530 Subject: [PATCH 20/46] fix: stock ledger variance report filter options (backport #44137) (#44150) fix: stock ledger variance report filter options (#44137) (cherry picked from commit e8bbf6492fc120d34eb8b36fd2c61ee1a9fdc5dc) Co-authored-by: rohitwaghchaure --- .../stock_ledger_variance.js | 18 +++++++++++++++++- .../stock_ledger_variance.py | 18 ++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js index 07e7b59b514..5dfb6627662 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js @@ -47,7 +47,23 @@ frappe.query_reports["Stock Ledger Variance"] = { fieldname: "difference_in", fieldtype: "Select", label: __("Difference In"), - options: ["", "Qty", "Value", "Valuation"], + options: [ + { + // Check "Stock Ledger Invariant Check" report with A - B column + label: __("Quantity (A - B)"), + value: "Qty", + }, + { + // Check "Stock Ledger Invariant Check" report with G - D column + label: __("Value (G - D)"), + value: "Value", + }, + { + // Check "Stock Ledger Invariant Check" report with I - K column + label: __("Valuation (I - K)"), + value: "Valuation", + }, + ], }, { fieldname: "include_disabled", diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 0b7e551c86f..808afadd05a 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -1,6 +1,8 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json + import frappe from frappe import _ from frappe.utils import cint, flt @@ -270,12 +272,16 @@ def has_difference(row, precision, difference_in, valuation_method): value_diff = flt(row.diff_value_diff, precision) valuation_diff = flt(row.valuation_diff, precision) else: - qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) - value_diff = ( - flt(row.diff_value_diff, precision) - or flt(row.fifo_value_diff, precision) - or flt(row.fifo_difference_diff, precision) - ) + qty_diff = flt(row.difference_in_qty, precision) + value_diff = flt(row.diff_value_diff, precision) + + if row.stock_queue and json.loads(row.stock_queue): + value_diff = value_diff or ( + flt(row.fifo_value_diff, precision) or flt(row.fifo_difference_diff, precision) + ) + + qty_diff = qty_diff or flt(row.fifo_qty_diff, precision) + valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision) if difference_in == "Qty" and qty_diff: From 725d107288675867dd14571b84945f00d4def45e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:55:32 +0530 Subject: [PATCH 21/46] fix: validation for serial no (backport #44133) (#44151) * fix: validation for serial no (#44133) (cherry picked from commit 93c8b4c39af90f9b60da7476855281a22bca0ffc) # Conflicts: # erpnext/stock/doctype/stock_entry/test_stock_entry.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle.py | 4 -- .../doctype/stock_entry/test_stock_entry.py | 55 ------------------- 2 files changed, 59 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 08aa978aa99..68c47b0d577 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -749,10 +749,6 @@ class SerialandBatchBundle(Document): ) def validate_incorrect_serial_nos(self, serial_nos): - if self.voucher_type == "Stock Entry" and self.voucher_no: - if frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose") == "Repack": - return - incorrect_serial_nos = frappe.get_all( "Serial No", filters={"name": ("in", serial_nos), "item_code": ("!=", self.item_code)}, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index a9529cc2ede..469b865dd59 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -970,61 +970,6 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, ste.submit) - def test_same_serial_nos_in_repack_or_manufacture_entries(self): - s1 = make_serialized_item(target_warehouse="_Test Warehouse - _TC") - serial_nos = get_serial_nos_from_bundle(s1.get("items")[0].serial_and_batch_bundle) - - s2 = make_stock_entry( - item_code="_Test Serialized Item With Series", - source="_Test Warehouse - _TC", - qty=2, - basic_rate=100, - purpose="Repack", - serial_no=serial_nos, - do_not_save=True, - ) - - frappe.flags.use_serial_and_batch_fields = True - - cls_obj = SerialBatchCreation( - { - "type_of_transaction": "Inward", - "serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle, - "item_code": "_Test Serialized Item", - "warehouse": "_Test Warehouse - _TC", - } - ) - - cls_obj.duplicate_package() - bundle_id = cls_obj.serial_and_batch_bundle - doc = frappe.get_doc("Serial and Batch Bundle", bundle_id) - doc.db_set( - { - "item_code": "_Test Serialized Item", - "warehouse": "_Test Warehouse - _TC", - } - ) - - doc.load_from_db() - - s2.append( - "items", - { - "item_code": "_Test Serialized Item", - "t_warehouse": "_Test Warehouse - _TC", - "qty": 2, - "basic_rate": 120, - "expense_account": "Stock Adjustment - _TC", - "conversion_factor": 1.0, - "cost_center": "_Test Cost Center - _TC", - "serial_and_batch_bundle": bundle_id, - }, - ) - - s2.submit() - s2.cancel() - frappe.flags.use_serial_and_batch_fields = False - def test_quality_check(self): item_code = "_Test Item For QC" if not frappe.db.exists("Item", item_code): From d61f696f85084bfec858a32c4d819f021e0a6bae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:28:41 +0530 Subject: [PATCH 22/46] feat: inventory dimension for rejected materials (backport #44156) (#44165) feat: inventory dimension for rejected materials (#44156) (cherry picked from commit 9bf16df41ed15f8ed57558262db5277c0d9f3ae4) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 9 ++++++ .../inventory_dimension.py | 23 +++++++++++-- .../test_inventory_dimension.py | 32 +++++++++++++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 046a0c7da30..4eb67b6f42e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -839,6 +839,15 @@ class StockController(AccountsController): if not dimension: continue + if ( + self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and row.get("rejected_warehouse") + and sl_dict.get("warehouse") == row.get("rejected_warehouse") + ): + fieldname = f"rejected_{dimension.source_fieldname}" + sl_dict[dimension.target_fieldname] = row.get(fieldname) + continue + if self.doctype in [ "Purchase Invoice", "Purchase Receipt", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 3bafa12983f..4f8a166932d 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -107,6 +107,7 @@ class InventoryDimension(Document): self.source_fieldname, f"to_{self.source_fieldname}", f"from_{self.source_fieldname}", + f"rejected_{self.source_fieldname}", ], ) } @@ -171,12 +172,12 @@ class InventoryDimension(Document): if label_start_with: label = f"{label_start_with} {self.dimension_name}" - return [ + dimension_fields = [ dict( fieldname="inventory_dimension", fieldtype="Section Break", insert_after=self.get_insert_after_fieldname(doctype), - label="Inventory Dimension", + label=_("Inventory Dimension"), collapsible=1, ), dict( @@ -184,13 +185,29 @@ class InventoryDimension(Document): fieldtype="Link", insert_after="inventory_dimension", options=self.reference_document, - label=label, + label=_(label), search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), ] + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + dimension_fields.append( + dict( + fieldname="rejected_" + self.source_fieldname, + fieldtype="Link", + insert_after=self.source_fieldname, + options=self.reference_document, + label=_("Rejected " + self.dimension_name), + search_index=1, + reqd=self.reqd, + mandatory_depends_on=self.mandatory_depends_on, + ) + ) + + return dimension_fields + def add_custom_fields(self): custom_fields = {} diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 918399a7f66..f8128ce0033 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -269,21 +269,47 @@ class TestInventoryDimension(FrappeTestCase): item_code = "Test Inventory Dimension Item" create_item(item_code) warehouse = create_warehouse("Store Warehouse") + rj_warehouse = create_warehouse("RJ Warehouse") + + if not frappe.db.exists("Store", "Rejected Store"): + frappe.get_doc({"doctype": "Store", "store_name": "Rejected Store"}).insert( + ignore_permissions=True + ) # Purchase Receipt -> Inward in Store 1 pr_doc = make_purchase_receipt( - item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True + item_code=item_code, + warehouse=warehouse, + qty=10, + rejected_qty=5, + rate=100, + rejected_warehouse=rj_warehouse, + do_not_submit=True, ) pr_doc.items[0].store = "Store 1" + pr_doc.items[0].rejected_store = "Rejected Store" pr_doc.save() pr_doc.submit() - entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"]) + entries = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": pr_doc.name, "warehouse": warehouse}, + fields=["store"], + order_by="creation", + ) - self.assertEqual(entries[0].warehouse, warehouse) self.assertEqual(entries[0].store, "Store 1") + entries = frappe.get_all( + "Stock Ledger Entry", + filters={"voucher_no": pr_doc.name, "warehouse": rj_warehouse}, + fields=["store"], + order_by="creation", + ) + + self.assertEqual(entries[0].store, "Rejected Store") + # Stock Entry -> Transfer from Store 1 to Store 2 se_doc = make_stock_entry( item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True From 2a54cd50041f40bcb5e748232521cbab4eee8387 Mon Sep 17 00:00:00 2001 From: UmakanthKaspa Date: Fri, 15 Nov 2024 05:43:25 +0000 Subject: [PATCH 23/46] refactor: set 'cannot_add_rows' directly in the allocations table field (optimized approach) (cherry picked from commit 5dd8eafdfc7e077f40c330819bcbcbb132bd3f40) --- erpnext/public/js/utils/unreconcile.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 4854bf6b452..7dba4705e40 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -100,6 +100,7 @@ erpnext.accounts.unreconcile_payment = { fieldtype: "Table", read_only: 1, fields: child_table_fields, + cannot_add_rows: true, }, ]; @@ -119,14 +120,6 @@ erpnext.accounts.unreconcile_payment = { return r.message; }; - const allocationsTableField = unreconcile_dialog_fields.find( - (field) => field.fieldname === "allocations" - ); - - if (allocationsTableField) { - allocationsTableField.cannot_add_rows = true; - } - let d = new frappe.ui.Dialog({ title: "UnReconcile Allocations", fields: unreconcile_dialog_fields, From 5848de76ea3049cf3b25c88cc0a510f7712945c5 Mon Sep 17 00:00:00 2001 From: vishakhdesai Date: Thu, 14 Nov 2024 17:31:34 +0530 Subject: [PATCH 24/46] fix: set conversion factor before applying price list (cherry picked from commit 9749fe23cc82181f1fb58aa481b0c3c7d633699e) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0efec214c0d..ca1b1c8c590 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1237,8 +1237,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(!r.exc) { - me.apply_price_list(item, true) frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor); + me.apply_price_list(item, true); } } }); From 08f6ceeb5001d5e9159c7a9c0134467121298da0 Mon Sep 17 00:00:00 2001 From: vishakhdesai Date: Wed, 13 Nov 2024 17:27:49 +0530 Subject: [PATCH 25/46] fix: set default party type in Payment Entry (cherry picked from commit 19222690d31e486ccd634d7530f2c85eda7cf4c0) --- .../doctype/payment_entry/payment_entry.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 7ababfec81a..a377aa04db2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -26,6 +26,10 @@ frappe.ui.form.on("Payment Entry", { } erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + + if (frm.is_new()) { + set_default_party_type(frm); + } }, setup: function (frm) { @@ -403,6 +407,8 @@ frappe.ui.form.on("Payment Entry", { }, payment_type: function (frm) { + set_default_party_type(frm); + if (frm.doc.payment_type == "Internal Transfer") { $.each( [ @@ -1776,3 +1782,16 @@ frappe.ui.form.on("Payment Entry Deduction", { frm.events.set_unallocated_amount(frm); }, }); + +function set_default_party_type(frm) { + if (frm.doc.party) return; + + let party_type; + if (frm.doc.payment_type == "Receive") { + party_type = "Customer"; + } else if (frm.doc.payment_type == "Pay") { + party_type = "Supplier"; + } + + if (party_type) frm.set_value("party_type", party_type); +} From c98a0ccd1dc5007941544ca49e0c2ad68d050323 Mon Sep 17 00:00:00 2001 From: Ninad1306 Date: Fri, 8 Nov 2024 12:51:05 +0530 Subject: [PATCH 26/46] fix: added disable_rounded_total field (cherry picked from commit f8524d526b5922e8223e21409f2997f2e7cd2f5b) # Conflicts: # erpnext/selling/doctype/quotation/quotation.json --- .../selling/doctype/quotation/quotation.json | 47 +++++++++++++++++++ .../selling/doctype/quotation/quotation.py | 1 + 2 files changed, 48 insertions(+) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 982e7326775..8d6b96c8e67 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -65,6 +65,7 @@ "grand_total", "rounding_adjustment", "rounded_total", + "disable_rounded_total", "in_words", "section_break_44", "apply_discount_on", @@ -661,6 +662,7 @@ "width": "200px" }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "base_rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment (Company Currency)", @@ -709,6 +711,7 @@ "width": "200px" }, { + "depends_on": "eval:!doc.disable_rounded_total", "fieldname": "rounding_adjustment", "fieldtype": "Currency", "label": "Rounding Adjustment", @@ -1067,13 +1070,57 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" +<<<<<<< HEAD +======= + }, + { + "fieldname": "utm_campaign", + "fieldtype": "Link", + "label": "Campaign", + "oldfieldname": "campaign", + "oldfieldtype": "Link", + "options": "UTM Campaign", + "print_hide": 1 + }, + { + "fieldname": "utm_source", + "fieldtype": "Link", + "label": "Source", + "oldfieldname": "source", + "oldfieldtype": "Select", + "options": "UTM Source", + "print_hide": 1 + }, + { + "fieldname": "utm_medium", + "fieldtype": "Link", + "label": "Medium", + "options": "UTM Medium", + "print_hide": 1 + }, + { + "fieldname": "utm_content", + "fieldtype": "Data", + "label": "Content", + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "disable_rounded_total", + "fieldtype": "Check", + "label": "Disable Rounded Total" +>>>>>>> f8524d526b (fix: added disable_rounded_total field) } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-03-20 16:04:21.567847", +======= + "modified": "2024-11-07 18:37:11.715189", +>>>>>>> f8524d526b (fix: added disable_rounded_total field) "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index a5994756c46..8e560b8d0ab 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -61,6 +61,7 @@ class Quotation(SellingController): customer_address: DF.Link | None customer_group: DF.Link | None customer_name: DF.Data | None + disable_rounded_total: DF.Check discount_amount: DF.Currency enq_det: DF.Text | None grand_total: DF.Currency From b6524946bc9a708b7d1dcc8c1c29b3d0feec7b4f Mon Sep 17 00:00:00 2001 From: Ninad1306 Date: Tue, 12 Nov 2024 15:25:14 +0530 Subject: [PATCH 27/46] test: test to validate rounded total (cherry picked from commit 5a6261d3b4b3084dc1f45bef6584c55043084336) --- .../selling/doctype/quotation/test_quotation.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 9a31e335a05..05f43f26559 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -715,6 +715,20 @@ class TestQuotation(FrappeTestCase): item_doc.taxes = [] item_doc.save() + def test_grand_total_and_rounded_total_values(self): + quotation = make_quotation(qty=6, rate=12.3, do_not_submit=1) + + self.assertEqual(quotation.grand_total, 73.8) + self.assertEqual(quotation.rounding_adjustment, 0.2) + self.assertEqual(quotation.rounded_total, 74) + + quotation.disable_rounded_total = 1 + quotation.save() + + self.assertEqual(quotation.grand_total, 73.8) + self.assertEqual(quotation.rounding_adjustment, 0) + self.assertEqual(quotation.rounded_total, 0) + test_records = frappe.get_test_records("Quotation") From c0d3f8cbbe347f7326b4acead396e28daec01fc0 Mon Sep 17 00:00:00 2001 From: sudarsan2001 Date: Thu, 14 Nov 2024 01:11:12 +0530 Subject: [PATCH 28/46] fix: set debit in transaction currency in GL Entry (cherry picked from commit 29a6eb21a3cf33cd5d7a76e476646bf9c5e63c24) --- .../doctype/payment_entry/payment_entry.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 8832b87eec7..3efcb155781 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1248,13 +1248,22 @@ class PaymentEntry(AccountsController): base_unallocated_amount = self.unallocated_amount * exchange_rate gle = party_gl_dict.copy() - gle.update( - { - dr_or_cr + "_in_account_currency": self.unallocated_amount, - dr_or_cr: base_unallocated_amount, - } - ) + gle.update( + self.get_gl_dict( + { + "account": self.party_account, + "party_type": self.party_type, + "party": self.party, + "against": against_account, + "account_currency": self.party_account_currency, + "cost_center": self.cost_center, + dr_or_cr + "_in_account_currency": self.unallocated_amount, + dr_or_cr: base_unallocated_amount, + }, + item=self, + ) + ) if self.book_advance_payments_in_separate_party_account: gle.update( { From c30a17cd7a2fd009127d802596b66e81394c712d Mon Sep 17 00:00:00 2001 From: sudarsan2001 Date: Thu, 14 Nov 2024 01:28:16 +0530 Subject: [PATCH 29/46] test: add unit test to validate gl values (cherry picked from commit e8b8a589be75a840d75a2e46cf55364c040158a1) --- .../payment_entry/test_payment_entry.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 771c91a462c..2dcd5d6076f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -956,6 +956,51 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(flt(expected_party_balance), party_balance) self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2)) + def test_gl_of_multi_currency_payment_transaction(self): + from erpnext.setup.doctype.currency_exchange.test_currency_exchange import save_new_records + + save_new_records(self.globalTestRecords["Currency Exchange"]) + paid_from = create_account( + parent_account="Current Liabilities - _TC", + account_name="Cash USD", + company="_Test Company", + account_type="Cash", + account_currency="USD", + ) + payment_entry = create_payment_entry( + party="_Test Supplier USD", + paid_from=paid_from, + paid_to="_Test Payable USD - _TC", + paid_amount=100, + save=True, + ) + payment_entry.source_exchange_rate = 84.4 + payment_entry.target_exchange_rate = 84.4 + payment_entry.save() + payment_entry = payment_entry.submit() + gle = qb.DocType("GL Entry") + gl_entries = ( + qb.from_(gle) + .select( + gle.account, + gle.debit, + gle.credit, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + gle.debit_in_transaction_currency, + gle.credit_in_transaction_currency, + ) + .orderby(gle.account) + .where(gle.voucher_no == payment_entry.name) + .run() + ) + expected_gl_entries = ( + ("_Test Payable USD - _TC", 8440.0, 0, 100.0, 0.0, 8440.0, 0.0), + (paid_from, 0, 8440.0, 0, 100.0, 0, 8440.0), + ) + + self.assertEqual(gl_entries, expected_gl_entries) + def test_multi_currency_payment_entry_with_taxes(self): payment_entry = create_payment_entry( party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True From 7cc31df58774d77ef185c0f327ae0623a156b675 Mon Sep 17 00:00:00 2001 From: sudarsan2001 Date: Thu, 14 Nov 2024 10:59:51 +0530 Subject: [PATCH 30/46] chore: change account name (cherry picked from commit 4a1cd5a8d6bf920b682fa731f1fc087e09ea94e5) --- .../accounts/doctype/payment_entry/test_payment_entry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 2dcd5d6076f..c61598b54a5 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -962,7 +962,7 @@ class TestPaymentEntry(FrappeTestCase): save_new_records(self.globalTestRecords["Currency Exchange"]) paid_from = create_account( parent_account="Current Liabilities - _TC", - account_name="Cash USD", + account_name="_Test Cash USD", company="_Test Company", account_type="Cash", account_currency="USD", @@ -995,10 +995,9 @@ class TestPaymentEntry(FrappeTestCase): .run() ) expected_gl_entries = ( - ("_Test Payable USD - _TC", 8440.0, 0, 100.0, 0.0, 8440.0, 0.0), - (paid_from, 0, 8440.0, 0, 100.0, 0, 8440.0), + (paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 8440.0), + ("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 8440.0, 0.0), ) - self.assertEqual(gl_entries, expected_gl_entries) def test_multi_currency_payment_entry_with_taxes(self): From d7deed6c450708e74a58469d852f909d9893a16a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 18 Nov 2024 16:25:44 +0530 Subject: [PATCH 31/46] refactor: assume any of the foreign currency as transaction currency On a foreign currency payment entry, assume any one of the foreign currency as the transaction currency (cherry picked from commit 6681882bd8003cac1d8d4eda76141ec5a6b6b246) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3efcb155781..b9fad5c9010 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1146,6 +1146,12 @@ class PaymentEntry(AccountsController): if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"): self.setup_party_account_field() + company_currency = erpnext.get_company_currency(self.company) + if self.paid_from_account_currency != company_currency: + self.currency = self.paid_from_account_currency + elif self.paid_to_account_currency != company_currency: + self.currency = self.paid_to_account_currency + gl_entries = [] self.add_party_gl_entries(gl_entries) self.add_bank_gl_entries(gl_entries) From b130e2065b5998ef42b754442fda9c5bf4cf60d9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:08:21 +0100 Subject: [PATCH 32/46] feat: new DocTypes "Code List" and "Common Code" (backport #43425) (#44173) Co-authored-by: David Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/edi/__init__.py | 0 erpnext/edi/doctype/__init__.py | 0 erpnext/edi/doctype/code_list/__init__.py | 0 erpnext/edi/doctype/code_list/code_list.js | 51 ++++ erpnext/edi/doctype/code_list/code_list.json | 112 +++++++++ erpnext/edi/doctype/code_list/code_list.py | 125 ++++++++++ .../edi/doctype/code_list/code_list_import.js | 218 ++++++++++++++++++ .../edi/doctype/code_list/code_list_import.py | 140 +++++++++++ .../edi/doctype/code_list/code_list_list.js | 8 + .../edi/doctype/code_list/test_code_list.py | 9 + erpnext/edi/doctype/common_code/__init__.py | 0 .../edi/doctype/common_code/common_code.js | 8 + .../edi/doctype/common_code/common_code.json | 103 +++++++++ .../edi/doctype/common_code/common_code.py | 114 +++++++++ .../doctype/common_code/common_code_list.js | 8 + .../doctype/common_code/test_common_code.py | 9 + erpnext/hooks.py | 8 + erpnext/modules.txt | 1 + 18 files changed, 914 insertions(+) create mode 100644 erpnext/edi/__init__.py create mode 100644 erpnext/edi/doctype/__init__.py create mode 100644 erpnext/edi/doctype/code_list/__init__.py create mode 100644 erpnext/edi/doctype/code_list/code_list.js create mode 100644 erpnext/edi/doctype/code_list/code_list.json create mode 100644 erpnext/edi/doctype/code_list/code_list.py create mode 100644 erpnext/edi/doctype/code_list/code_list_import.js create mode 100644 erpnext/edi/doctype/code_list/code_list_import.py create mode 100644 erpnext/edi/doctype/code_list/code_list_list.js create mode 100644 erpnext/edi/doctype/code_list/test_code_list.py create mode 100644 erpnext/edi/doctype/common_code/__init__.py create mode 100644 erpnext/edi/doctype/common_code/common_code.js create mode 100644 erpnext/edi/doctype/common_code/common_code.json create mode 100644 erpnext/edi/doctype/common_code/common_code.py create mode 100644 erpnext/edi/doctype/common_code/common_code_list.js create mode 100644 erpnext/edi/doctype/common_code/test_common_code.py diff --git a/erpnext/edi/__init__.py b/erpnext/edi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/edi/doctype/__init__.py b/erpnext/edi/doctype/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/edi/doctype/code_list/__init__.py b/erpnext/edi/doctype/code_list/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/edi/doctype/code_list/code_list.js b/erpnext/edi/doctype/code_list/code_list.js new file mode 100644 index 00000000000..f8b9a2003fd --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.js @@ -0,0 +1,51 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Code List", { + refresh: (frm) => { + if (!frm.doc.__islocal) { + frm.add_custom_button(__("Import Genericode File"), function () { + erpnext.edi.import_genericode(frm); + }); + } + }, + setup: (frm) => { + frm.savetrash = () => { + frm.validate_form_action("Delete"); + frappe.confirm( + __( + "Are you sure you want to delete {0}?

This action will also delete all associated Common Code documents.

", + [frm.docname.bold()] + ), + function () { + return frappe.call({ + method: "frappe.client.delete", + args: { + doctype: frm.doctype, + name: frm.docname, + }, + freeze: true, + freeze_message: __("Deleting {0} and all associated Common Code documents...", [ + frm.docname, + ]), + callback: function (r) { + if (!r.exc) { + frappe.utils.play_sound("delete"); + frappe.model.clear_doc(frm.doctype, frm.docname); + window.history.back(); + } + }, + }); + } + ); + }; + + frm.set_query("default_common_code", function (doc) { + return { + filters: { + code_list: doc.name, + }, + }; + }); + }, +}); diff --git a/erpnext/edi/doctype/code_list/code_list.json b/erpnext/edi/doctype/code_list/code_list.json new file mode 100644 index 00000000000..ffcc2f2b605 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.json @@ -0,0 +1,112 @@ +{ + "actions": [], + "allow_copy": 1, + "allow_rename": 1, + "autoname": "prompt", + "creation": "2024-09-29 06:55:03.920375", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "title", + "canonical_uri", + "url", + "default_common_code", + "column_break_nkls", + "version", + "publisher", + "publisher_id", + "section_break_npxp", + "description" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + }, + { + "fieldname": "publisher", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Publisher" + }, + { + "columns": 1, + "fieldname": "version", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Version" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "canonical_uri", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Canonical URI" + }, + { + "fieldname": "column_break_nkls", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_npxp", + "fieldtype": "Section Break" + }, + { + "fieldname": "publisher_id", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Publisher ID" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL", + "options": "URL" + }, + { + "description": "This value shall be used when no matching Common Code for a record is found.", + "fieldname": "default_common_code", + "fieldtype": "Link", + "label": "Default Common Code", + "options": "Common Code" + } + ], + "index_web_pages_for_search": 1, + "links": [ + { + "link_doctype": "Common Code", + "link_fieldname": "code_list" + } + ], + "modified": "2024-11-16 17:01:40.260293", + "modified_by": "Administrator", + "module": "EDI", + "name": "Code List", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "description", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py new file mode 100644 index 00000000000..8957c6565b9 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -0,0 +1,125 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from typing import TYPE_CHECKING + +import frappe +from frappe.model.document import Document + +if TYPE_CHECKING: + from lxml.etree import Element + + +class CodeList(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + canonical_uri: DF.Data | None + default_common_code: DF.Link | None + description: DF.SmallText | None + publisher: DF.Data | None + publisher_id: DF.Data | None + title: DF.Data | None + url: DF.Data | None + version: DF.Data | None + # end: auto-generated types + + def on_trash(self): + if not frappe.flags.in_bulk_delete: + self.__delete_linked_docs() + + def __delete_linked_docs(self): + self.db_set("default_common_code", None) + + linked_docs = frappe.get_all( + "Common Code", + filters={"code_list": self.name}, + fields=["name"], + ) + + for doc in linked_docs: + frappe.delete_doc("Common Code", doc.name) + + def get_codes_for(self, doctype: str, name: str) -> tuple[str]: + """Get the applicable codes for a doctype and name""" + return get_codes_for(self.name, doctype, name) + + def get_docnames_for(self, doctype: str, code: str) -> tuple[str]: + """Get the mapped docnames for a doctype and code""" + return get_docnames_for(self.name, doctype, code) + + def get_default_code(self) -> str | None: + """Get the default common code for this code list""" + return ( + frappe.db.get_value("Common Code", self.default_common_code, "common_code") + if self.default_common_code + else None + ) + + def from_genericode(self, root: "Element"): + """Extract Code List details from genericode XML""" + self.title = root.find(".//Identification/ShortName").text + self.version = root.find(".//Identification/Version").text + self.canonical_uri = root.find(".//CanonicalUri").text + # optionals + self.description = getattr(root.find(".//Identification/LongName"), "text", None) + self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + if not self.publisher: + self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None) + self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None) + self.url = getattr(root.find(".//Identification/LocationUri"), "text", None) + + +def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]: + """Return the common code for a given record""" + CommonCode = frappe.qb.DocType("Common Code") + DynamicLink = frappe.qb.DocType("Dynamic Link") + + codes = ( + frappe.qb.from_(CommonCode) + .join(DynamicLink) + .on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code")) + .select(CommonCode.common_code) + .where( + (DynamicLink.link_doctype == doctype) + & (DynamicLink.link_name == name) + & (CommonCode.code_list == code_list) + ) + .distinct() + .orderby(CommonCode.common_code) + ).run() + + return tuple(c[0] for c in codes) if codes else () + + +def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]: + """Return the record name for a given common code""" + CommonCode = frappe.qb.DocType("Common Code") + DynamicLink = frappe.qb.DocType("Dynamic Link") + + docnames = ( + frappe.qb.from_(CommonCode) + .join(DynamicLink) + .on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code")) + .select(DynamicLink.link_name) + .where( + (DynamicLink.link_doctype == doctype) + & (CommonCode.common_code == code) + & (CommonCode.code_list == code_list) + ) + .distinct() + .orderby(DynamicLink.idx) + ).run() + + return tuple(d[0] for d in docnames) if docnames else () + + +def get_default_code(code_list: str) -> str | None: + """Return the default common code for a given code list""" + code_id = frappe.db.get_value("Code List", code_list, "default_common_code") + return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None diff --git a/erpnext/edi/doctype/code_list/code_list_import.js b/erpnext/edi/doctype/code_list/code_list_import.js new file mode 100644 index 00000000000..4a33f3e2fe6 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.js @@ -0,0 +1,218 @@ +frappe.provide("erpnext.edi"); + +erpnext.edi.import_genericode = function (listview_or_form) { + let doctype = "Code List"; + let docname = undefined; + if (listview_or_form.doc !== undefined) { + docname = listview_or_form.doc.name; + } + new frappe.ui.FileUploader({ + method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode", + doctype: doctype, + docname: docname, + allow_toggle_private: false, + allow_take_photo: false, + on_success: function (_file_doc, r) { + listview_or_form.refresh(); + show_column_selection_dialog(r.message); + }, + }); +}; + +function show_column_selection_dialog(context) { + let title_description = __("If there is no title column, use the code column for the title."); + let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]); + let fields = [ + { + fieldtype: "HTML", + fieldname: "code_list_info", + options: `
${__( + "You are importing data for the code list:" + )} ${frappe.utils.get_form_link( + "Code List", + context.code_list, + true, + context.code_list_title + )}
`, + }, + { + fieldtype: "Section Break", + }, + { + fieldname: "import_column", + label: __("Import"), + fieldtype: "Column Break", + }, + { + fieldname: "title_column", + label: __("as Title"), + fieldtype: "Select", + reqd: 1, + options: context.columns, + default: default_title, + description: default_title ? null : title_description, + }, + { + fieldname: "code_column", + label: __("as Code"), + fieldtype: "Select", + options: context.columns, + reqd: 1, + default: get_default(context.columns, ["code", "Code", "value"]), + }, + { + fieldname: "filters_column", + label: __("Filter"), + fieldtype: "Column Break", + }, + ]; + + if (context.columns.length > 2) { + fields.splice(5, 0, { + fieldname: "description_column", + label: __("as Description"), + fieldtype: "Select", + options: [null].concat(context.columns), + default: get_default(context.columns, [ + "description", + "Description", + "remark", + __("description"), + __("Description"), + ]), + }); + } + + // Add filterable columns + for (let column in context.filterable_columns) { + fields.push({ + fieldname: `filter_${column}`, + label: __("by {}", [column]), + fieldtype: "Select", + options: [null].concat(context.filterable_columns[column]), + }); + } + + fields.push( + { + fieldname: "preview_section", + label: __("Preview"), + fieldtype: "Section Break", + }, + { + fieldname: "preview_html", + fieldtype: "HTML", + } + ); + + let d = new frappe.ui.Dialog({ + title: __("Select Columns and Filters"), + fields: fields, + primary_action_label: __("Import"), + size: "large", // This will make the modal wider + primary_action(values) { + let filters = {}; + for (let field in values) { + if (field.startsWith("filter_") && values[field]) { + filters[field.replace("filter_", "")] = values[field]; + } + } + frappe + .xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", { + code_list_name: context.code_list, + file_name: context.file, + code_column: values.code_column, + title_column: values.title_column, + description_column: values.description_column, + filters: filters, + }) + .then((count) => { + frappe.msgprint(__("Import completed. {0} common codes created.", [count])); + }); + d.hide(); + }, + }); + + d.fields_dict.code_column.df.onchange = () => update_preview(d, context); + d.fields_dict.title_column.df.onchange = (e) => { + let field = d.fields_dict.title_column; + if (!e.target.value) { + field.df.description = title_description; + field.refresh(); + } else { + field.df.description = null; + field.refresh(); + } + update_preview(d, context); + }; + + // Add onchange events for filterable columns + for (let column in context.filterable_columns) { + d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context); + } + + d.show(); + update_preview(d, context); +} + +/** + * Return the first key from the keys array that is found in the columns array. + */ +function get_default(columns, keys) { + return keys.find((key) => columns.includes(key)); +} + +function update_preview(dialog, context) { + let code_column = dialog.get_value("code_column"); + let title_column = dialog.get_value("title_column"); + let description_column = dialog.get_value("description_column"); + + let html = ''; + if (title_column) html += ``; + if (code_column) html += ``; + if (description_column) html += ``; + + // Add headers for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + html += ``; + } + } + + html += ""; + + for (let i = 0; i < 3; i++) { + html += ""; + if (title_column) { + let title = context.example_values[title_column][i] || ""; + html += ``; + } + if (code_column) { + let code = context.example_values[code_column][i] || ""; + html += ``; + } + if (description_column) { + let description = context.example_values[description_column][i] || ""; + html += ``; + } + + // Add values for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + let value = context.example_values[column][i] || ""; + html += ``; + } + } + + html += ""; + } + + html += "
${__("Title")}${__("Code")}${__("Description")}${__(column)}
${truncate(title)}${truncate(code)}${truncate(description)}${truncate(value)}
"; + + dialog.fields_dict.preview_html.$wrapper.html(html); +} + +function truncate(value, maxLength = 40) { + if (typeof value !== "string") return ""; + return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value; +} diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py new file mode 100644 index 00000000000..50df3be471e --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -0,0 +1,140 @@ +import json + +import frappe +import requests +from frappe import _ +from lxml import etree + +URL_PREFIXES = ("http://", "https://") + + +@frappe.whitelist() +def import_genericode(): + doctype = "Code List" + docname = frappe.form_dict.docname + content = frappe.local.uploaded_file + + # recover the content, if it's a link + if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES): + try: + # If it's a URL, fetch the content and make it a local file (for durable audit) + response = requests.get(frappe.local.uploaded_file_url) + response.raise_for_status() + frappe.local.uploaded_file = content = response.content + frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1] + frappe.local.uploaded_file_url = None + except Exception as e: + frappe.throw(f"
{e!s}
", title=_("Fetching Error")) + + if file_url := frappe.local.uploaded_file_url: + file_path = frappe.utils.file_manager.get_file_path(file_url) + with open(file_path.encode(), mode="rb") as f: + content = f.read() + + # Parse the xml content + parser = etree.XMLParser(remove_blank_text=True) + try: + root = etree.fromstring(content, parser=parser) + except Exception as e: + frappe.throw(f"
{e!s}
", title=_("Parsing Error")) + + # Extract the name (CanonicalVersionUri) from the parsed XML + name = root.find(".//CanonicalVersionUri").text + docname = docname or name + + if frappe.db.exists(doctype, docname): + code_list = frappe.get_doc(doctype, docname) + if code_list.name != name: + frappe.throw(_("The uploaded file does not match the selected Code List.")) + else: + # Create a new Code List document with the extracted name + code_list = frappe.new_doc(doctype) + code_list.name = name + + code_list.from_genericode(root) + code_list.save() + + # Attach the file and provide a recoverable identifier + file_doc = frappe.get_doc( + { + "doctype": "File", + "attached_to_doctype": "Code List", + "attached_to_name": code_list.name, + "folder": "Home/Attachments", + "file_name": frappe.local.uploaded_filename, + "file_url": frappe.local.uploaded_file_url, + "is_private": 1, + "content": content, + } + ).save() + + # Get available columns and example values + columns, example_values, filterable_columns = get_genericode_columns_and_examples(root) + + return { + "code_list": code_list.name, + "code_list_title": code_list.title, + "file": file_doc.name, + "columns": columns, + "example_values": example_values, + "filterable_columns": filterable_columns, + } + + +@frappe.whitelist() +def process_genericode_import( + code_list_name: str, + file_name: str, + code_column: str, + title_column: str | None = None, + description_column: str | None = None, + filters: str | None = None, +): + from erpnext.edi.doctype.common_code.common_code import import_genericode + + column_map = {"code": code_column, "title": title_column, "description": description_column} + + return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None) + + +def get_genericode_columns_and_examples(root): + columns = [] + example_values = {} + filterable_columns = {} + + # Get column names + for column in root.findall(".//Column"): + column_id = column.get("Id") + columns.append(column_id) + example_values[column_id] = [] + filterable_columns[column_id] = set() + + # Get all values and count unique occurrences + for row in root.findall(".//SimpleCodeList/Row"): + for value in row.findall("Value"): + column_id = value.get("ColumnRef") + if column_id not in columns: + # Handle undeclared column + columns.append(column_id) + example_values[column_id] = [] + filterable_columns[column_id] = set() + + simple_value = value.find("./SimpleValue") + if simple_value is None: + continue + + filterable_columns[column_id].add(simple_value.text) + + # Get example values (up to 3) and filter columns with cardinality <= 5 + for row in root.findall(".//SimpleCodeList/Row")[:3]: + for value in row.findall("Value"): + column_id = value.get("ColumnRef") + simple_value = value.find("./SimpleValue") + if simple_value is None: + continue + + example_values[column_id].append(simple_value.text) + + filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5} + + return columns, example_values, filterable_columns diff --git a/erpnext/edi/doctype/code_list/code_list_list.js b/erpnext/edi/doctype/code_list/code_list_list.js new file mode 100644 index 00000000000..08125de2903 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings["Code List"] = { + onload: function (listview) { + listview.page.add_inner_button(__("Import Genericode File"), function () { + erpnext.edi.import_genericode(listview); + }); + }, + hide_name_column: true, +}; diff --git a/erpnext/edi/doctype/code_list/test_code_list.py b/erpnext/edi/doctype/code_list/test_code_list.py new file mode 100644 index 00000000000..d37b1ee8f5a --- /dev/null +++ b/erpnext/edi/doctype/code_list/test_code_list.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCodeList(FrappeTestCase): + pass diff --git a/erpnext/edi/doctype/common_code/__init__.py b/erpnext/edi/doctype/common_code/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/edi/doctype/common_code/common_code.js b/erpnext/edi/doctype/common_code/common_code.js new file mode 100644 index 00000000000..646d5c85b74 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Common Code", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/edi/doctype/common_code/common_code.json b/erpnext/edi/doctype/common_code/common_code.json new file mode 100644 index 00000000000..b2cb43fa575 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code.json @@ -0,0 +1,103 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2024-09-29 07:01:18.133067", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "code_list", + "title", + "common_code", + "description", + "column_break_wxsw", + "additional_data", + "section_break_rhgh", + "applies_to" + ], + "fields": [ + { + "fieldname": "code_list", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Code List", + "options": "Code List", + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Title", + "length": 300, + "reqd": 1 + }, + { + "fieldname": "column_break_wxsw", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_rhgh", + "fieldtype": "Section Break" + }, + { + "fieldname": "applies_to", + "fieldtype": "Table", + "label": "Applies To", + "options": "Dynamic Link" + }, + { + "fieldname": "common_code", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Common Code", + "length": 300, + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "additional_data", + "fieldtype": "Code", + "label": "Additional Data", + "max_height": "190px", + "read_only": 1 + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "max_height": "60px" + } + ], + "links": [], + "modified": "2024-11-06 07:46:17.175687", + "modified_by": "Administrator", + "module": "EDI", + "name": "Common Code", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "common_code,description", + "show_title_field_in_link": 1, + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/edi/doctype/common_code/common_code.py b/erpnext/edi/doctype/common_code/common_code.py new file mode 100644 index 00000000000..d558b2d282f --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code.py @@ -0,0 +1,114 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import hashlib + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils.data import get_link_to_form +from lxml import etree + + +class CommonCode(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink + from frappe.types import DF + + additional_data: DF.Code | None + applies_to: DF.Table[DynamicLink] + code_list: DF.Link + common_code: DF.Data + description: DF.SmallText | None + title: DF.Data + # end: auto-generated types + + def validate(self): + self.validate_distinct_references() + + def validate_distinct_references(self): + """Ensure no two Common Codes of the same Code List are linked to the same document.""" + for link in self.applies_to: + existing_links = frappe.get_all( + "Common Code", + filters=[ + ["name", "!=", self.name], + ["code_list", "=", self.code_list], + ["Dynamic Link", "link_doctype", "=", link.link_doctype], + ["Dynamic Link", "link_name", "=", link.link_name], + ], + fields=["name", "common_code"], + ) + + if existing_links: + existing_link = existing_links[0] + frappe.throw( + _("{0} {1} is already linked to Common Code {2}.").format( + link.link_doctype, + link.link_name, + get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]), + ) + ) + + def from_genericode(self, column_map: dict, xml_element: "etree.Element"): + """Populate the Common Code document from a genericode XML element + + Args: + column_map (dict): A mapping of column names to XML column references. Keys: code, title, description + code (etree.Element): The XML element representing a code in the genericode file + """ + title_column = column_map.get("title") + code_column = column_map["code"] + description_column = column_map.get("description") + + self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text + + if title_column: + simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue") + self.title = simple_value_title.text if simple_value_title is not None else self.common_code + + if description_column: + simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue") + self.description = simple_value_descr.text if simple_value_descr is not None else None + + self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True) + + +def simple_hash(input_string, length=6): + return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest() + + +def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None): + """Import genericode file and create Common Code entries""" + file_path = frappe.utils.file_manager.get_file_path(file_name) + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file_path, parser=parser) + root = tree.getroot() + + # Construct the XPath expression + xpath_expr = ".//SimpleCodeList/Row" + filter_conditions = [ + f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items() + ] + if filter_conditions: + xpath_expr += "[" + " and ".join(filter_conditions) + "]" + + elements = root.xpath(xpath_expr) + total_elements = len(elements) + for i, xml_element in enumerate(elements, start=1): + common_code: "CommonCode" = frappe.new_doc("Common Code") + common_code.code_list = code_list + common_code.from_genericode(column_map, xml_element) + common_code.save() + frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes")) + + return total_elements + + +def on_doctype_update(): + frappe.db.add_index("Common Code", ["code_list", "common_code"]) diff --git a/erpnext/edi/doctype/common_code/common_code_list.js b/erpnext/edi/doctype/common_code/common_code_list.js new file mode 100644 index 00000000000..de1b665b161 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code_list.js @@ -0,0 +1,8 @@ +frappe.listview_settings["Common Code"] = { + onload: function (listview) { + listview.page.add_inner_button(__("Import Genericode File"), function () { + erpnext.edi.import_genericode(listview); + }); + }, + hide_name_column: true, +}; diff --git a/erpnext/edi/doctype/common_code/test_common_code.py b/erpnext/edi/doctype/common_code/test_common_code.py new file mode 100644 index 00000000000..e9c67b2cc82 --- /dev/null +++ b/erpnext/edi/doctype/common_code/test_common_code.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestCommonCode(FrappeTestCase): + pass diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 30121e5f2cb..882adec4d51 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -35,6 +35,14 @@ doctype_js = { "Newsletter": "public/js/newsletter.js", "Contact": "public/js/contact.js", } +doctype_list_js = { + "Code List": [ + "edi/doctype/code_list/code_list_import.js", + ], + "Common Code": [ + "edi/doctype/code_list/code_list_import.js", + ], +} override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"} diff --git a/erpnext/modules.txt b/erpnext/modules.txt index c53cdf467d2..b8b12e90fb0 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -18,3 +18,4 @@ Communication Telephony Bulk Transaction Subcontracting +EDI \ No newline at end of file From 8cc59e3be71f005e2c970eba8a654bbd11eb95d3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 18 Nov 2024 16:39:06 +0530 Subject: [PATCH 33/46] refactor: update test case (cherry picked from commit 4aab6f55f5bb47719fad95e366516a42b7f859e3) --- .../doctype/payment_entry/test_payment_entry.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index c61598b54a5..8758110534f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -957,9 +957,12 @@ class TestPaymentEntry(FrappeTestCase): self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2)) def test_gl_of_multi_currency_payment_transaction(self): - from erpnext.setup.doctype.currency_exchange.test_currency_exchange import save_new_records + from erpnext.setup.doctype.currency_exchange.test_currency_exchange import ( + save_new_records, + test_records, + ) - save_new_records(self.globalTestRecords["Currency Exchange"]) + save_new_records(test_records) paid_from = create_account( parent_account="Current Liabilities - _TC", account_name="_Test Cash USD", @@ -995,8 +998,8 @@ class TestPaymentEntry(FrappeTestCase): .run() ) expected_gl_entries = ( - (paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 8440.0), - ("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 8440.0, 0.0), + (paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0), + ("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0), ) self.assertEqual(gl_entries, expected_gl_entries) From 0ea6691189cff2141a914667adc023b3226ed23b Mon Sep 17 00:00:00 2001 From: Ismail Arif <38789073+ismxilxrif@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:56:35 +0800 Subject: [PATCH 34/46] chore: update oldest_items.json, change owner back to administrator Signed-off-by: Ismail Arif <38789073+ismxilxrif@users.noreply.github.com> (cherry picked from commit 7ceb24fb4cbff39b3f0e1cf2dd2db8f965aefe7e) --- erpnext/stock/dashboard_chart/oldest_items/oldest_items.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json b/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json index 46ad308f230..a55fe7a6a6c 100644 --- a/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json +++ b/erpnext/stock/dashboard_chart/oldest_items/oldest_items.json @@ -15,7 +15,7 @@ "module": "Stock", "name": "Oldest Items", "number_of_groups": 0, - "owner": "rohitw1991@gmail.com", + "owner": "Administrator", "report_name": "Stock Ageing", "roles": [], "timeseries": 0, From c2748e923e84713c9341aa1763129911bec65e9b Mon Sep 17 00:00:00 2001 From: ajiragroup <108009061+ajiragroup@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:13:54 +0545 Subject: [PATCH 35/46] refactor: update label and description on short year checkbox Is short/long year. (cherry picked from commit 1d6b9b405f0fef99d59330c7e1bd601f9e72c0b9) --- erpnext/accounts/doctype/fiscal_year/fiscal_year.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json index 66db37fe13b..de8f0337a3d 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.json +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.json @@ -72,10 +72,10 @@ }, { "default": "0", - "description": "Less than 12 months.", + "description": "More/Less than 12 months.", "fieldname": "is_short_year", "fieldtype": "Check", - "label": "Is Short Year", + "label": "Is Short/Long Year", "set_only_once": 1 } ], From 608966158aa81a4e71d31afec019c8913f89926a Mon Sep 17 00:00:00 2001 From: Nikolas Beckel <15029707+nikolas-beckel@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:40:55 +0100 Subject: [PATCH 36/46] fix: check if pricing rule matches with coupon code (#44104) * fix: check if pricing rule matches with coupon code * fix: correct linting error (cherry picked from commit 9d31bf7647882d5118617c86161f82429778c919) --- .../accounts/doctype/pricing_rule/pricing_rule.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 72ad0d096bc..73cb2483811 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -451,6 +451,16 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or [] ) + if pricing_rule.coupon_code_based == 1: + if not args.coupon_code: + return item_details + + coupon_code = frappe.db.get_value( + doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name" + ) + if args.coupon_code != coupon_code: + continue + if pricing_rule.get("suggestion"): continue @@ -476,9 +486,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False): pricing_rule.apply_rule_on_other_items ) - if pricing_rule.coupon_code_based == 1 and args.coupon_code is None: - return item_details - if not pricing_rule.validate_applied_rule: if pricing_rule.price_or_product_discount == "Price": apply_price_discount_rule(pricing_rule, item_details, args) From 7abcfca1cb1ec5d71126f5e9c9fd93bb51f65f17 Mon Sep 17 00:00:00 2001 From: Corentin Forler Date: Sat, 16 Nov 2024 14:35:58 +0100 Subject: [PATCH 37/46] fix(setup): Fix typo in COA setup (cherry picked from commit a245cc6b07b08a00088f09f19159ba97370695a0) --- erpnext/setup/setup_wizard/operations/taxes_setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index 0faebb6ab4c..6561f386c55 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -87,7 +87,10 @@ def simple_to_detailed(templates): def from_detailed_data(company_name, data): """Create Taxes and Charges Templates from detailed data.""" charts_company_name = company_name - if frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on"): + if ( + frappe.db.get_value("Company", company_name, "create_chart_of_accounts_based_on") + == "Existing Company" + ): charts_company_name = frappe.db.get_value("Company", company_name, "existing_company") coa_name = frappe.db.get_value("Company", charts_company_name, "chart_of_accounts") coa_data = data.get("chart_of_accounts", {}) From 6bff9d39e312dc5a5f157def06cbfaa8b5a54fa6 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 16 Apr 2024 12:20:16 +0530 Subject: [PATCH 38/46] fix: remove validate_name_in_customer function (cherry picked from commit 2b32d3644f909ba34a429c607335a0b7b54d7953) --- erpnext/setup/doctype/customer_group/customer_group.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/setup/doctype/customer_group/customer_group.py b/erpnext/setup/doctype/customer_group/customer_group.py index 06f2f43374e..5dd0fd02011 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.py +++ b/erpnext/setup/doctype/customer_group/customer_group.py @@ -71,14 +71,9 @@ class CustomerGroup(NestedSet): ) def on_update(self): - self.validate_name_with_customer() super().on_update() self.validate_one_root() - def validate_name_with_customer(self): - if frappe.db.exists("Customer", self.name): - frappe.msgprint(_("A customer with the same name already exists"), raise_exception=1) - def get_parent_customer_groups(customer_group): lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"]) From ad0c65500a70c5dd13e245dd41037fdfa5ac34ea Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:59:16 +0530 Subject: [PATCH 39/46] fix: update project cost from timesheet (backport #44211) (#44212) fix: update project cost from timesheet (#44211) (cherry picked from commit b21fb8f8b63aa517f6a0da8ec70e151213abc10b) Co-authored-by: rohitwaghchaure --- erpnext/projects/doctype/timesheet/timesheet.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 70494e9e966..7ab661c8822 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -169,10 +169,14 @@ class Timesheet(Document): task.save() tasks.append(data.task) - elif data.project and data.project not in projects: - frappe.get_doc("Project", data.project).update_project() + if data.project and data.project not in projects: projects.append(data.project) + for project in projects: + project_doc = frappe.get_doc("Project", project) + project_doc.update_project() + project_doc.save() + def validate_dates(self): for data in self.time_logs: if data.from_time and data.to_time and time_diff_in_hours(data.to_time, data.from_time) < 0: From f3c3f170a7d385aa971063d53de2650d6b3b6557 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 19 Nov 2024 14:32:43 +0530 Subject: [PATCH 40/46] fix: validate sales team to ensure all sales person are enabled (cherry picked from commit 548dbb33eb0ce911aef70efdf686338185575620) --- erpnext/controllers/selling_controller.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 49710de06f6..89a2111d50f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -167,6 +167,9 @@ class SellingController(StockController): total = 0.0 sales_team = self.get("sales_team") + + self.validate_sales_team(sales_team) + for sales_person in sales_team: self.round_floats_in(sales_person) @@ -186,6 +189,20 @@ class SellingController(StockController): if sales_team and total != 100.0: throw(_("Total allocated percentage for sales team should be 100")) + def validate_sales_team(self, sales_team): + sales_persons = [d.sales_person for d in sales_team] + + if not sales_persons: + return + + sales_person_status = frappe.db.get_all( + "Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"] + ) + + for row in sales_person_status: + if not row.enabled: + frappe.throw(_("Sales Person {0} is disabled.").format(row.name)) + def validate_max_discount(self): for d in self.get("items"): if d.item_code: From 83b9680318700fab2f60f7f81f1e96125ad78f56 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 19 Nov 2024 12:38:34 +0530 Subject: [PATCH 41/46] fix: disable conversion to user tz for sales order calender (cherry picked from commit cdf098c1939b4811da003d663f475c0c1d4d4899) --- erpnext/selling/doctype/sales_order/sales_order.py | 5 ++++- erpnext/selling/doctype/sales_order/sales_order_calendar.js | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 88528d7178f..d8b3f3c6dcf 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1230,7 +1230,10 @@ def get_events(start, end, filters=None): """, {"start": start, "end": end}, as_dict=True, - update={"allDay": 0}, + update={ + "allDay": 0, + "convertToUserTz": 0, + }, ) return data diff --git a/erpnext/selling/doctype/sales_order/sales_order_calendar.js b/erpnext/selling/doctype/sales_order/sales_order_calendar.js index f4c0e2ba72a..59a32bde7a3 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_calendar.js +++ b/erpnext/selling/doctype/sales_order/sales_order_calendar.js @@ -8,6 +8,7 @@ frappe.views.calendar["Sales Order"] = { id: "name", title: "customer_name", allDay: "allDay", + convertToUserTz: "convertToUserTz", }, gantt: true, filters: [ From 514fe69b6520b5a041889473312754ad178f2a10 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 19 Nov 2024 18:00:35 +0530 Subject: [PATCH 42/46] refactor: Update `Payment Request` search query in PE's reference (cherry picked from commit 4ab3499a173753a7bf8e8863edbcc4394c1f2469) --- .../doctype/payment_request/payment_request.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 577a1ea2426..ae974a8cf0e 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -945,17 +945,18 @@ def validate_payment(doc, method=None): @frappe.whitelist() def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters): # permission checks in `get_list()` - reference_doctype = filters.get("reference_doctype") - reference_name = filters.get("reference_doctype") + filters = frappe._dict(filters) - if not reference_doctype or not reference_name: + if not filters.reference_doctype or not filters.reference_name: return [] + if txt: + filters.name = ["like", f"%{txt}%"] + open_payment_requests = frappe.get_list( "Payment Request", filters={ - "reference_doctype": filters["reference_doctype"], - "reference_name": filters["reference_name"], + **filters, "status": ["!=", "Paid"], "outstanding_amount": ["!=", 0], # for compatibility with old data "docstatus": 1, From 4335659905e3c430c221941a4657b9e60af36e60 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Wed, 20 Nov 2024 12:00:41 +0530 Subject: [PATCH 43/46] fix: non group pos warehouse (cherry picked from commit d526be03946923004582fcbc9044c18734ca36f0) --- erpnext/selling/page/point_of_sale/pos_item_details.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 4673eaa9858..ad4b4cd15be 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -272,7 +272,7 @@ erpnext.PointOfSale.ItemDetails = class { }; this.warehouse_control.df.get_query = () => { return { - filters: { company: this.events.get_frm().doc.company }, + filters: { company: this.events.get_frm().doc.company, is_group: 0 }, }; }; this.warehouse_control.refresh(); From 74838394183432a4aa2cf839e844aa1735727e35 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 18 Nov 2024 20:08:11 +0530 Subject: [PATCH 44/46] fix: payment reco for jv with negative dr or cr amount (cherry picked from commit fee79b944575f6797ae3846e7394158a1daaac2d) --- .../payment_reconciliation.py | 16 +++++++++------- erpnext/accounts/utils.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index bbbb3c978ff..68e9eef711a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -211,12 +211,14 @@ class PaymentReconciliation(Document): if self.get("cost_center"): conditions.append(jea.cost_center == self.cost_center) - dr_or_cr = ( - "credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == "Receivable" - else "debit_in_account_currency" - ) - conditions.append(jea[dr_or_cr].gt(0)) + account_type = erpnext.get_party_account_type(self.party_type) + + if account_type == "Receivable": + dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency + elif account_type == "Payable": + dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency + + conditions.append(dr_or_cr.gt(0)) if self.bank_cash_account: conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%")) @@ -231,7 +233,7 @@ class PaymentReconciliation(Document): je.posting_date, je.remark.as_("remarks"), jea.name.as_("reference_row"), - jea[dr_or_cr].as_("amount"), + dr_or_cr.as_("amount"), jea.is_advance, jea.exchange_rate, jea.account_currency.as_("currency"), diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 37dbaef51a8..144039b794f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -630,6 +630,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]: update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name)) + rev_dr_or_cr = ( + "debit_in_account_currency" + if d["dr_or_cr"] == "credit_in_account_currency" + else "credit_in_account_currency" + ) + if jv_detail.get(rev_dr_or_cr): + d["dr_or_cr"] = rev_dr_or_cr + d["allocated_amount"] = d["allocated_amount"] * -1 + d["unadjusted_amount"] = d["unadjusted_amount"] * -1 + if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: # adjust the unreconciled balance amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) From 234741f35f6575e8f61d3a22d00774b203254f8a Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 19 Nov 2024 12:17:35 +0530 Subject: [PATCH 45/46] fix: added test cases (cherry picked from commit 6f9ea6422d9b03dea918267f6272b7bd36f96375) --- .../test_payment_reconciliation.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 1b19949bb7e..3f0fb29d671 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -632,6 +632,42 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("payments")), 0) + def test_negative_debit_or_credit_journal_against_invoice(self): + transaction_date = nowdate() + amount = 100 + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + # credit debtors account to record a payment + je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date) + je.accounts[1].party_type = "Customer" + je.accounts[1].party = self.customer + je.accounts[1].credit_in_account_currency = 0 + je.accounts[1].debit_in_account_currency = -1 * amount + je.save() + je.submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Difference amount should not be calculated for base currency accounts + for row in pr.allocation: + self.assertEqual(flt(row.get("difference_amount")), 0.0) + + pr.reconcile() + + # assert outstanding + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(si.outstanding_amount, 0) + + # check PR tool output + self.assertEqual(len(pr.get("invoices")), 0) + self.assertEqual(len(pr.get("payments")), 0) + def test_journal_against_journal(self): transaction_date = nowdate() sales = "Sales - _PR" @@ -954,6 +990,100 @@ class TestPaymentReconciliation(FrappeTestCase): frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" ) + def test_difference_amount_via_negative_debit_or_credit_journal_entry(self): + # Make Sale Invoice + si = self.create_sales_invoice( + qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True + ) + si.customer = self.customer4 + si.currency = "EUR" + si.conversion_rate = 85 + si.debit_to = self.debtors_eur + si.save().submit() + + # Make payment using Journal Entry + je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate()) + je1.multi_currency = 1 + je1.accounts[0].exchange_rate = 1 + je1.accounts[0].credit_in_account_currency = -8000 + je1.accounts[0].credit = -8000 + je1.accounts[0].debit_in_account_currency = 0 + je1.accounts[0].debit = 0 + je1.accounts[1].party_type = "Customer" + je1.accounts[1].party = self.customer4 + je1.accounts[1].exchange_rate = 80 + je1.accounts[1].credit_in_account_currency = 100 + je1.accounts[1].credit = 8000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je1.save() + je1.submit() + + je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate()) + je2.multi_currency = 1 + je2.accounts[0].exchange_rate = 1 + je2.accounts[0].credit_in_account_currency = -16000 + je2.accounts[0].credit = -16000 + je2.accounts[0].debit_in_account_currency = 0 + je2.accounts[0].debit = 0 + je2.accounts[1].party_type = "Customer" + je2.accounts[1].party = self.customer4 + je2.accounts[1].exchange_rate = 80 + je2.accounts[1].credit_in_account_currency = 200 + je1.accounts[1].credit = 16000 + je1.accounts[1].debit_in_account_currency = 0 + je1.accounts[1].debit = 0 + je2.save() + je2.submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer4 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 2) + + # Test exact payment allocation + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[0].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Test partial payment allocation (with excess payment entry) + pr.set("allocation", []) + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.invoices] + payments = [pr.payments[1].as_dict()] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR" + + self.assertEqual(pr.allocation[0].allocated_amount, 100) + self.assertEqual(pr.allocation[0].difference_amount, -500) + + # Check if difference journal entry gets generated for difference amount after reconciliation + pr.reconcile() + total_credit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, + "sum(credit) as amount", + group_by="reference_name", + )[0].amount + + # total credit includes the exchange gain/loss amount + self.assertEqual(flt(total_credit_amount, 2), 8500) + + jea_parent = frappe.db.get_all( + "Journal Entry Account", + filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500}, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" + ) + def test_difference_amount_via_payment_entry(self): # Make Sale Invoice si = self.create_sales_invoice( From 80f0d5b5ec8d20d70ec091650dba8ece6f2851c6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Nov 2024 12:51:29 +0530 Subject: [PATCH 46/46] chore: resolve conflict --- .../selling/doctype/quotation/quotation.json | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 8d6b96c8e67..d6ee87b5dee 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1070,57 +1070,19 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" -<<<<<<< HEAD -======= - }, - { - "fieldname": "utm_campaign", - "fieldtype": "Link", - "label": "Campaign", - "oldfieldname": "campaign", - "oldfieldtype": "Link", - "options": "UTM Campaign", - "print_hide": 1 - }, - { - "fieldname": "utm_source", - "fieldtype": "Link", - "label": "Source", - "oldfieldname": "source", - "oldfieldtype": "Select", - "options": "UTM Source", - "print_hide": 1 - }, - { - "fieldname": "utm_medium", - "fieldtype": "Link", - "label": "Medium", - "options": "UTM Medium", - "print_hide": 1 - }, - { - "fieldname": "utm_content", - "fieldtype": "Data", - "label": "Content", - "print_hide": 1 }, { "default": "0", "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" ->>>>>>> f8524d526b (fix: added disable_rounded_total field) } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-03-20 16:04:21.567847", -======= "modified": "2024-11-07 18:37:11.715189", ->>>>>>> f8524d526b (fix: added disable_rounded_total field) "modified_by": "Administrator", "module": "Selling", "name": "Quotation",