From b840eb90ebf558d91471c8bd1c629bc15214b94c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 18 Jan 2024 19:06:41 +0530 Subject: [PATCH 01/65] fix: Asset Depreciation WDV as per Income Tax Act --- erpnext/assets/doctype/asset/asset.py | 21 +++++++++++++++++-- .../create_custom_field_for_finance_book.py | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e5e022802d2..b90bf19b429 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -10,6 +10,7 @@ from frappe import _ from frappe.utils import ( add_days, add_months, + add_years, cint, date_diff, flt, @@ -23,6 +24,7 @@ from frappe.utils import ( import erpnext from erpnext.accounts.general_ledger import make_reverse_gl_entries +from erpnext.accounts.utils import get_fiscal_year from erpnext.assets.doctype.asset.depreciation import ( get_depreciation_accounts, get_disposal_account_and_cost_center, @@ -381,14 +383,22 @@ class Asset(AccountsController): should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date) depreciation_amount = 0 - number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1] + yearly_opening_wdv = value_after_depreciation + current_fiscal_year_end_date = None for n in range(start[finance_book.idx - 1], final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue + schedule_date = add_months(finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)) + if not current_fiscal_year_end_date: + current_fiscal_year_end_date = get_fiscal_year(finance_book.depreciation_start_date)[2] + elif getdate(schedule_date) > getdate(current_fiscal_year_end_date): + current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1) + yearly_opening_wdv = value_after_depreciation + if n > 0 and len(self.get("schedules")) > n - 1: prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount else: @@ -397,6 +407,7 @@ class Asset(AccountsController): depreciation_amount = get_depreciation_amount( self, value_after_depreciation, + yearly_opening_wdv, finance_book, n, prev_depreciation_amount, @@ -494,7 +505,10 @@ class Asset(AccountsController): if not depreciation_amount: continue - value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) + value_after_depreciation = flt( + value_after_depreciation - flt(depreciation_amount), + self.precision("gross_purchase_amount"), + ) # Adjust depreciation amount in the last period based on the expected value after useful life if finance_book.expected_value_after_useful_life and ( @@ -1380,6 +1394,7 @@ def get_total_days(date, frequency): def get_depreciation_amount( asset, depreciable_value, + yearly_opening_wdv, fb_row, schedule_idx=0, prev_depreciation_amount=0, @@ -1397,6 +1412,7 @@ def get_depreciation_amount( asset, fb_row, depreciable_value, + yearly_opening_wdv, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, @@ -1542,6 +1558,7 @@ def get_wdv_or_dd_depr_amount( asset, fb_row, depreciable_value, + yearly_opening_wdv, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, diff --git a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py index 2b8666d21b6..e0292098e7a 100644 --- a/erpnext/patches/v13_0/create_custom_field_for_finance_book.py +++ b/erpnext/patches/v13_0/create_custom_field_for_finance_book.py @@ -14,7 +14,7 @@ def execute(): "label": "For Income Tax", "fieldtype": "Check", "insert_after": "finance_book_name", - "description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.", + "description": "If the asset is put to use for less than 180 days in the first year, the first year's depreciation rate will be reduced by 50%.", } ] } From f604798a45fce39cd5e41c6a0aca8d7240848cdb Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 19 Jan 2024 11:29:26 +0530 Subject: [PATCH 02/65] fix: asset module test cases --- erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py | 2 +- erpnext/assets/doctype/asset/test_asset.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py index 6e946f74660..82147f4f6ab 100644 --- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py @@ -41,7 +41,7 @@ def test_record_generator(): ] start = 2012 - end = now_datetime().year + 5 + end = now_datetime().year + 25 for year in range(start, end): test_records.append( { diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 21afd5df851..708172b2980 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -845,7 +845,7 @@ class TestDepreciationMethods(AssetSetup): ["2030-12-31", 28630.14, 28630.14], ["2031-12-31", 35684.93, 64315.07], ["2032-12-31", 17842.47, 82157.54], - ["2033-06-06", 5342.46, 87500.0], + ["2033-06-06", 5342.47, 87500.01], ] schedules = [ @@ -957,7 +957,7 @@ class TestDepreciationBasics(AssetSetup): }, ) - depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) + depreciation_amount = get_depreciation_amount(asset, 100000, 100000, asset.finance_books[0]) self.assertEqual(depreciation_amount, 30000) def test_make_depreciation_schedule(self): From 0a6af795c48439cf10fc3b08b7ab531f38b586d8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 4 May 2023 16:27:43 +0530 Subject: [PATCH 03/65] refactor: cr notes will post for itself (cherry picked from commit db76e8a277f4f62dde079b2429f02689ea3fc25b) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 4 +--- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2af11159298..25e1e59f094 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -671,9 +671,7 @@ class PurchaseInvoice(BuyingController): "credit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "project": self.project, "cost_center": self.cost_center, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 66d9022d08f..a6cf9646b4d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1069,9 +1069,7 @@ class SalesInvoice(SellingController): "debit_in_account_currency": base_grand_total if self.party_account_currency == self.company_currency else grand_total, - "against_voucher": self.return_against - if cint(self.is_return) and self.return_against - else self.name, + "against_voucher": self.name, "against_voucher_type": self.doctype, "cost_center": self.cost_center, "project": self.project, From 014fcfa611ebdac821594d7958c7034da5289bd9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 15:59:57 +0530 Subject: [PATCH 04/65] refactor: remove return_against for cr/dr note filter (cherry picked from commit 00878707aecb9ee7efae49d1dea81150b9adbb1a) --- .../payment_reconciliation/payment_reconciliation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2b98baf2210..442df539f74 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -228,15 +228,12 @@ class PaymentReconciliation(Document): self.common_filter_conditions.append(ple.account == self.receivable_payable_account) self.get_return_invoices() - return_invoices = [ - x for x in self.return_invoices if x.return_against == None or x.return_against == "" - ] outstanding_dr_or_cr = [] - if return_invoices: + if self.return_invoices: ple_query = QueryPaymentLedger() return_outstanding = ple_query.get_voucher_outstandings( - vouchers=return_invoices, + vouchers=self.return_invoices, common_filter=self.common_filter_conditions, posting_date=self.ple_posting_date_filter, min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, From 43b40d92e57413950667788b05b9574bb58306d3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 13 Aug 2023 15:48:25 +0530 Subject: [PATCH 05/65] refactor(test): return invoice will have -ve outstanding (cherry picked from commit b30c1e1abfcb45950709e7b12481d9e187b845f4) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d050299912d..45a01ab04aa 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1528,8 +1528,8 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(party_credited, 1000) # Check outstanding amount - self.assertFalse(si1.outstanding_amount) - self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500) + self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000) + self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) def test_gle_made_when_asset_is_returned(self): create_asset_data() From 03040c1c7f16ffbbb79fa3c8123a88c43f8703ba Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Aug 2023 09:02:41 +0530 Subject: [PATCH 06/65] refactor(test): ledger entries will be against itself (cherry picked from commit 0e2fb1188a66d6f1e6fe89564106c024e940107f) --- .../test_payment_ledger_entry.py | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index fc6dbba7e7f..ce9579ed613 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase): cr_note1.return_against = si3.name cr_note1 = cr_note1.save().submit() - pl_entries = ( + pl_entries_si3 = ( qb.from_(ple) .select( ple.voucher_type, @@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase): .run(as_dict=True) ) - expected_values = [ + pl_entries_cr_note1 = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + ple.against_voucher_type, + ple.against_voucher_no, + ple.amount, + ple.delinked, + ) + .where( + (ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name) + ) + .orderby(ple.creation) + .run(as_dict=True) + ) + + expected_values_for_si3 = [ { "voucher_type": si3.doctype, "voucher_no": si3.name, @@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase): "against_voucher_no": si3.name, "amount": amount, "delinked": 0, - }, + } + ] + # credit/debit notes post ledger entries against itself + expected_values_for_cr_note1 = [ { "voucher_type": cr_note1.doctype, "voucher_no": cr_note1.name, - "against_voucher_type": si3.doctype, - "against_voucher_no": si3.name, + "against_voucher_type": cr_note1.doctype, + "against_voucher_no": cr_note1.name, "amount": -amount, "delinked": 0, }, ] - self.assertEqual(pl_entries[0], expected_values[0]) - self.assertEqual(pl_entries[1], expected_values[1]) + self.assertEqual(pl_entries_si3, expected_values_for_si3) + self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1) def test_je_against_inv_and_note(self): ple = self.ple From fecab1338e40a57c304ef1445ab6c2a6b1edec98 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 16:31:10 +0530 Subject: [PATCH 07/65] refactor(test): payments to invoice with -ve outstanding (cherry picked from commit f6e4ac2b6267ac0b88f399ada1482fc1366a126a) --- .../payment_entry/test_payment_entry.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index a7f1f08cbc2..d035149ff93 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -705,7 +705,50 @@ class TestPaymentEntry(FrappeTestCase): pe2.submit() # create return entry against si1 - create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1) + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") + + # create JE(credit note) manually against si1 and cr_note + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": si1.company, + "voucher_type": "Credit Note", + "posting_date": nowdate(), + } + ) + je.append( + "accounts", + { + "account": si1.debit_to, + "party_type": "Customer", + "party": si1.customer, + "debit": 0, + "credit": 100, + "debit_in_account_currency": 0, + "credit_in_account_currency": 100, + "reference_type": si1.doctype, + "reference_name": si1.name, + "cost_center": si1.items[0].cost_center, + }, + ) + je.append( + "accounts", + { + "account": cr_note.debit_to, + "party_type": "Customer", + "party": cr_note.customer, + "debit": 100, + "credit": 0, + "debit_in_account_currency": 100, + "credit_in_account_currency": 0, + "reference_type": cr_note.doctype, + "reference_name": cr_note.name, + "cost_center": cr_note.items[0].cost_center, + }, + ) + je.save().submit() + si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount") self.assertEqual(si1_outstanding, -100) From 1aeeac4d0623110d642f378578df910e840d1886 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Aug 2023 10:10:42 +0530 Subject: [PATCH 08/65] refactor: criteria for `Credit Note Issued` and `Debit Note Issued` (cherry picked from commit 8f695123cd1e1e931b9ccac314687e010db1524d) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 8 ++------ erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 25e1e59f094..802ac8080a9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1538,12 +1538,8 @@ class PurchaseInvoice(BuyingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to debit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Debit Note Issued" elif self.is_return == 1: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a6cf9646b4d..fc37e5ec67f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1696,12 +1696,8 @@ class SalesInvoice(SellingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to credit note issued against invoice - elif ( - outstanding_amount <= 0 - and self.is_return == 0 - and frappe.db.get_value( - "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} - ) + elif self.is_return == 0 and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} ): self.status = "Credit Note Issued" elif self.is_return == 1: From 956f05238a18d7efdf64adcc16ba9b4f01581540 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 23 Aug 2023 17:58:15 +0530 Subject: [PATCH 09/65] refactor: Payment btn criteria for Cr/Dr notes (cherry picked from commit 60eee564bfaa473ecb1afcf16d6e6f65b9928e34) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 3 +-- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 0add6c57da6..665fc6edcc9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -99,8 +99,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } } - if(doc.docstatus == 1 && doc.outstanding_amount != 0 - && !(doc.is_return && doc.return_against) && !doc.on_hold) { + if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 1e8428d9e29..2ac7370f4ef 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -91,8 +91,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e if(doc.update_stock) this.show_stock_ledger(); - if (doc.docstatus == 1 && doc.outstanding_amount!=0 - && !(cint(doc.is_return) && doc.return_against)) { + if (doc.docstatus == 1 && doc.outstanding_amount!=0) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), From 5c9ad21a3f993ee8127947ac9cde5da8c8611ab6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 30 Nov 2023 13:11:11 +0530 Subject: [PATCH 10/65] refactor: dimensions section in allocation table in reconciliation (cherry picked from commit 1cde804c773de41520a6148e7d99ab0c23c39ae1) --- .../payment_reconciliation_allocation.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 491c67818df..cbfd9b2d8b9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -24,6 +24,7 @@ "difference_account", "exchange_rate", "currency", + "accounting_dimensions_section", "cost_center" ], "fields": [ @@ -157,12 +158,17 @@ "fieldname": "gain_loss_posting_date", "fieldtype": "Date", "label": "Difference Posting Date" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-11-17 17:33:38.612615", + "modified": "2023-12-14 12:32:45.554730", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", From b25e8ae14c5b3225fdb4cff7436f17a03ce2103b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Dec 2023 12:16:50 +0530 Subject: [PATCH 11/65] refactor: update dimension doctypes in hooks (cherry picked from commit cfb3d872673844f04f5c9dd3f7d7f56288e5dd22) --- erpnext/hooks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 481b740d775..5a4e8539bf6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -548,6 +548,8 @@ accounting_dimension_doctypes = [ "Account Closing Balance", "Supplier Quotation", "Supplier Quotation Item", + "Payment Reconciliation", + "Payment Reconciliation Allocation", ] # get matching queries for Bank Reconciliation From 4114c0e85415480e44909ceccb855e84b2312a3c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Dec 2023 12:39:13 +0530 Subject: [PATCH 12/65] refactor: dimensions filter section in payment reconciliation (cherry picked from commit 20e0acc20a218029d7101a1ba6ff3c1ae03fac02) --- .../payment_reconciliation.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index e8985de4e1e..a14a74a1f47 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -24,6 +24,7 @@ "invoice_limit", "payment_limit", "bank_cash_account", + "accounting_dimensions_section", "cost_center", "sec_break1", "invoice_name", @@ -199,6 +200,14 @@ "fieldname": "payment_name", "fieldtype": "Data", "label": "Filter on Payment" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval: doc.invoices.length == 0", + "depends_on": "eval:doc.receivable_payable_account", + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions Filter" } ], "hide_toolbar": 1, @@ -206,7 +215,7 @@ "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-11-17 17:33:55.701726", + "modified": "2023-12-14 12:38:44.910625", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", From 81b87ef2e27089805b9069a715f08952f7e8884f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 14 Dec 2023 13:33:24 +0530 Subject: [PATCH 13/65] refactor: column break in dimension section (cherry picked from commit 20576e0f47ba3c4937121bfab1e0d8d395a590ce) --- .../payment_reconciliation/payment_reconciliation.json | 7 ++++++- .../payment_reconciliation_allocation.json | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index a14a74a1f47..948bae3dfe6 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -26,6 +26,7 @@ "bank_cash_account", "accounting_dimensions_section", "cost_center", + "dimension_col_break", "sec_break1", "invoice_name", "invoices", @@ -208,6 +209,10 @@ "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions Filter" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, @@ -215,7 +220,7 @@ "is_virtual": 1, "issingle": 1, "links": [], - "modified": "2023-12-14 12:38:44.910625", + "modified": "2023-12-14 13:38:16.264013", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index cbfd9b2d8b9..3f85b213500 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -25,7 +25,8 @@ "exchange_rate", "currency", "accounting_dimensions_section", - "cost_center" + "cost_center", + "dimension_col_break" ], "fields": [ { @@ -163,12 +164,16 @@ "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" } ], "is_virtual": 1, "istable": 1, "links": [], - "modified": "2023-12-14 12:32:45.554730", + "modified": "2023-12-14 13:38:26.104150", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", From 66cadb8b9f2afd5938401d2cb9134f6bb823f5e2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Dec 2023 16:02:12 +0530 Subject: [PATCH 14/65] refactor: handle dimension filters (cherry picked from commit c1fe4bcc64775507a3bd8e02b61274d8dc2d6447) --- .../payment_reconciliation/payment_reconciliation.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2b98baf2210..b8096843d6a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( is_any_doc_running, ) @@ -592,6 +593,14 @@ class PaymentReconciliation(Document): if not invoices_to_reconcile: frappe.throw(_("No records found in Allocation table")) + def build_dimensions_filter_conditions(self): + ple = qb.DocType("Payment Ledger Entry") + dimensions_and_defaults = get_dimensions() + for x in dimensions_and_defaults[0]: + dimension = x.fieldname + if self.get(dimension): + self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) + def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False): self.common_filter_conditions.clear() self.accounting_dimension_filter_conditions.clear() @@ -615,6 +624,8 @@ class PaymentReconciliation(Document): if self.to_payment_date: self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date)) + self.build_dimensions_filter_conditions() + def get_conditions(self, get_payments=False): condition = " and company = '{0}' ".format(self.company) From 3d62bce885611ae31ddf69d1af12527da82ddd15 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Dec 2023 17:25:02 +0530 Subject: [PATCH 15/65] refactor: pass dimension filters to query (cherry picked from commit ff60ec85b85d5548886e247b72cf1262587feba3) # Conflicts: # erpnext/controllers/accounts_controller.py --- .../payment_reconciliation.py | 9 ++++ erpnext/controllers/accounts_controller.py | 45 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b8096843d6a..f0e381b822d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -113,6 +113,15 @@ class PaymentReconciliation(Document): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = self.get_conditions(get_payments=True) + # pass dynamic dimension filter values to query builder + dimensions = {} + dimensions_and_defaults = get_dimensions() + for x in dimensions_and_defaults[0]: + dimension = x.fieldname + if self.get(dimension): + dimensions.update({dimension: self.get(dimension)}) + condition.update({"accounting_dimensions": dimensions}) + payment_entries = get_advance_payment_entries_for_regional( self.party_type, self.party, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cc8ee9226a4..d5bc69fe5db 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,11 @@ import json import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +<<<<<<< HEAD +======= +from frappe.query_builder import Criterion +from frappe.query_builder.custom import ConstantColumn +>>>>>>> ff60ec85b8 (refactor: pass dimension filters to query) from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( add_days, @@ -2542,6 +2547,7 @@ def get_advance_payment_entries( payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" +<<<<<<< HEAD if order_list or against_all_orders: if order_list: reference_condition = " and t2.reference_name in ({0})".format( @@ -2550,6 +2556,45 @@ def get_advance_payment_entries( else: reference_condition = "" order_list = [] +======= + if payment_type == "Receive": + q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) + else: + q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) + + if condition: + # conditions should be built as an array and passed as Criterion + common_filter_conditions = [] + + common_filter_conditions.append(payment_entry.company == condition["company"]) + if condition.get("name", None): + common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%")) + + if condition.get("from_payment_date"): + common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"])) + + if condition.get("to_payment_date"): + common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"])) + + if condition.get("get_payments") == True: + if condition.get("cost_center"): + common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"]) + + if condition.get("accounting_dimensions"): + for field, val in condition.get("accounting_dimensions").items(): + common_filter_conditions.append(payment_entry[field] == val) + + if condition.get("minimum_payment_amount"): + common_filter_conditions.append( + payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"]) + ) + + if condition.get("maximum_payment_amount"): + common_filter_conditions.append( + payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"]) + ) + q = q.where(Criterion.all(common_filter_conditions)) +>>>>>>> ff60ec85b8 (refactor: pass dimension filters to query) payment_name_filter = "" if payment_name: From b2db6d054658e26dea6f3576a2b6fb1cc2872c9b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 19 Dec 2023 17:07:54 +0530 Subject: [PATCH 16/65] refactor: set query filters for dimensions (cherry picked from commit ad8475cb8b24d40b04f86903feee08ecac6aa1f1) --- .../payment_reconciliation.js | 21 +++++++++++++++++++ .../payment_reconciliation.py | 17 +++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 5cdedb73c09..16d1839679c 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -83,6 +83,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.change_custom_button_type('Allocate', null, 'default'); } + this.frm.trigger("set_query_for_dimension_filters"); + // check for any running reconciliation jobs if (this.frm.doc.receivable_payable_account) { this.frm.call({ @@ -113,6 +115,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } } + set_query_for_dimension_filters() { + frappe.call({ + method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters", + args: { + company: this.frm.doc.company, + }, + callback: (r) => { + if (!r.exc && r.message) { + r.message.forEach(x => { + this.frm.set_query(x.fieldname, () => { + return { + 'filters': x.filters + }; + }); + }); + } + } + }); + } company() { this.frm.set_value('party', ''); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index f0e381b822d..45620bae59f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -757,3 +757,20 @@ def reconcile_dr_cr_note(dr_cr_notes, company): @erpnext.allow_regional def adjust_allocations_for_taxes(doc): pass + + +@frappe.whitelist() +def get_queries_for_dimension_filters(company: str = None): + dimensions_with_filters = [] + for d in get_dimensions()[0]: + filters = {} + meta = frappe.get_meta(d.document_type) + if meta.has_field("company") and company: + filters.update({"company": company}) + + if meta.is_tree: + filters.update({"is_group": 0}) + + dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters}) + + return dimensions_with_filters From dac422a0e149905ee739c18b5840ec096d54c1f0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Dec 2023 17:19:27 +0530 Subject: [PATCH 17/65] refactor: pass dimension details to query (cherry picked from commit 5dc22e1811bb1841bb8c790cc3a1e1315cef6074) # Conflicts: # erpnext/accounts/utils.py --- .../payment_reconciliation.py | 17 ++++++++++++----- erpnext/accounts/utils.py | 6 ++++++ erpnext/controllers/accounts_controller.py | 2 ++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 45620bae59f..b7be45a7ebd 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -29,6 +29,7 @@ class PaymentReconciliation(Document): self.common_filter_conditions = [] self.accounting_dimension_filter_conditions = [] self.ple_posting_date_filter = [] + self.dimensions = get_dimensions()[0] def load_from_db(self): # 'modified' attribute is required for `run_doc_method` to work properly. @@ -115,8 +116,7 @@ class PaymentReconciliation(Document): # pass dynamic dimension filter values to query builder dimensions = {} - dimensions_and_defaults = get_dimensions() - for x in dimensions_and_defaults[0]: + for x in self.dimensions: dimension = x.fieldname if self.get(dimension): dimensions.update({dimension: self.get(dimension)}) @@ -472,7 +472,7 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() def get_payment_details(self, row, dr_or_cr): - return frappe._dict( + payment_details = frappe._dict( { "voucher_type": row.get("reference_type"), "voucher_no": row.get("reference_name"), @@ -495,6 +495,14 @@ class PaymentReconciliation(Document): } ) + dimensions_dict = {} + for x in self.dimensions: + if row.get(x.fieldname): + dimensions_dict.update({x.fieldname: row.get(x.fieldname)}) + + payment_details.update({"dimensions": dimensions_dict}) + return payment_details + def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: if not self.get(fieldname): @@ -604,8 +612,7 @@ class PaymentReconciliation(Document): def build_dimensions_filter_conditions(self): ple = qb.DocType("Payment Ledger Entry") - dimensions_and_defaults = get_dimensions() - for x in dimensions_and_defaults[0]: + for x in self.dimensions: dimension = x.fieldname if self.get(dimension): self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index d9fb75a86d2..74284d9c1d9 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -630,6 +630,11 @@ def update_reference_in_payment_entry( if d.difference_amount is not None else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.difference_amount, +<<<<<<< HEAD +======= + "account": d.account, + "dimensions": d.dimensions, +>>>>>>> 5dc22e1811 (refactor: pass dimension details to query) } if d.voucher_detail_no: @@ -1991,6 +1996,7 @@ def create_gain_loss_journal( ref2_dn, ref2_detail_no, cost_center, + dimensions, ) -> str: journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d5bc69fe5db..9da71654227 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1178,6 +1178,7 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), arg.get("cost_center"), + {}, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1258,6 +1259,7 @@ class AccountsController(TransactionBase): self.name, d.idx, self.cost_center, + {}, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( From 8f87c588ecec027019e66809c73a7eef2f19cea2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Dec 2023 17:42:41 +0530 Subject: [PATCH 18/65] refactor: replace sql with query builder for Jourals query (cherry picked from commit 9c5a79209eb014c90aac46a5dd5ed0d9b7cb8f87) --- .../payment_reconciliation.py | 133 ++++++++---------- 1 file changed, 61 insertions(+), 72 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b7be45a7ebd..5fcb35e7846 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -136,66 +136,67 @@ class PaymentReconciliation(Document): return payment_entries def get_jv_entries(self): - condition = self.get_conditions() + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + conditions = self.get_journal_filter_conditions() + + # Dimension filters + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + conditions.append(jea[dimension] == self.get(dimension)) if self.payment_name: - condition += f" and t1.name like '%%{self.payment_name}%%'" + conditions.append(je.name.like(f"%%{self.payent_name}%%")) if self.get("cost_center"): - condition += f" and t2.cost_center = '{self.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)) - bank_account_condition = ( - "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" + if self.bank_cash_account: + conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%")) + + journal_query = ( + qb.from_(je) + .inner_join(jea) + .on(jea.parent == je.name) + .select( + ConstantColumn("Journal Entry").as_("reference_type"), + je.name.as_("reference_name"), + je.posting_date, + je.remark.as_("remarks"), + jea.name.as_("reference_row"), + jea[dr_or_cr].as_("amount"), + jea.is_advance, + jea.exchange_rate, + jea.account_currency.as_("currency"), + jea.cost_center.as_("cost_center"), + ) + .where( + (je.docstatus == 1) + & (jea.party_type == self.party_type) + & (jea.party == self.party) + & (jea.account == self.receivable_payable_account) + & ( + (jea.reference_type == "") + | (jea.reference_type.isnull()) + | (jea.reference_type.isin(("Sales Order", "Purchase Order"))) + ) + ) + .where(Criterion.all(conditions)) + .orderby(je.posting_date) ) - limit = f"limit {self.payment_limit}" if self.payment_limit else " " + if self.payment_limit: + journal_query = journal_query.limit(self.payment_limit) - # nosemgrep - journal_entries = frappe.db.sql( - """ - select - "Journal Entry" as reference_type, t1.name as reference_name, - t1.posting_date, t1.remark as remarks, t2.name as reference_row, - {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate, - t2.account_currency as currency, t2.cost_center as cost_center - from - `tabJournal Entry` t1, `tabJournal Entry Account` t2 - where - t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1 - and t2.party_type = %(party_type)s and t2.party = %(party)s - and t2.account = %(account)s and {dr_or_cr} > 0 {condition} - and (t2.reference_type is null or t2.reference_type = '' or - (t2.reference_type in ('Sales Order', 'Purchase Order') - and t2.reference_name is not null and t2.reference_name != '')) - and (CASE - WHEN t1.voucher_type in ('Debit Note', 'Credit Note') - THEN 1=1 - ELSE {bank_account_condition} - END) - order by t1.posting_date - {limit} - """.format( - **{ - "dr_or_cr": dr_or_cr, - "bank_account_condition": bank_account_condition, - "condition": condition, - "limit": limit, - } - ), - { - "party_type": self.party_type, - "party": self.party, - "account": self.receivable_payable_account, - "bank_cash_account": "%%%s%%" % self.bank_cash_account, - }, - as_dict=1, - ) + journal_entries = journal_query.run(as_dict=True) return list(journal_entries) @@ -642,37 +643,25 @@ class PaymentReconciliation(Document): self.build_dimensions_filter_conditions() - def get_conditions(self, get_payments=False): - condition = " and company = '{0}' ".format(self.company) + def get_journal_filter_conditions(self): + conditions = [] + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + conditions.append(je.company == self.company) - if self.get("cost_center") and get_payments: - condition = " and cost_center = '{0}' ".format(self.cost_center) + if self.from_payment_date: + conditions.append(je.posting_date.gte(self.from_payment_date)) - condition += ( - " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) - if self.from_payment_date - else "" - ) - condition += ( - " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) - if self.to_payment_date - else "" - ) + if self.to_payment_date: + conditions.append(je.posting_date.lte(self.to_payment_date)) if self.minimum_payment_amount: - condition += ( - " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) - if get_payments - else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) - ) - if self.maximum_payment_amount: - condition += ( - " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) - if get_payments - else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) - ) + conditions.append(je.total_debit.gte(self.minimum_payment_amount)) - return condition + if self.maximum_payment_amount: + conditions.append(je.total_debit.lte(self.maximumb_payment_amount)) + + return conditions def reconcile_dr_cr_note(dr_cr_notes, company): From ecd36501afea1a413b2ef7a7389303ce3baf883c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 2 Jan 2024 11:33:02 +0530 Subject: [PATCH 19/65] refactor: partial change on outstanding invoice popup (cherry picked from commit 2154502955166243e354897d7dcb22d1987c4693) --- .../doctype/payment_entry/payment_entry.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6d9d6920105..b7a4e854ff2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -629,6 +629,20 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_unallocated_amount(frm); }, + get_dimensions: function(frm) { + let result = []; + frappe.call({ + method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", + async: false, + callback: function(r) { + if(!r.exc) { + result = r.message[0].map(elem => elem.document_type); + } + } + }); + return result; + }, + get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { const today = frappe.datetime.get_today(); const fields = [ From 41c074d0bbca82b21fcd27b66adc84fa8a03c6b0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jan 2024 14:56:51 +0530 Subject: [PATCH 20/65] fix: typo's and parameter changes (cherry picked from commit 0ec17590ae062fbda0c14a2806ec1ac07c638593) --- .../doctype/payment_reconciliation/payment_reconciliation.py | 5 +++-- erpnext/accounts/utils.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5fcb35e7846..30250caddb2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -147,7 +147,7 @@ class PaymentReconciliation(Document): conditions.append(jea[dimension] == self.get(dimension)) if self.payment_name: - conditions.append(je.name.like(f"%%{self.payent_name}%%")) + conditions.append(je.name.like(f"%%{self.payment_name}%%")) if self.get("cost_center"): conditions.append(jea.cost_center == self.cost_center) @@ -659,7 +659,7 @@ class PaymentReconciliation(Document): conditions.append(je.total_debit.gte(self.minimum_payment_amount)) if self.maximum_payment_amount: - conditions.append(je.total_debit.lte(self.maximumb_payment_amount)) + conditions.append(je.total_debit.lte(self.maximum_payment_amount)) return conditions @@ -747,6 +747,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.against_voucher, None, inv.cost_center, + frappe._dict(), ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 74284d9c1d9..9af81168e59 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1998,6 +1998,8 @@ def create_gain_loss_journal( cost_center, dimensions, ) -> str: + # TODO: pass dimensions to Journal + journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = company From 937262b57299f63e9207637a777adee5d5974325 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 15 Jan 2024 16:13:26 +0530 Subject: [PATCH 21/65] refactor: Credit Note and its Exc gain/loss JE inherits dimensions (cherry picked from commit ab939cc6e8ab3669f1e9b0f007e9459be180ac32) --- .../payment_reconciliation.py | 31 ++++++++++++++----- erpnext/accounts/utils.py | 6 ++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 30250caddb2..f8408b4b13d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -402,8 +402,15 @@ class PaymentReconciliation(Document): row = self.append("allocation", {}) row.update(entry) + def update_dimension_values_in_allocated_entries(self, res): + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + res[dimension] = self.get(dimension) + return res + def get_allocated_entry(self, pay, inv, allocated_amount): - return frappe._dict( + res = frappe._dict( { "reference_type": pay.get("reference_type"), "reference_name": pay.get("reference_name"), @@ -419,6 +426,9 @@ class PaymentReconciliation(Document): } ) + res = self.update_dimension_values_in_allocated_entries(res) + return res + def reconcile_allocations(self, skip_ref_details_update_for_pe=False): adjust_allocations_for_taxes(self) dr_or_cr = ( @@ -444,7 +454,7 @@ class PaymentReconciliation(Document): reconcile_against_document(entry_list, skip_ref_details_update_for_pe) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes, self.company) + reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions) @frappe.whitelist() def reconcile(self): @@ -496,12 +506,10 @@ class PaymentReconciliation(Document): } ) - dimensions_dict = {} for x in self.dimensions: if row.get(x.fieldname): - dimensions_dict.update({x.fieldname: row.get(x.fieldname)}) + payment_details[x.fieldname] = row.get(x.fieldname) - payment_details.update({"dimensions": dimensions_dict}) return payment_details def check_mandatory_to_fetch(self): @@ -664,7 +672,7 @@ class PaymentReconciliation(Document): return conditions -def reconcile_dr_cr_note(dr_cr_notes, company): +def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None): for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -714,6 +722,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company): } ) + # Credit Note(JE) will inherit the same dimension values as payment + dimensions_dict = frappe._dict() + if active_dimensions: + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = inv.get(dim.fieldname) + + jv.accounts[0].update(dimensions_dict) + jv.accounts[1].update(dimensions_dict) + jv.flags.ignore_mandatory = True jv.flags.skip_remarks_creation = True jv.flags.ignore_exchange_rate = True @@ -747,7 +764,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.against_voucher, None, inv.cost_center, - frappe._dict(), + dimensions_dict, ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9af81168e59..07ba01fe529 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1998,8 +1998,6 @@ def create_gain_loss_journal( cost_center, dimensions, ) -> str: - # TODO: pass dimensions to Journal - journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = company @@ -2032,7 +2030,7 @@ def create_gain_loss_journal( dr_or_cr + "_in_account_currency": 0, } ) - + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_account = frappe._dict( @@ -2048,7 +2046,7 @@ def create_gain_loss_journal( reverse_dr_or_cr: abs(exc_gain_loss), } ) - + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_entry.save() From c3ffb7a4c4acee7a962def5feef735aec557aef7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 17 Jan 2024 12:37:43 +0530 Subject: [PATCH 22/65] refactor: apply dimension filters on cr/dr notes (cherry picked from commit 188ff8cde794bb1ef1043f0e47469d65944aac1e) --- .../doctype/payment_reconciliation/payment_reconciliation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index f8408b4b13d..d400dcdceb9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -253,6 +253,7 @@ class PaymentReconciliation(Document): min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None, max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None, get_payments=True, + accounting_dimensions=self.accounting_dimension_filter_conditions, ) for inv in return_outstanding: From c1591ec8e1a504d31936e88ed16e5a5b6cc5e38b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 17 Jan 2024 12:50:05 +0530 Subject: [PATCH 23/65] chore: test dimension filter output (cherry picked from commit e3c44231abbbe389a1f815ab77f2d6ff0c614e1b) --- .../tests/test_accounts_controller.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 97d3c5c32de..3a3e6def48a 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase): 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes 40 series - Company default Cost center is unset + 50 series - Dimension inheritence """ def setUp(self): @@ -1255,3 +1256,88 @@ class TestAccountsController(FrappeTestCase): ) frappe.db.set_value("Company", self.company, "cost_center", cc) + + def setup_dimensions(self): + # create dimension + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + ) + + create_dimension() + # make it non-mandatory + loc = frappe.get_doc("Accounting Dimension", "Location") + for x in loc.dimension_defaults: + x.mandatory_for_bs = False + x.mandatory_for_pl = False + loc.save() + + def test_50_dimensions_filter(self): + """ + Gain/Loss JE should inherit its dimension from payment + """ + self.setup_dimensions() + rate_in_account_currency = 1 + + # Invoices + si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si1.department = "Management" + si1.save().submit() + + si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si2.department = "Operations" + si2.save().submit() + + # Payments + cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note1.department = "Management" + cr_note1.is_return = 1 + cr_note1.save().submit() + + cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note2.department = "Legal" + cr_note2.is_return = 1 + cr_note2.save().submit() + + pe1 = get_payment_entry(si1.doctype, si1.name) + pe1.references = [] + pe1.department = "Research & Development" + pe1.save().submit() + + pe2 = get_payment_entry(si1.doctype, si1.name) + pe2.references = [] + pe2.department = "Management" + pe2.save().submit() + + je1 = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + je1.accounts[0].party_type = "Customer" + je1.accounts[0].party = self.customer + je1.accounts[0].department = "Management" + je1.save().submit() + + # assert dimension filter's result + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 5) + + pr.department = "Legal" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 1) + + pr.department = "Management" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 3) + + pr.department = "Research & Development" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 1) From de948f23c130d95936309be1a790e41878f6e059 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 13:20:06 +0530 Subject: [PATCH 24/65] test: dimension inheritance for cr note reconciliation (cherry picked from commit ba5a7c8cd8ee6fc09b0d81ffbe8b364e584f1f1b) --- .../tests/test_accounts_controller.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 3a3e6def48a..a448ad4a572 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1273,7 +1273,7 @@ class TestAccountsController(FrappeTestCase): def test_50_dimensions_filter(self): """ - Gain/Loss JE should inherit its dimension from payment + Test workings of dimension filters """ self.setup_dimensions() rate_in_account_currency = 1 @@ -1341,3 +1341,45 @@ class TestAccountsController(FrappeTestCase): pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) + + def test_51_cr_note_should_inherit_dimension_from_payment(self): + self.setup_dimensions() + rate_in_account_currency = 1 + + # Invoice + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si.department = "Management" + si.save().submit() + + # Payment + cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note.department = "Management" + cr_note.is_return = 1 + cr_note.save().submit() + + pr = self.create_payment_reconciliation() + pr.department = "Management" + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss) + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_cr_note), 2) + self.assertEqual(exc_je_for_si, exc_je_for_cr_note) + + for x in exc_je_for_si + exc_je_for_cr_note: + with self.subTest(x=x): + self.assertEqual( + [cr_note.department, cr_note.department], + frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"), + ) From a9197023199547351f5cdd59e80e6b2ebda013ae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 14:35:06 +0530 Subject: [PATCH 25/65] refactor: pass dimension values to Gain/Loss journal (cherry picked from commit c44eb432a59fb3ffb3748e47356068499f1129b1) # Conflicts: # erpnext/accounts/utils.py --- .../payment_reconciliation.py | 2 +- erpnext/accounts/utils.py | 32 ++++++++++++++++--- erpnext/controllers/accounts_controller.py | 8 +++-- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index d400dcdceb9..79897e0803a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -452,7 +452,7 @@ class PaymentReconciliation(Document): reconciled_entry.append(payment_details) if entry_list: - reconcile_against_document(entry_list, skip_ref_details_update_for_pe) + reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions) if dr_or_cr_notes: reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 07ba01fe529..c207d9338e6 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -434,7 +434,19 @@ def add_cc(args=None): return cc.name -def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep +def _build_dimensions_dict_for_exc_gain_loss( + entry: dict | object = None, active_dimensions: list = None +): + dimensions_dict = frappe._dict() + if entry and active_dimensions: + for dim in active_dimensions: + dimensions_dict[dim.fieldname] = entry.get(dim.fieldname) + return dimensions_dict + + +def reconcile_against_document( + args, skip_ref_details_update_for_pe=False, active_dimensions=None +): # nosemgrep """ Cancel PE or JV, Update against document, split if required and resubmit """ @@ -459,6 +471,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n check_if_advance_entry_modified(entry) validate_allocated_amount(entry) + dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions) + # update ref in advance entry if voucher_type == "Journal Entry": referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) @@ -466,10 +480,19 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # amount and account in args # referenced_row is used to deduplicate gain/loss journal entry.update({"referenced_row": referenced_row}) - doc.make_exchange_gain_loss_journal([entry]) + doc.make_exchange_gain_loss_journal([entry], dimensions_dict) else: +<<<<<<< HEAD update_reference_in_payment_entry( entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe +======= + referenced_row = update_reference_in_payment_entry( + entry, + doc, + do_not_save=True, + skip_ref_details_update_for_pe=skip_ref_details_update_for_pe, + dimensions_dict=dimensions_dict, +>>>>>>> c44eb432a5 (refactor: pass dimension values to Gain/Loss journal) ) doc.save(ignore_permissions=True) @@ -618,7 +641,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): def update_reference_in_payment_entry( - d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False + d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None ): reference_details = { "reference_doctype": d.against_voucher_type, @@ -666,8 +689,9 @@ def update_reference_in_payment_entry( if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() + payment_entry.make_exchange_gain_loss_journal( - frappe._dict({"difference_posting_date": d.difference_posting_date}) + frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict ) if not do_not_save: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9da71654227..a7baddb9b0a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1123,7 +1123,9 @@ class AccountsController(TransactionBase): return True return False - def make_exchange_gain_loss_journal(self, args: dict = None) -> None: + def make_exchange_gain_loss_journal( + self, args: dict = None, dimensions_dict: dict = None + ) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ @@ -1178,7 +1180,7 @@ class AccountsController(TransactionBase): self.name, arg.get("referenced_row"), arg.get("cost_center"), - {}, + dimensions_dict, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1259,7 +1261,7 @@ class AccountsController(TransactionBase): self.name, d.idx, self.cost_center, - {}, + dimensions_dict, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( From ec58c309d24c38866481c55310c7748f5b16c281 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 18 Jan 2024 17:08:30 +0530 Subject: [PATCH 26/65] test: dimension inheritance in PE reconciliation (cherry picked from commit 6148fb024b7157d637aa2308e7c856969858468d) --- .../tests/test_accounts_controller.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index a448ad4a572..331599f7877 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1342,7 +1342,7 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) - def test_51_cr_note_should_inherit_dimension_from_payment(self): + def test_51_cr_note_should_inherit_dimension(self): self.setup_dimensions() rate_in_account_currency = 1 @@ -1383,3 +1383,39 @@ class TestAccountsController(FrappeTestCase): [cr_note.department, cr_note.department], frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"), ) + + def test_52_dimension_inhertiance_exc_gain_loss(self): + # Sales Invoice in Foreign Currency + self.setup_dimensions() + rate = 80 + rate_in_account_currency = 1 + dimension = "Research & Development" + + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) + si.department = dimension + si.save().submit() + + pe = self.create_payment_entry(amount=1, source_exc_rate=82).save() + pe.department = dimension + pe = pe.save().submit() + + pr = self.create_payment_reconciliation() + pr.department = dimension + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + journals = self.get_journals_for(si.doctype, si.name) + self.assertEqual( + [dimension, dimension], + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", [x.parent for x in journals])}, + pluck="department", + ), + ) From d4828f3cf58fca98651dd2b4904e80ac867e98f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jan 2024 16:44:20 +0530 Subject: [PATCH 27/65] refactor: pass dimensions on advance allocation (cherry picked from commit cbd443a78afbc7c58055881e534a8aa56ca4bea6) --- erpnext/controllers/accounts_controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a7baddb9b0a..c3de4e1241a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -31,6 +31,7 @@ from frappe.utils import ( import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, + get_dimensions, ) from erpnext.accounts.doctype.pricing_rule.utils import ( apply_pricing_rule_for_free_items, @@ -1353,7 +1354,13 @@ class AccountsController(TransactionBase): if lst: from erpnext.accounts.utils import reconcile_against_document - reconcile_against_document(lst) + # pass dimension values to utility method + active_dimensions = get_dimensions()[0] + for x in lst: + for dim in active_dimensions: + if self.get(dim.fieldname): + x.update({dim.fieldname: self.get(dim.fieldname)}) + reconcile_against_document(lst, active_dimensions=active_dimensions) def on_cancel(self): from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( From 15db7b8ae492d7a9e1adaf035697368bf2b4bff0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jan 2024 16:50:54 +0530 Subject: [PATCH 28/65] test: dimension inheritance on adv allocation (cherry picked from commit fcf4687c523202436234814af3da4c4d84f5eba9) --- .../tests/test_accounts_controller.py | 58 +++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 331599f7877..fad216d5a43 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1389,18 +1389,18 @@ class TestAccountsController(FrappeTestCase): self.setup_dimensions() rate = 80 rate_in_account_currency = 1 - dimension = "Research & Development" + dpt = "Research & Development" si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True) - si.department = dimension + si.department = dpt si.save().submit() pe = self.create_payment_entry(amount=1, source_exc_rate=82).save() - pe.department = dimension + pe.department = dpt pe = pe.save().submit() pr = self.create_payment_reconciliation() - pr.department = dimension + pr.department = dpt pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) @@ -1410,9 +1410,57 @@ class TestAccountsController(FrappeTestCase): pr.reconcile() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 0) + + # Exc Gain/Loss journals should inherit dimension from parent journals = self.get_journals_for(si.doctype, si.name) self.assertEqual( - [dimension, dimension], + [dpt, dpt], + frappe.db.get_all( + "Journal Entry Account", + filters={"parent": ("in", [x.parent for x in journals])}, + pluck="department", + ), + ) + + def test_53_dimension_inheritance_on_advance(self): + self.setup_dimensions() + dpt = "Research & Development" + + adv = self.create_payment_entry(amount=1, source_exc_rate=85) + adv.department = dpt + adv.save().submit() + adv.reload() + + # Sales Invoices in different exchange rates + si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True) + si.department = dpt + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "reference_row": advances[0].reference_row, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, + }, + ) + si = si.save().submit() + + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exc Gain/Loss journals should inherit dimension from parent + journals = self.get_journals_for(si.doctype, si.name) + self.assertEqual( + [dpt, dpt], frappe.db.get_all( "Journal Entry Account", filters={"parent": ("in", [x.parent for x in journals])}, From 51bc225fe550b470fad324c6a656da7300c23969 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 19 Jan 2024 18:00:31 +0530 Subject: [PATCH 29/65] refactor: dynamic dimension filters in pop up (cherry picked from commit f8bbb0619cbbbaace8f54a9f8758c3962ebe4725) --- .../doctype/payment_entry/payment_entry.js | 45 +++++++++---------- .../doctype/payment_entry/payment_entry.py | 8 ++++ .../public/js/utils/dimension_tree_filter.js | 4 ++ 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index b7a4e854ff2..309141fe4c2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -629,23 +629,9 @@ frappe.ui.form.on('Payment Entry', { frm.events.set_unallocated_amount(frm); }, - get_dimensions: function(frm) { - let result = []; - frappe.call({ - method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions", - async: false, - callback: function(r) { - if(!r.exc) { - result = r.message[0].map(elem => elem.document_type); - } - } - }); - return result; - }, - get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) { const today = frappe.datetime.get_today(); - const fields = [ + let fields = [ {fieldtype:"Section Break", label: __("Posting Date")}, {fieldtype:"Date", label: __("From Date"), fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)}, @@ -660,18 +646,29 @@ frappe.ui.form.on('Payment Entry', { fieldname:"outstanding_amt_greater_than", default: 0}, {fieldtype:"Column Break"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, - {fieldtype:"Section Break"}, - {fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center", - "get_query": function() { - return { - "filters": {"company": frm.doc.company} - } + ]; + + if (frm.dimension_filters) { + let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2); + + fields.push({fieldtype:"Section Break"}); + frm.dimension_filters.map((elem, idx)=>{ + fields.push({ + fieldtype: "Link", + label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label, + options: elem.document_type, + fieldname: elem.fieldname || elem.document_type + }); + if(idx+1 == column_break_insertion_point) { + fields.push({fieldtype:"Column Break"}); } - }, - {fieldtype:"Column Break"}, + }); + } + + fields = fields.concat([ {fieldtype:"Section Break"}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, - ]; + ]); let btn_text = ""; diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a3ebf045fc9..acb6c924408 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -13,6 +13,7 @@ from pypika import Case from pypika.functions import Coalesce, Sum import erpnext +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.accounts.doctype.bank_account.bank_account import ( get_bank_account_details, get_party_bank_account, @@ -1453,6 +1454,13 @@ def get_outstanding_reference_documents(args): condition += " and cost_center='%s'" % args.get("cost_center") accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) + # dynamic dimension filters + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + if args.get(dim.fieldname): + condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname)) + accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname)) + date_fields_dict = { "posting_date": ["from_posting_date", "to_posting_date"], "due_date": ["from_due_date", "to_due_date"], diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 3f70c09f667..27d00bacb88 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -25,6 +25,10 @@ erpnext.accounts.dimensions = { }, setup_filters(frm, doctype) { + if (doctype == 'Payment Entry' && this.accounting_dimensions) { + frm.dimension_filters = this.accounting_dimensions + } + if (this.accounting_dimensions) { this.accounting_dimensions.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { From 9a3bde93508ba5765184a61e87860fc537532be9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jan 2024 11:59:20 +0530 Subject: [PATCH 30/65] refactor: update dimensions, only if provided (cherry picked from commit ec0f17ca8bd810e41ae73f5a45f304ba38c63d0a) --- erpnext/accounts/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index c207d9338e6..2c0d370c04c 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -2054,7 +2054,8 @@ def create_gain_loss_journal( dr_or_cr + "_in_account_currency": 0, } ) - journal_account.update(dimensions) + if dimensions: + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_account = frappe._dict( @@ -2070,7 +2071,8 @@ def create_gain_loss_journal( reverse_dr_or_cr: abs(exc_gain_loss), } ) - journal_account.update(dimensions) + if dimensions: + journal_account.update(dimensions) journal_entry.append("accounts", journal_account) journal_entry.save() From e70f0f6d8d1665c99d1d2e69b04de6d538b33b32 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 22 Jan 2024 12:06:07 +0530 Subject: [PATCH 31/65] refactor: handle dynamic dimension in order query (cherry picked from commit 7c2cb70387d7dbb7f976d28919ce21f25a0b6acd) --- 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 acb6c924408..66c82be596c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1688,6 +1688,12 @@ def get_orders_to_be_billed( if doc and hasattr(doc, "cost_center") and doc.cost_center: condition = " and cost_center='%s'" % cost_center + # dynamic dimension filters + active_dimensions = get_dimensions()[0] + for dim in active_dimensions: + if filters.get(dim.fieldname): + condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname)) + if party_account_currency == company_currency: grand_total_field = "base_grand_total" rounded_total_field = "base_rounded_total" From c759406ebbe225fc4f1bb7d7b4050e0386055cb8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:57:23 +0100 Subject: [PATCH 32/65] fix(Batch): reload doc after splitting to show updated qty --- erpnext/stock/doctype/batch/batch.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index 3b07e4e80c1..3b5b7b03cc5 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -128,19 +128,16 @@ frappe.ui.form.on('Batch', { fieldtype: 'Data', }], (data) => { - frappe.call({ - method: 'erpnext.stock.doctype.batch.batch.split_batch', - args: { + frappe.xcall( + 'erpnext.stock.doctype.batch.batch.split_batch', + { item_code: frm.doc.item, batch_no: frm.doc.name, qty: data.qty, warehouse: $btn.attr('data-warehouse'), new_batch_id: data.new_batch_id - }, - callback: (r) => { - frm.refresh(); - }, - }); + } + ).then(() => frm.reload_doc()); }, __('Split Batch'), __('Split') From dd3b77ae288b53736583575cf687e1a7cc95d64d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:02:42 +0100 Subject: [PATCH 33/65] refactor(Batch): use const instead of var --- erpnext/stock/doctype/batch/batch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js index 3b5b7b03cc5..38d3d1cc376 100644 --- a/erpnext/stock/doctype/batch/batch.js +++ b/erpnext/stock/doctype/batch/batch.js @@ -52,7 +52,7 @@ frappe.ui.form.on('Batch', { // sort by qty r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 }); - var rows = $('
').appendTo(section); + const rows = $('
').appendTo(section); // show (r.message || []).forEach(function(d) { @@ -76,7 +76,7 @@ frappe.ui.form.on('Batch', { // move - ask for target warehouse and make stock entry rows.find('.btn-move').on('click', function() { - var $btn = $(this); + const $btn = $(this); const fields = [ { fieldname: 'to_warehouse', @@ -115,7 +115,7 @@ frappe.ui.form.on('Batch', { // split - ask for new qty and batch ID (optional) // and make stock entry via batch.batch_split rows.find('.btn-split').on('click', function() { - var $btn = $(this); + const $btn = $(this); frappe.prompt([{ fieldname: 'qty', label: __('New Batch Qty'), From b310a55727e16ce511173eef1acd6db49f720227 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Jan 2024 10:29:55 +0530 Subject: [PATCH 34/65] fix: not able to edit address through portal (cherry picked from commit b046d980ad83b906d46515082bd32038daadcb3c) --- erpnext/utilities/web_form/addresses/addresses.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/utilities/web_form/addresses/addresses.json b/erpnext/utilities/web_form/addresses/addresses.json index 2f5e180731a..4e2d8e36c2c 100644 --- a/erpnext/utilities/web_form/addresses/addresses.json +++ b/erpnext/utilities/web_form/addresses/addresses.json @@ -8,26 +8,29 @@ "allow_print": 0, "amount": 0.0, "amount_based_on_field": 0, + "anonymous": 0, + "apply_document_permissions": 1, + "condition_json": "[]", "creation": "2016-06-24 15:50:33.196990", "doc_type": "Address", "docstatus": 0, "doctype": "Web Form", "idx": 0, "is_standard": 1, + "list_columns": [], + "list_title": "", "login_required": 1, "max_attachment_size": 0, - "modified": "2019-10-15 06:55:30.405119", - "modified_by": "Administrator", + "modified": "2024-01-24 10:28:35.026064", + "modified_by": "rohitw1991@gmail.com", "module": "Utilities", "name": "addresses", "owner": "Administrator", "published": 1, "route": "address", - "route_to_success_link": 0, "show_attachments": 0, - "show_in_grid": 0, + "show_list": 1, "show_sidebar": 0, - "sidebar_items": [], "success_url": "/addresses", "title": "Address", "web_form_fields": [ From e3fdb6f55cabd34ee97b47b7f50c4aabac59ac06 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Jan 2024 12:15:55 +0530 Subject: [PATCH 35/65] fix: not able to edit / change address from portal --- .../templates/includes/cart/address_card.html | 4 +- .../includes/cart/address_picker_card.html | 2 +- .../templates/includes/cart/cart_address.html | 77 +++++++++++++++++++ erpnext/templates/pages/cart.js | 73 ------------------ 4 files changed, 80 insertions(+), 76 deletions(-) diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html index 830ed649f5f..a4f10cc643e 100644 --- a/erpnext/templates/includes/cart/address_card.html +++ b/erpnext/templates/includes/cart/address_card.html @@ -7,11 +7,11 @@
{{ address.display }}
- + {{ _('Edit') }} - + \ No newline at end of file diff --git a/erpnext/templates/includes/cart/address_picker_card.html b/erpnext/templates/includes/cart/address_picker_card.html index 646210e65f1..dd9adad66f2 100644 --- a/erpnext/templates/includes/cart/address_picker_card.html +++ b/erpnext/templates/includes/cart/address_picker_card.html @@ -7,6 +7,6 @@

{{ address.display }}

- {{ _('Edit') }} + {{ _('Edit') }} diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index cf600173731..525a56af1f0 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -186,3 +186,80 @@ frappe.ready(() => { } }); + + \ No newline at end of file diff --git a/erpnext/templates/pages/cart.js b/erpnext/templates/pages/cart.js index fb2d159dcf9..1cf8a1a28e8 100644 --- a/erpnext/templates/pages/cart.js +++ b/erpnext/templates/pages/cart.js @@ -12,7 +12,6 @@ $.extend(shopping_cart, { }, bind_events: function() { - shopping_cart.bind_address_picker_dialog(); shopping_cart.bind_place_order(); shopping_cart.bind_request_quotation(); shopping_cart.bind_change_qty(); @@ -21,78 +20,6 @@ $.extend(shopping_cart, { shopping_cart.bind_coupon_code(); }, - bind_address_picker_dialog: function() { - const d = this.get_update_address_dialog(); - this.parent.find('.btn-change-address').on('click', (e) => { - const type = $(e.currentTarget).parents('.address-container').attr('data-address-type'); - $(d.get_field('address_picker').wrapper).html( - this.get_address_template(type) - ); - d.show(); - }); - }, - - get_update_address_dialog() { - let d = new frappe.ui.Dialog({ - title: "Select Address", - fields: [{ - 'fieldtype': 'HTML', - 'fieldname': 'address_picker', - }], - primary_action_label: __('Set Address'), - primary_action: () => { - const $card = d.$wrapper.find('.address-card.active'); - const address_type = $card.closest('[data-address-type]').attr('data-address-type'); - const address_name = $card.closest('[data-address-name]').attr('data-address-name'); - frappe.call({ - type: "POST", - method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address", - freeze: true, - args: { - address_type, - address_name - }, - callback: function(r) { - d.hide(); - if (!r.exc) { - $(".cart-tax-items").html(r.message.total); - shopping_cart.parent.find( - `.address-container[data-address-type="${address_type}"]` - ).html(r.message.address); - } - } - }); - } - }); - - return d; - }, - - get_address_template(type) { - return { - shipping: `
-
- {% for address in shipping_addresses %} -
- {% include "templates/includes/cart/address_picker_card.html" %} -
- {% endfor %} -
-
`, - billing: `
-
- {% for address in billing_addresses %} -
- {% include "templates/includes/cart/address_picker_card.html" %} -
- {% endfor %} -
-
`, - }[type]; - }, - bind_place_order: function() { $(".btn-place-order").on("click", function() { shopping_cart.place_order(this); From 780c069268084e6cb1b4bacde8cad98485e925d1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 24 Jan 2024 11:45:15 +0530 Subject: [PATCH 36/65] fix: email list for auto reorder material request (cherry picked from commit 764f3422a0826e22d116616daa3849d02cc1e117) --- .../material_request/test_material_request.py | 56 +++++++++++++++++ erpnext/stock/reorder_item.py | 61 +++++++++++++++---- 2 files changed, 106 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 03f58c664d3..b5817c750e2 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -762,6 +762,62 @@ class TestMaterialRequest(FrappeTestCase): self.assertEqual(mr.per_ordered, 100) self.assertEqual(existing_requested_qty, current_requested_qty) + def test_auto_email_users_with_company_user_permissions(self): + from erpnext.stock.reorder_item import get_email_list + + comapnywise_users = { + "_Test Company": "test_auto_email_@example.com", + "_Test Company 1": "test_auto_email_1@example.com", + } + + permissions = [] + + for company, user in comapnywise_users.items(): + if not frappe.db.exists("User", user): + frappe.get_doc( + { + "doctype": "User", + "email": user, + "first_name": user, + "send_notifications": 0, + "enabled": 1, + "user_type": "System User", + "roles": [{"role": "Purchase Manager"}], + } + ).insert(ignore_permissions=True) + + if not frappe.db.exists( + "User Permission", {"user": user, "allow": "Company", "for_value": company} + ): + perm_doc = frappe.get_doc( + { + "doctype": "User Permission", + "user": user, + "allow": "Company", + "for_value": company, + "apply_to_all_doctypes": 1, + } + ).insert(ignore_permissions=True) + + permissions.append(perm_doc) + + comapnywise_mr_list = frappe._dict({}) + mr1 = make_material_request() + comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name) + + mr2 = make_material_request( + company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1" + ) + comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name) + + for company, mr_list in comapnywise_mr_list.items(): + emails = get_email_list(company) + + self.assertTrue(comapnywise_users[company] in emails) + + for perm in permissions: + perm.delete() + def get_in_transit_warehouse(company): if not frappe.db.exists("Warehouse Type", "Transit"): diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index 3c429a2c22d..e172cecb236 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -145,6 +145,7 @@ def create_material_request(material_requests): mr.log_error("Unable to create material request") + company_wise_mr = frappe._dict({}) for request_type in material_requests: for company in material_requests[request_type]: try: @@ -206,17 +207,19 @@ def create_material_request(material_requests): mr.submit() mr_list.append(mr) + company_wise_mr.setdefault(company, []).append(mr) + except Exception: _log_exception(mr) - if mr_list: + if company_wise_mr: if getattr(frappe.local, "reorder_email_notify", None) is None: frappe.local.reorder_email_notify = cint( frappe.db.get_value("Stock Settings", None, "reorder_email_notify") ) if frappe.local.reorder_email_notify: - send_email_notification(mr_list) + send_email_notification(company_wise_mr) if exceptions_list: notify_errors(exceptions_list) @@ -224,20 +227,56 @@ def create_material_request(material_requests): return mr_list -def send_email_notification(mr_list): +def send_email_notification(company_wise_mr): """Notify user about auto creation of indent""" - email_list = frappe.db.sql_list( - """select distinct r.parent - from `tabHas Role` r, tabUser p - where p.name = r.parent and p.enabled = 1 and p.docstatus < 2 - and r.role in ('Purchase Manager','Stock Manager') - and p.name not in ('Administrator', 'All', 'Guest')""" + for company, mr_list in company_wise_mr.items(): + email_list = get_email_list(company) + + if not email_list: + continue + + msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + + frappe.sendmail( + recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg + ) + + +def get_email_list(company): + users = get_comapny_wise_users(company) + user_table = frappe.qb.DocType("User") + role_table = frappe.qb.DocType("Has Role") + + query = ( + frappe.qb.from_(user_table) + .inner_join(role_table) + .on(user_table.name == role_table.parent) + .select(user_table.email) + .where( + (role_table.role.isin(["Purchase Manager", "Stock Manager"])) + & (user_table.name.notin(["Administrator", "All", "Guest"])) + & (user_table.enabled == 1) + & (user_table.docstatus < 2) + ) ) - msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list}) + if users: + query = query.where(user_table.name.isin(users)) - frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) + emails = query.run(as_dict=True) + + return list(set([email.email for email in emails])) + + +def get_comapny_wise_users(company): + users = frappe.get_all( + "User Permission", + filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1}, + fields=["user"], + ) + + return [user.user for user in users] def notify_errors(exceptions_list): From 2c431f394eea0a496c12dafc28f96377ac4d37cb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 24 Jan 2024 16:01:17 +0530 Subject: [PATCH 37/65] chore: resolve conflicts --- erpnext/accounts/utils.py | 8 ---- erpnext/controllers/accounts_controller.py | 45 ---------------------- 2 files changed, 53 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2c0d370c04c..b3e9996b515 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -482,17 +482,12 @@ def reconcile_against_document( entry.update({"referenced_row": referenced_row}) doc.make_exchange_gain_loss_journal([entry], dimensions_dict) else: -<<<<<<< HEAD - update_reference_in_payment_entry( - entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe -======= referenced_row = update_reference_in_payment_entry( entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe, dimensions_dict=dimensions_dict, ->>>>>>> c44eb432a5 (refactor: pass dimension values to Gain/Loss journal) ) doc.save(ignore_permissions=True) @@ -653,11 +648,8 @@ def update_reference_in_payment_entry( if d.difference_amount is not None else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.difference_amount, -<<<<<<< HEAD -======= "account": d.account, "dimensions": d.dimensions, ->>>>>>> 5dc22e1811 (refactor: pass dimension details to query) } if d.voucher_detail_no: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c3de4e1241a..cd7b7b69529 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,11 +7,6 @@ import json import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied -<<<<<<< HEAD -======= -from frappe.query_builder import Criterion -from frappe.query_builder.custom import ConstantColumn ->>>>>>> ff60ec85b8 (refactor: pass dimension filters to query) from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( add_days, @@ -2558,7 +2553,6 @@ def get_advance_payment_entries( payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" -<<<<<<< HEAD if order_list or against_all_orders: if order_list: reference_condition = " and t2.reference_name in ({0})".format( @@ -2567,45 +2561,6 @@ def get_advance_payment_entries( else: reference_condition = "" order_list = [] -======= - if payment_type == "Receive": - q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) - else: - q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) - - if condition: - # conditions should be built as an array and passed as Criterion - common_filter_conditions = [] - - common_filter_conditions.append(payment_entry.company == condition["company"]) - if condition.get("name", None): - common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%")) - - if condition.get("from_payment_date"): - common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"])) - - if condition.get("to_payment_date"): - common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"])) - - if condition.get("get_payments") == True: - if condition.get("cost_center"): - common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"]) - - if condition.get("accounting_dimensions"): - for field, val in condition.get("accounting_dimensions").items(): - common_filter_conditions.append(payment_entry[field] == val) - - if condition.get("minimum_payment_amount"): - common_filter_conditions.append( - payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"]) - ) - - if condition.get("maximum_payment_amount"): - common_filter_conditions.append( - payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"]) - ) - q = q.where(Criterion.all(common_filter_conditions)) ->>>>>>> ff60ec85b8 (refactor: pass dimension filters to query) payment_name_filter = "" if payment_name: From 295395918c3515f1ce6757a9ababab42a87d4800 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 11 Jan 2024 16:00:48 +0100 Subject: [PATCH 38/65] fix: Payment Terms Status for Sales Order report should show all payment terms from order not only this comming from template (cherry picked from commit 6c8f52b26f0601135ec0dd1be94835951936cd11) --- .../payment_terms_status_for_sales_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 3682c5fd62e..c6e46475387 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -210,7 +210,6 @@ def get_so_with_invoices(filters): .where( (so.docstatus == 1) & (so.status.isin(["To Deliver and Bill", "To Bill"])) - & (so.payment_terms_template != "NULL") & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) ) From 941f8824e5f31f42b0f54182d790e8178c9511ab Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 25 Jan 2024 11:40:44 +0530 Subject: [PATCH 39/65] fix: linter issue --- erpnext/assets/doctype/asset/asset.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b90bf19b429..3fcb9720dd1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -392,7 +392,9 @@ class Asset(AccountsController): if skip_row: continue - schedule_date = add_months(finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)) + schedule_date = add_months( + finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) + ) if not current_fiscal_year_end_date: current_fiscal_year_end_date = get_fiscal_year(finance_book.depreciation_start_date)[2] elif getdate(schedule_date) > getdate(current_fiscal_year_end_date): From 9a5995a3e5f1ff806855f8b88a4dc0b0a3ce73a8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 25 Jan 2024 11:27:14 +0530 Subject: [PATCH 40/65] fix: do not delete batches implicitly --- erpnext/controllers/stock_controller.py | 5 ----- .../purchase_receipt/test_purchase_receipt.py | 14 +++++++++++++- .../stock/doctype/stock_entry/test_stock_entry.py | 2 +- .../test_stock_reconciliation.py | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 403f71be5eb..5768026df2b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -402,11 +402,6 @@ class StockController(AccountsController): d.batch_no = None d.db_set("batch_no", None) - for data in frappe.get_all( - "Batch", {"reference_name": self.name, "reference_doctype": self.doctype} - ): - frappe.delete_doc("Batch", data.name) - def get_sl_entries(self, d, args): sl_dict = frappe._dict( { diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index b444e864e3d..3a81c4a5462 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -193,7 +193,6 @@ class TestPurchaseReceipt(FrappeTestCase): batch_no = pr.items[0].batch_no pr.cancel() - self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name})) self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no})) def test_purchase_receipt_gl_entry(self): @@ -2172,6 +2171,19 @@ class TestPurchaseReceipt(FrappeTestCase): pr_doc.reload() self.assertFalse(pr_doc.items[0].from_warehouse) + def test_do_not_delete_batch_implicitly(self): + item = make_item( + "_Test Item With Delete Batch", + {"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TBWDB.#####"}, + ).name + + pr = make_purchase_receipt(item_code=item, qty=10, rate=100) + batch_no = pr.items[0].batch_no + self.assertTrue(frappe.db.exists("Batch", batch_no)) + + pr.cancel() + self.assertTrue(frappe.db.exists("Batch", batch_no)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c4f26c4baaf..8afe23a1d7a 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -737,7 +737,7 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(batch_in_serial_no, None) self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") - self.assertEqual(frappe.db.exists("Batch", batch_no), None) + self.assertTrue(frappe.db.exists("Batch", batch_no)) def test_serial_batch_item_qty_deduction(self): """ diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 19c04afe909..9499566f341 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -312,7 +312,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): sr.cancel() self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") - self.assertEqual(frappe.db.exists("Batch", batch_no), None) + self.assertTrue(frappe.db.exists("Batch", batch_no)) def test_stock_reco_balance_qty_for_serial_and_batch_item(self): from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry From 1e32c6207e2d344b2ba43f8be42f952340eacba5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:51:48 +0530 Subject: [PATCH 41/65] fix: default enable closing stock balance (backport #39551) (#39553) fix: default enable closing stock balance (#39551) (cherry picked from commit d1fb90edffbbbd48722fddda1ed3d9144783b774) Co-authored-by: rohitwaghchaure --- erpnext/stock/report/stock_balance/stock_balance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 6de5f00ece8..fe6e83eddad 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -99,7 +99,7 @@ frappe.query_reports["Stock Balance"] = { "fieldname": 'ignore_closing_balance', "label": __('Ignore Closing Balance'), "fieldtype": 'Check', - "default": 1 + "default": 0 }, ], From 1dacb794415cdf138be3707dd95cabe5bff3e9f2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 22 Jan 2024 16:58:31 +0530 Subject: [PATCH 42/65] fix: fetch correct quantity and amount for grouped asset (cherry picked from commit 06f48c678be51c85a98bd0c6175fa210a47788f0) --- erpnext/assets/doctype/asset/asset.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f711577abbe..120ca44cd23 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -519,16 +519,16 @@ frappe.ui.form.on('Asset', { indicator: 'red' }); } - var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset'); - var asset_quantity = is_grouped_asset ? item.qty : 1; - var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount')); - - frm.set_value('gross_purchase_amount', purchase_amount); - frm.set_value('purchase_receipt_amount', purchase_amount); - frm.set_value('asset_quantity', asset_quantity); - frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); - if(item.asset_location) { frm.set_value('location', item.asset_location); } + frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => { + var asset_quantity = r.is_grouped_asset ? item.qty : 1; + var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount')); + frm.set_value('gross_purchase_amount', purchase_amount); + frm.set_value('purchase_receipt_amount', purchase_amount); + frm.set_value('asset_quantity', asset_quantity); + frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center); + if(item.asset_location) { frm.set_value('location', item.asset_location); } + }); }, set_depreciation_rate: function(frm, row) { From c26f7bbed0160a533c782d44410001c5fcab9913 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:22:15 +0530 Subject: [PATCH 43/65] fix: incorrect amount in the material request item (backport #39567) (#39568) fix: incorrect amount in the material request item (#39567) fix: incoorect amount in the material request (cherry picked from commit 2bdfdeeb9a5f2fd98cf67fc983a920790e56e1e1) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 5922af25879..e501fbba0e4 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -514,6 +514,13 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten schedule_date() { set_schedule_date(this.frm); } + + qty(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + row.amount = flt(row.qty) * flt(row.rate); + frappe.model.set_value(cdt, cdn, "amount", row.amount); + refresh_field("amount", row.name, row.parentfield); + } }; // for backward compatibility: combine new and previous states From 9fd1692db2c1cf45b34273bdd984c37bab13d403 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 25 Jan 2024 19:08:39 +0530 Subject: [PATCH 44/65] fix: RM valuation rate in SCR (#39541) --- .../controllers/subcontracting_controller.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 772fbd54512..5c069b1e275 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -343,7 +343,7 @@ class SubcontractingController(StockController): i += 1 def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): - doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" + doctype = "BOM Explosion Item" if exploded_item else "BOM Item" fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] alias_dict = { @@ -447,6 +447,16 @@ class SubcontractingController(StockController): rm_obj = self.append(self.raw_material_table, bom_item) rm_obj.reference_name = item_row.name + if self.doctype == self.subcontract_data.order_doctype: + rm_obj.required_qty = qty + rm_obj.amount = rm_obj.required_qty * rm_obj.rate + else: + rm_obj.consumed_qty = 0 + setattr( + rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) + ) + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + if self.doctype == "Subcontracting Receipt": args = frappe._dict( { @@ -465,16 +475,6 @@ class SubcontractingController(StockController): ) rm_obj.rate = get_incoming_rate(args) - if self.doctype == self.subcontract_data.order_doctype: - rm_obj.required_qty = qty - rm_obj.amount = rm_obj.required_qty * rm_obj.rate - else: - rm_obj.consumed_qty = 0 - setattr( - rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) - ) - self.__set_batch_nos(bom_item, item_row, rm_obj, qty) - def __get_qty_based_on_material_transfer(self, item_row, transfer_item): key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) From a072cfbf3fb8a8f704039072316bf433874a32f1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 23 Jan 2024 13:11:06 +0100 Subject: [PATCH 45/65] refactor(Sales Invoice): set account for mode of payment (cherry picked from commit 3815f07c33d9c84230304d53a735d8f580f3288e) --- .../accounts/doctype/pos_invoice/pos_invoice.py | 7 ------- .../doctype/sales_invoice/sales_invoice.py | 17 +++++++---------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 2405aba38a3..383b9dab24a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -13,7 +13,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( SalesInvoice, - get_bank_cash_account, get_mode_of_payment_info, update_multi_mode_option, ) @@ -52,7 +51,6 @@ class POSInvoice(SalesInvoice): self.validate_stock_availablility() self.validate_return_items_qty() self.set_status() - self.set_account_for_mode_of_payment() self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() @@ -584,11 +582,6 @@ class POSInvoice(SalesInvoice): update_multi_mode_option(self, pos_profile) self.paid_amount = 0 - def set_account_for_mode_of_payment(self): - for pay in self.payments: - if not pay.account: - pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") - @frappe.whitelist() def create_payment_request(self): for pay in self.payments: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index fc37e5ec67f..19f52ad1e77 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -245,7 +245,8 @@ class SalesInvoice(SellingController): self.calculate_taxes_and_totals() def before_save(self): - set_account_for_mode_of_payment(self) + self.set_account_for_mode_of_payment() + self.set_paid_amount() def on_submit(self): self.validate_pos_paid_amount() @@ -538,9 +539,6 @@ class SalesInvoice(SellingController): ): data.sales_invoice = sales_invoice - def on_update(self): - self.set_paid_amount() - def on_update_after_submit(self): if hasattr(self, "repost_required"): fields_to_check = [ @@ -571,6 +569,11 @@ class SalesInvoice(SellingController): self.paid_amount = paid_amount self.base_paid_amount = base_paid_amount + def set_account_for_mode_of_payment(self): + for payment in self.payments: + if not payment.account: + payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account") + def validate_time_sheets_are_submitted(self): for data in self.timesheets: if data.time_sheet: @@ -1943,12 +1946,6 @@ def make_sales_return(source_name, target_doc=None): return make_return_doc("Sales Invoice", source_name, target_doc) -def set_account_for_mode_of_payment(self): - for data in self.payments: - if not data.account: - data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") - - def get_inter_company_details(doc, doctype): if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: parties = frappe.db.get_all( From 47c591ccf16565ad20ac88d7ba7a45bd6c990690 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 26 Jan 2024 14:25:05 +0530 Subject: [PATCH 46/65] fix(ecom): do not create a new contact if a contact already exists (#39290) --- erpnext/e_commerce/shopping_cart/cart.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 030b439ae4b..029506b7a1e 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -501,6 +501,7 @@ def get_party(user=None): contact_name = get_contact_name(user) party = None + contact = None if contact_name: contact = frappe.get_doc("Contact", contact_name) if contact.links: @@ -538,11 +539,15 @@ def get_party(user=None): customer.flags.ignore_mandatory = True customer.insert(ignore_permissions=True) - contact = frappe.new_doc("Contact") - contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]}) + if not contact: + contact = frappe.new_doc("Contact") + contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]}) + contact.insert(ignore_permissions=True) + contact.reload() + contact.append("links", dict(link_doctype="Customer", link_name=customer.name)) contact.flags.ignore_mandatory = True - contact.insert(ignore_permissions=True) + contact.save(ignore_permissions=True) return customer From 4626ea79ef2cf1b0b0a13874f1e548bd51ceb0f2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 27 Jan 2024 09:55:26 +0530 Subject: [PATCH 47/65] refactor: do currency conversion on future amount columns (cherry picked from commit 0de4197c886f6dfa500c0b3f1869c13cc3807aaf) --- .../accounts_receivable.py | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 8f7b99aef16..9219cc5e383 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -5,7 +5,7 @@ from collections import OrderedDict import frappe -from frappe import _, qb, scrub +from frappe import _, qb, query_builder, scrub from frappe.query_builder import Criterion from frappe.query_builder.functions import Date, Substring, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate @@ -578,6 +578,8 @@ class ReceivablePayableReport(object): def get_future_payments_from_payment_entry(self): pe = frappe.qb.DocType("Payment Entry") pe_ref = frappe.qb.DocType("Payment Entry Reference") + ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) + return ( frappe.qb.from_(pe) .inner_join(pe_ref) @@ -589,6 +591,11 @@ class ReceivablePayableReport(object): (pe.posting_date).as_("future_date"), (pe_ref.allocated_amount).as_("future_amount"), (pe.reference_no).as_("future_ref"), + ifelse( + pe.payment_type == "Receive", + pe.source_exchange_rate * pe_ref.allocated_amount, + pe.target_exchange_rate * pe_ref.allocated_amount, + ).as_("future_amount_in_base_currency"), ) .where( (pe.docstatus < 2) @@ -625,13 +632,24 @@ class ReceivablePayableReport(object): query = query.select( Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount") ) + query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency")) else: query = query.select( Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount") ) + query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency")) else: query = query.select( - Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount") + Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_( + "future_amount_in_base_currency" + ) + ) + query = query.select( + Sum( + jea.debit_in_account_currency + if self.account_type == "Payable" + else jea.credit_in_account_currency + ).as_("future_amount") ) query = query.having(qb.Field("future_amount") > 0) @@ -647,14 +665,19 @@ class ReceivablePayableReport(object): row.remaining_balance = row.outstanding row.future_amount = 0.0 for future in self.future_payments.get((row.voucher_no, row.party), []): - if row.remaining_balance > 0 and future.future_amount: - if future.future_amount > row.outstanding: + if self.filters.in_party_currency: + future_amount_field = "future_amount" + else: + future_amount_field = "future_amount_in_base_currency" + + if row.remaining_balance > 0 and future.get(future_amount_field): + if future.get(future_amount_field) > row.outstanding: row.future_amount = row.outstanding - future.future_amount = future.future_amount - row.outstanding + future[future_amount_field] = future.get(future_amount_field) - row.outstanding row.remaining_balance = 0 else: - row.future_amount += future.future_amount - future.future_amount = 0 + row.future_amount += future.get(future_amount_field) + future[future_amount_field] = 0 row.remaining_balance = row.outstanding - row.future_amount row.setdefault("future_ref", []).append( From 12ac371b2267832e4bd686c0ffbbcac0a04be24d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 27 Jan 2024 11:14:29 +0530 Subject: [PATCH 48/65] test: future payment with foreign currency (cherry picked from commit 7b37389115cd10aac154c292e7b34fcefd4d7ba2) --- .../test_accounts_receivable.py | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 976935b99f6..6ff81be0ab7 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -772,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): # post sorting output should be [[Additional Debtors, ...], [Debtors, ...]] report_output = sorted(report_output, key=lambda x: x[0]) self.assertEqual(expected_data, report_output) + + def test_future_payments_on_foreign_currency(self): + self.customer2 = ( + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "Jane Doe", + "type": "Individual", + "default_currency": "USD", + } + ) + .insert() + .submit() + ) + + si = self.create_sales_invoice(do_not_submit=True) + si.posting_date = add_days(today(), -1) + si.customer = self.customer2 + si.currency = "USD" + si.conversion_rate = 80 + si.debit_to = self.debtors_usd + si.save().submit() + + # full payment in USD + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.base_received_amount = 7500 + pe.received_amount = 7500 + pe.source_exchange_rate = 75 + pe.save().submit() + + filters = frappe._dict( + { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_future_payments": True, + "in_party_currency": False, + } + ) + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [8000.0, 8000.0, 500.0, 7500.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + filters.in_party_currency = True + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 0.0, 100.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # partial payment in USD on a future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.base_received_amount = 6750 + pe.received_amount = 6750 + pe.source_exchange_rate = 75 + pe.paid_amount = 90 # in USD + pe.references[0].allocated_amount = 90 + pe.save().submit() + + filters.in_party_currency = False + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [8000.0, 8000.0, 1250.0, 6750.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + filters.in_party_currency = True + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 10.0, 90.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) From bf61030dab65e8fff4d6752e320257dacc555f75 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:25:04 +0530 Subject: [PATCH 49/65] fix: prevent extra transfer against inter transfer transaction (backport #39213) (#39595) * fix: prevent extra transfer against inter transfer transaction (#39213) * fix: prevent extra transfer against inter transfer transaction * fix: internal transfer dashboard (cherry picked from commit 8fdc244e16c26a74944a3a67613f8b64009a69b0) # Conflicts: # erpnext/controllers/stock_controller.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 115 +++++++++++++++++- .../delivery_note/delivery_note_dashboard.py | 6 +- .../purchase_receipt/test_purchase_receipt.py | 3 +- 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 5768026df2b..f6ccb824aea 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,7 +6,7 @@ from collections import defaultdict from typing import List, Tuple import frappe -from frappe import _ +from frappe import _, bold from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext @@ -668,6 +668,9 @@ class StockController(AccountsController): self.validate_in_transit_warehouses() self.validate_multi_currency() self.validate_packed_items() + + if self.get("is_internal_supplier"): + self.validate_internal_transfer_qty() else: self.validate_internal_transfer_warehouse() @@ -706,6 +709,116 @@ class StockController(AccountsController): if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"): frappe.throw(_("Packed Items cannot be transferred internally")) + def validate_internal_transfer_qty(self): + if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: + return + + item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty() + if not item_wise_transfer_qty: + return + + item_wise_received_qty = self.get_item_wise_inter_received_qty() + precision = frappe.get_precision(self.doctype + " Item", "qty") + + over_receipt_allowance = frappe.db.get_single_value( + "Stock Settings", "over_delivery_receipt_allowance" + ) + + parent_doctype = { + "Purchase Receipt": "Delivery Note", + "Purchase Invoice": "Sales Invoice", + }.get(self.doctype) + + for key, transferred_qty in item_wise_transfer_qty.items(): + recevied_qty = flt(item_wise_received_qty.get(key), precision) + if over_receipt_allowance: + transferred_qty = transferred_qty + flt( + transferred_qty * over_receipt_allowance / 100, precision + ) + + if recevied_qty > flt(transferred_qty, precision): + frappe.throw( + _("For Item {0} cannot be received more than {1} qty against the {2} {3}").format( + bold(key[1]), + bold(flt(transferred_qty, precision)), + bold(parent_doctype), + get_link_to_form(parent_doctype, self.get("inter_company_reference")), + ) + ) + + def get_item_wise_inter_transfer_qty(self): + reference_field = "inter_company_reference" + if self.doctype == "Purchase Invoice": + reference_field = "inter_company_invoice_reference" + + parent_doctype = { + "Purchase Receipt": "Delivery Note", + "Purchase Invoice": "Sales Invoice", + }.get(self.doctype) + + child_doctype = parent_doctype + " Item" + + parent_tab = frappe.qb.DocType(parent_doctype) + child_tab = frappe.qb.DocType(child_doctype) + + query = ( + frappe.qb.from_(parent_doctype) + .inner_join(child_tab) + .on(child_tab.parent == parent_tab.name) + .select( + child_tab.name, + child_tab.item_code, + child_tab.qty, + ) + .where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1)) + ) + + data = query.run(as_dict=True) + item_wise_transfer_qty = defaultdict(float) + for row in data: + item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty) + + return item_wise_transfer_qty + + def get_item_wise_inter_received_qty(self): + child_doctype = self.doctype + " Item" + + parent_tab = frappe.qb.DocType(self.doctype) + child_tab = frappe.qb.DocType(child_doctype) + + query = ( + frappe.qb.from_(self.doctype) + .inner_join(child_tab) + .on(child_tab.parent == parent_tab.name) + .select( + child_tab.item_code, + child_tab.qty, + ) + .where(parent_tab.docstatus < 2) + ) + + if self.doctype == "Purchase Invoice": + query = query.select( + child_tab.sales_invoice_item.as_("name"), + ) + + query = query.where( + parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference + ) + else: + query = query.select( + child_tab.delivery_note_item.as_("name"), + ) + + query = query.where(parent_tab.inter_company_reference == self.inter_company_reference) + + data = query.run(as_dict=True) + item_wise_transfer_qty = defaultdict(float) + for row in data: + item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty) + + return item_wise_transfer_qty + def validate_putaway_capacity(self): # if over receipt is attempted while 'apply putaway rule' is disabled # and if rule was applied on the transaction, validate it. diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index d4a574da73f..2440701af98 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -8,6 +8,7 @@ def get_data(): "Stock Entry": "delivery_note_no", "Quality Inspection": "reference_name", "Auto Repeat": "reference_document", + "Purchase Receipt": "inter_company_reference", }, "internal_links": { "Sales Order": ["items", "against_sales_order"], @@ -22,6 +23,9 @@ def get_data(): {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, {"label": _("Returns"), "items": ["Stock Entry"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, - {"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]}, + { + "label": _("Internal Transfer"), + "items": ["Material Request", "Purchase Order", "Purchase Receipt"], + }, ], } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 3a81c4a5462..0fc04c5f83e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1625,7 +1625,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr.items[0].rejected_warehouse = from_warehouse pr.save() - self.assertRaises(OverAllowanceError, pr.submit) + self.assertRaises(frappe.ValidationError, pr.submit) # Step 5: Test Over Receipt Allowance frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50) @@ -1639,6 +1639,7 @@ class TestPurchaseReceipt(FrappeTestCase): to_warehouse=target_warehouse, ) + pr.reload() pr.submit() frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) From 66be3c551f4666651b32d78a4fb15cdeed75b189 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 25 Jan 2024 16:13:24 +0530 Subject: [PATCH 50/65] fix: enqueue JV submission when more than 100 accounts (cherry picked from commit 53b44ccf2907ee15ca4dd6291400272e7cc2acf3) --- .../doctype/journal_entry/journal_entry.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 82a9c0f1524..d6400c52b95 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -78,6 +78,20 @@ class JournalEntry(AccountsController): if not self.title: self.title = self.get_title() + def submit(self): + if len(self.accounts) > 100: + msgprint(_("The task has been enqueued as a background job."), alert=True) + self.queue_action("submit", timeout=4600) + else: + self._submit() + + def cancel(self): + if len(self.accounts) > 100: + msgprint(_("The task has been enqueued as a background job."), alert=True) + self.queue_action("cancel", timeout=4600) + else: + self._cancel() + def on_submit(self): self.validate_cheque_info() self.check_credit_limit() From 04728792f588cf84cdf53082b92cf0eff4a19f37 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 26 Jan 2024 20:03:21 +0530 Subject: [PATCH 51/65] fix: return doc obj after submit (cherry picked from commit fc677811b7d896879d9c2537b0527635a8ff3855) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index d6400c52b95..6e3019bb8f0 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -83,14 +83,14 @@ class JournalEntry(AccountsController): msgprint(_("The task has been enqueued as a background job."), alert=True) self.queue_action("submit", timeout=4600) else: - self._submit() + return self._submit() def cancel(self): if len(self.accounts) > 100: msgprint(_("The task has been enqueued as a background job."), alert=True) self.queue_action("cancel", timeout=4600) else: - self._cancel() + return self._cancel() def on_submit(self): self.validate_cheque_info() From 2389b41f51d61bd3b79b6b2014be20865737e130 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 13:36:10 +0530 Subject: [PATCH 52/65] fix amount not updated when change rate in material request (backport #39606) (#39614) fix amount not updated when change rate in material request (#39606) * fix amount not updated when change rate in material request * make code consistent (cherry picked from commit efade9b9aee13c4702c69841cdb6766f305d5b30) Co-authored-by: Jeffry Suryadharma <41689493+jeffrysurya@users.noreply.github.com> --- erpnext/stock/doctype/material_request/material_request.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index e501fbba0e4..675a3e978c0 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -429,6 +429,9 @@ frappe.ui.form.on("Material Request Item", { rate: function(frm, doctype, name) { const item = locals[doctype][name]; + item.amount = flt(item.qty) * flt(item.rate); + frappe.model.set_value(doctype, name, "amount", item.amount); + refresh_field("amount", item.name, item.parentfield); frm.events.get_item_data(frm, item, false); }, From ea779fcad9b7dd64b9c39ec38644632fb66a6138 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Jan 2024 14:44:16 +0530 Subject: [PATCH 53/65] refactor: build payment entry query separately --- .../payment_reconciliation.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 79897e0803a..3246327864d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -112,7 +112,7 @@ class PaymentReconciliation(Document): def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" - condition = self.get_conditions(get_payments=True) + condition = self.get_payment_entry_conditions() # pass dynamic dimension filter values to query builder dimensions = {} @@ -652,6 +652,29 @@ class PaymentReconciliation(Document): self.build_dimensions_filter_conditions() + def get_payment_entry_conditions(self): + condition = " and company = '{0}' ".format(self.company) + + if self.get("cost_center"): + condition = " and cost_center = '{0}' ".format(self.cost_center) + + condition += ( + " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) + if self.from_payment_date + else "" + ) + condition += ( + " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) + if self.to_payment_date + else "" + ) + + if self.minimum_payment_amount: + condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) + if self.maximum_payment_amount: + condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) + return condition + def get_journal_filter_conditions(self): conditions = [] je = qb.DocType("Journal Entry") From 1e341f0ff62bfcef778d4f792da6c611ac041372 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 29 Jan 2024 13:15:00 +0530 Subject: [PATCH 54/65] fix: do not auto-populate item delivery date (cherry picked from commit 49cb11c1f357bd73afb0b150a502a418d5560067) --- erpnext/selling/doctype/quotation/quotation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d3832172545..de2cef90929 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -309,7 +309,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0) target.qty = balance_qty if balance_qty > 0 else 0 target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) - target.delivery_date = nowdate() if obj.against_blanket_order: target.against_blanket_order = obj.against_blanket_order From c50988b1bc8b6dbfbec80763e4ecb8a5bb45c38d Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 29 Jan 2024 14:19:25 +0530 Subject: [PATCH 55/65] fix: qtn tests using delivery date (cherry picked from commit 079cd30b9c80739f6beec910b838b62f077e236b) --- erpnext/selling/doctype/quotation/test_quotation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 691c5b03d2d..4dba030c20e 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -87,7 +87,6 @@ class TestQuotation(FrappeTestCase): self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name) self.assertEqual(sales_order.customer, "_Test Customer") - sales_order.delivery_date = "2014-01-01" sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() sales_order.insert() @@ -120,7 +119,6 @@ class TestQuotation(FrappeTestCase): self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name) self.assertEqual(sales_order.customer, "_Test Customer") - sales_order.delivery_date = "2014-01-01" sales_order.naming_series = "_T-Quotation-" sales_order.transaction_date = nowdate() sales_order.insert() From ff0daedd52bf60e17093f5ed5adbb3e5b35acf94 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Jan 2024 16:34:42 +0530 Subject: [PATCH 56/65] refactor: convert sql to query builder on Payments query --- .../payment_reconciliation.py | 43 +++--- erpnext/controllers/accounts_controller.py | 125 +++++++++--------- 2 files changed, 84 insertions(+), 84 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 3246327864d..435837f4460 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -114,14 +114,6 @@ class PaymentReconciliation(Document): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = self.get_payment_entry_conditions() - # pass dynamic dimension filter values to query builder - dimensions = {} - for x in self.dimensions: - dimension = x.fieldname - if self.get(dimension): - dimensions.update({dimension: self.get(dimension)}) - condition.update({"accounting_dimensions": dimensions}) - payment_entries = get_advance_payment_entries_for_regional( self.party_type, self.party, @@ -653,27 +645,32 @@ class PaymentReconciliation(Document): self.build_dimensions_filter_conditions() def get_payment_entry_conditions(self): - condition = " and company = '{0}' ".format(self.company) + conditions = [] + pe = qb.DocType("Payment Entry") + conditions.append(pe.company == self.company) if self.get("cost_center"): - condition = " and cost_center = '{0}' ".format(self.cost_center) + conditions.append(pe.cost_center == self.cost_center) - condition += ( - " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) - if self.from_payment_date - else "" - ) - condition += ( - " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) - if self.to_payment_date - else "" - ) + if self.from_payment_date: + conditions.append(pe.posting_date.gte(self.from_payment_date)) + + if self.to_payment_date: + conditions.append(pe.posting_date.lte(self.to_payment_date)) if self.minimum_payment_amount: - condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) + conditions.append(pe.unallocated_amount.gte(flt(self.minimum_payment_amount))) + if self.maximum_payment_amount: - condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) - return condition + conditions.append(pe.unallocated_amount.lte(flt(self.maximum_payment_amount))) + + # pass dynamic dimension filter values to payment query + for x in self.dimensions: + dimension = x.fieldname + if self.get(dimension): + conditions.append(pe[dimension] == self.get(dimension)) + + return conditions def get_journal_filter_conditions(self): conditions = [] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index cd7b7b69529..bb87a776a1d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,8 @@ import json import frappe from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.query_builder import Criterion +from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( add_days, @@ -2541,6 +2543,9 @@ def get_advance_payment_entries( condition=None, payment_name=None, ): + pe = qb.DocType("Payment Entry") + per = qb.DocType("Payment Entry Reference") + party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = ( "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" @@ -2551,76 +2556,74 @@ def get_advance_payment_entries( ) payment_entries_against_order, unallocated_payment_entries = [], [] - limit_cond = "limit %s" % limit if limit else "" + + if payment_name: + condition.append(pe.name.like(f"%%{payment_name}%%")) if order_list or against_all_orders: if order_list: - reference_condition = " and t2.reference_name in ({0})".format( - ", ".join(["%s"] * len(order_list)) + condition.append(per.refernce_name.isin(order_list)) + payment_entries_query = ( + qb.from_(pe) + .inner_join(per) + .on(pe.name == per.parent) + .select( + ConstantColumn("Payment Entry").as_("reference_type"), + pe.name, + pe.remarks, + per.allocated_amount.as_("amount"), + per.name.as_("reference_row"), + per.reference_name.as_("against_order"), + pe.posting_date, + pe[currency_field].as_("currency"), + pe[exchange_rate_field].as_("exchange_rate"), ) - else: - reference_condition = "" - order_list = [] - - payment_name_filter = "" - if payment_name: - payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name) - - if not condition: - condition = "" - - payment_entries_against_order = frappe.db.sql( - """ - select - 'Payment Entry' as reference_type, t1.name as reference_name, - t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, - t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency, t1.{5} as exchange_rate - from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 - where - t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s - and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 - and t2.reference_doctype = %s {2} {3} {6} - order by t1.posting_date {4} - """.format( - currency_field, - party_account_field, - reference_condition, - condition, - limit_cond, - exchange_rate_field, - payment_name_filter, - ), - [party_account, payment_type, party_type, party, order_doctype] + order_list, - as_dict=1, + .where( + (pe[party_account_field] == party_account) + & (pe.payment_type == payment_type) + & (pe.party_type == party_type) + & (pe.party == party) + & (pe.docstatus == 1) + & (per.reference_doctype == order_doctype) + ) + .where(Criterion.all(condition)) + .orderby(pe.posting_date) ) + if limit: + payment_entries_query = payment_entries_query.limit(limit) + + payment_entries_against_order = payment_entries_query.run(as_dict=1) + if include_unallocated: - payment_name_filter = "" - if payment_name: - payment_name_filter = " and name like '%%{0}%%'".format(payment_name) - - unallocated_payment_entries = frappe.db.sql( - """ - select 'Payment Entry' as reference_type, name as reference_name, posting_date, - remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency - from `tabPayment Entry` - where - {0} = %s and party_type = %s and party = %s and payment_type = %s - and docstatus = 1 and unallocated_amount > 0 {condition} {4} - order by posting_date {1} - """.format( - party_account_field, - limit_cond, - exchange_rate_field, - currency_field, - payment_name_filter, - condition=condition or "", - ), - (party_account, party_type, party, payment_type), - as_dict=1, + unallocated_payment_query = ( + qb.from_(pe) + .select( + ConstantColumn("Payment Entry").as_("reference_type"), + pe.name.as_("reference_name"), + pe.posting_date, + pe.remarks, + pe.unallocated_amount.as_("amount"), + pe[exchange_rate_field].as_("exchange_rate"), + pe[currency_field].as_("currency"), + ) + .where( + (pe[party_account_field] == party_account) + & (pe.party_type == party_type) + & (pe.party == party) + & (pe.payment_type == payment_type) + & (pe.docstatus == 1) + & (pe.unallocated_amount.gt(0)) + ) + .where(Criterion.all(condition)) + .orderby(pe.posting_date) ) + if limit: + unallocated_payment_query = unallocated_payment_query.limit(limit) + + unallocated_payment_entries = unallocated_payment_query.run(as_dict=1) + return list(payment_entries_against_order) + list(unallocated_payment_entries) From c054316127e60d233d0b54fcbdbb260c1e99da48 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Jan 2024 17:41:56 +0530 Subject: [PATCH 57/65] chore: fix typo and initialize a list --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bb87a776a1d..e4a079c5882 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2557,12 +2557,15 @@ def get_advance_payment_entries( payment_entries_against_order, unallocated_payment_entries = [], [] + if not condition: + condition = [] + if payment_name: condition.append(pe.name.like(f"%%{payment_name}%%")) if order_list or against_all_orders: if order_list: - condition.append(per.refernce_name.isin(order_list)) + condition.append(per.reference_name.isin(order_list)) payment_entries_query = ( qb.from_(pe) .inner_join(per) From 1966ea15ba830699804aff2721aa62c260676aa0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Jan 2024 21:05:21 +0530 Subject: [PATCH 58/65] refactor: pass orders name in a separate criterion --- erpnext/controllers/accounts_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e4a079c5882..ac197259679 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2564,15 +2564,16 @@ def get_advance_payment_entries( condition.append(pe.name.like(f"%%{payment_name}%%")) if order_list or against_all_orders: + orders_condition = [] if order_list: - condition.append(per.reference_name.isin(order_list)) + orders_condition.append(per.reference_name.isin(order_list)) payment_entries_query = ( qb.from_(pe) .inner_join(per) .on(pe.name == per.parent) .select( ConstantColumn("Payment Entry").as_("reference_type"), - pe.name, + pe.name.as_("reference_name"), pe.remarks, per.allocated_amount.as_("amount"), per.name.as_("reference_row"), @@ -2590,6 +2591,7 @@ def get_advance_payment_entries( & (per.reference_doctype == order_doctype) ) .where(Criterion.all(condition)) + .where(Criterion.all(orders_condition)) .orderby(pe.posting_date) ) From d0c810accdece6507a290a12b2fe0ac1e4fa042f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 30 Jan 2024 05:43:17 +0530 Subject: [PATCH 59/65] refactor(test): disable dimensions post test --- .../tests/test_accounts_controller.py | 61 +++++++++++++++---- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index fad216d5a43..5e3077ea8c8 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1258,18 +1258,53 @@ class TestAccountsController(FrappeTestCase): frappe.db.set_value("Company", self.company, "cost_center", cc) def setup_dimensions(self): - # create dimension - from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( - create_dimension, - ) + if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): + frappe.get_doc( + { + "doctype": "Accounting Dimension", + "document_type": "Department", + } + ).insert() + else: + dimension = frappe.get_doc("Accounting Dimension", "Department") + dimension.disabled = 0 + dimension.save() - create_dimension() - # make it non-mandatory - loc = frappe.get_doc("Accounting Dimension", "Location") - for x in loc.dimension_defaults: - x.mandatory_for_bs = False - x.mandatory_for_pl = False - loc.save() + if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): + dimension1 = frappe.get_doc( + { + "doctype": "Accounting Dimension", + "document_type": "Location", + } + ) + dimension1.append( + "dimension_defaults", + { + "company": "_Test Company", + "reference_document": "Location", + "default_dimension": "Block 1", + "mandatory_for_bs": 0, + "mandatory_for_pl": 0, + }, + ) + + dimension1.insert() + dimension1.save() + else: + dimension1 = frappe.get_doc("Accounting Dimension", "Location") + dimension1.disabled = 0 + dimension1.save() + + def disable_dimensions(self): + if frappe.db.exists("Accounting Dimension", {"document_type": "Department"}): + dimension = frappe.get_doc("Accounting Dimension", "Department") + dimension.disabled = 1 + dimension.save() + + if frappe.db.exists("Accounting Dimension", {"document_type": "Location"}): + dimension1 = frappe.get_doc("Accounting Dimension", "Location") + dimension1.disabled = 1 + dimension1.save() def test_50_dimensions_filter(self): """ @@ -1341,6 +1376,7 @@ class TestAccountsController(FrappeTestCase): pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 1) + self.disable_dimensions() def test_51_cr_note_should_inherit_dimension(self): self.setup_dimensions() @@ -1383,6 +1419,7 @@ class TestAccountsController(FrappeTestCase): [cr_note.department, cr_note.department], frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"), ) + self.disable_dimensions() def test_52_dimension_inhertiance_exc_gain_loss(self): # Sales Invoice in Foreign Currency @@ -1421,6 +1458,7 @@ class TestAccountsController(FrappeTestCase): pluck="department", ), ) + self.disable_dimensions() def test_53_dimension_inheritance_on_advance(self): self.setup_dimensions() @@ -1467,3 +1505,4 @@ class TestAccountsController(FrappeTestCase): pluck="department", ), ) + self.disable_dimensions() From c5ce4db315f955f7c0c95c51667f98c27139463f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 25 Jan 2024 14:05:42 +0530 Subject: [PATCH 60/65] refactor: prevent '{debit/credit}_to' account mismatch (cherry picked from commit 6f2fae1b6133286ab882e421e69f6a87ecb97d4a) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ac197259679..f1385c721d8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -187,6 +187,7 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() self.validate_party_account_currency() + self.validate_return_against_account() if self.doctype in ["Purchase Invoice", "Sales Invoice"]: if invalid_advances := [ @@ -320,6 +321,32 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) +<<<<<<< HEAD +======= + def remove_serial_and_batch_bundle(self): + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)}, + ) + + for bundle in bundles: + frappe.delete_doc("Serial and Batch Bundle", bundle.name) + + def validate_return_against_account(self): + if ( + self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against + ): + cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to" + cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To" + cr_dr_account = self.get(cr_dr_account_field) + if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account: + frappe.throw( + _("'{0}' account: '{1}' should match the Return Against Invoice").format( + frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account) + ) + ) + +>>>>>>> 6f2fae1b61 (refactor: prevent '{debit/credit}_to' account mismatch) def validate_deferred_income_expense_account(self): field_map = { "Sales Invoice": "deferred_revenue_account", From faeca79c689987a0a666d5f2bf5e56a2a6a5661a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 25 Jan 2024 14:21:18 +0530 Subject: [PATCH 61/65] test: account mismatch validation (cherry picked from commit 8bdc76073367d4a912524f16bf098d94340f50c5) --- .../doctype/sales_invoice/test_sales_invoice.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 45a01ab04aa..abf0fc5c0f3 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1531,6 +1531,19 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000) self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500) + def test_return_invoice_with_account_mismatch(self): + debtors2 = create_account( + parent_account="Accounts Receivable - _TC", + account_name="Debtors 2", + company="_Test Company", + account_type="Receivable", + ) + si = create_sales_invoice(qty=1, rate=1000) + cr_note = create_sales_invoice( + qty=-1, rate=1000, is_return=1, return_against=si.name, debit_to=debtors2, do_not_save=True + ) + self.assertRaises(frappe.ValidationError, cr_note.save) + def test_gle_made_when_asset_is_returned(self): create_asset_data() asset = create_asset(item_code="Macbook Pro") From 9212a74913d653c857f6d9b692fe83ea96a148ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 25 Jan 2024 14:29:07 +0530 Subject: [PATCH 62/65] test: debit note account mismatch (cherry picked from commit bdca718103a98eba64e69715eaf628dfd9377d5e) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py --- .../purchase_invoice/test_purchase_invoice.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index dc2b37291e8..f17c60bcd09 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1893,6 +1893,55 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") +<<<<<<< HEAD +======= + def test_debit_note_with_account_mismatch(self): + new_creditors = create_account( + parent_account="Accounts Payable - _TC", + account_name="Creditors 2", + company="_Test Company", + account_type="Payable", + ) + pi = make_purchase_invoice(qty=1, rate=1000) + dr_note = make_purchase_invoice( + qty=-1, rate=1000, is_return=1, return_against=pi.name, do_not_save=True + ) + dr_note.credit_to = new_creditors + + self.assertRaises(frappe.ValidationError, dr_note.save) + + def test_debit_note_without_item(self): + pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True) + pi.items[0].item_code = "" + pi.save() + + self.assertFalse(pi.items[0].item_code) + pi.submit() + + return_pi = make_purchase_invoice( + item_name="_Test Item", + is_return=1, + return_against=pi.name, + qty=-10, + do_not_save=True, + ) + return_pi.items[0].item_code = "" + return_pi.save() + return_pi.submit() + self.assertEqual(return_pi.docstatus, 1) + + +def set_advance_flag(company, flag, default_account): + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": flag, + "default_advance_paid_account": default_account, + }, + ) + +>>>>>>> bdca718103 (test: debit note account mismatch) def check_gl_entries( doc, From 0884c5ed83048a71db3947fd5d3df119b7a7d408 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 30 Jan 2024 11:03:25 +0530 Subject: [PATCH 63/65] chore: resolve conflicts --- .../purchase_invoice/test_purchase_invoice.py | 34 ------------------- erpnext/controllers/accounts_controller.py | 12 ------- 2 files changed, 46 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f17c60bcd09..6942e28726f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1893,8 +1893,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC") -<<<<<<< HEAD -======= def test_debit_note_with_account_mismatch(self): new_creditors = create_account( parent_account="Accounts Payable - _TC", @@ -1910,38 +1908,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertRaises(frappe.ValidationError, dr_note.save) - def test_debit_note_without_item(self): - pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True) - pi.items[0].item_code = "" - pi.save() - - self.assertFalse(pi.items[0].item_code) - pi.submit() - - return_pi = make_purchase_invoice( - item_name="_Test Item", - is_return=1, - return_against=pi.name, - qty=-10, - do_not_save=True, - ) - return_pi.items[0].item_code = "" - return_pi.save() - return_pi.submit() - self.assertEqual(return_pi.docstatus, 1) - - -def set_advance_flag(company, flag, default_account): - frappe.db.set_value( - "Company", - company, - { - "book_advance_payments_in_separate_party_account": flag, - "default_advance_paid_account": default_account, - }, - ) - ->>>>>>> bdca718103 (test: debit note account mismatch) def check_gl_entries( doc, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f1385c721d8..a4597da8e27 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -321,17 +321,6 @@ class AccountsController(TransactionBase): (self.doctype, self.name), ) -<<<<<<< HEAD -======= - def remove_serial_and_batch_bundle(self): - bundles = frappe.get_all( - "Serial and Batch Bundle", - filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)}, - ) - - for bundle in bundles: - frappe.delete_doc("Serial and Batch Bundle", bundle.name) - def validate_return_against_account(self): if ( self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against @@ -346,7 +335,6 @@ class AccountsController(TransactionBase): ) ) ->>>>>>> 6f2fae1b61 (refactor: prevent '{debit/credit}_to' account mismatch) def validate_deferred_income_expense_account(self): field_map = { "Sales Invoice": "deferred_revenue_account", From f6725e43425043eaba7dcdd3cf3768a857a39ee6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 30 Jan 2024 11:25:20 +0530 Subject: [PATCH 64/65] fix: do not consider rejected warehouses in pick list (#39539) * fix: do not picked rejected materials * test: test case for pick list without rejected materials --- .../doctype/sales_order/test_sales_order.py | 78 ++++++++++++++++ .../stock/doctype/pick_list/pick_list.json | 10 ++- erpnext/stock/doctype/pick_list/pick_list.py | 88 +++++++++++++++++-- .../stock/doctype/warehouse/warehouse.json | 10 ++- 4 files changed, 175 insertions(+), 11 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 3d4c035fdca..2de0168812d 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.sales_order import ( WarehouseRequired, + create_pick_list, make_delivery_note, make_material_request, make_raw_material_request, @@ -2082,6 +2083,83 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def test_pick_list_without_rejected_materials(self): + serial_and_batch_item = make_item( + "_Test Serial and Batch Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TSBIFRM-.#####", + "serial_no_series": "SN-TSBIFRM-.#####", + }, + ).name + + serial_item = make_item( + "_Test Serial Item for Rejected Materials", + properties={ + "has_serial_no": 1, + "serial_no_series": "SN-TSIFRM-.#####", + }, + ).name + + batch_item = make_item( + "_Test Batch Item for Rejected Materials", + properties={ + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TBIFRM-.#####", + }, + ).name + + normal_item = make_item("_Test Normal Item for Rejected Materials").name + + warehouse = "_Test Warehouse - _TC" + rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC" + + if not frappe.db.exists("Warehouse", rejected_warehouse): + frappe.get_doc( + { + "doctype": "Warehouse", + "warehouse_name": rejected_warehouse, + "company": "_Test Company", + "warehouse_group": "_Test Warehouse Group", + "is_rejected_warehouse": 1, + } + ).insert() + + se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse}) + + se.save() + se.submit() + + se = make_stock_entry( + item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True + ) + for item in [serial_and_batch_item, serial_item, batch_item]: + se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse}) + + se.save() + se.submit() + + so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True) + + for item in [serial_and_batch_item, serial_item, batch_item]: + so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse}) + + so.save() + so.submit() + + pick_list = create_pick_list(so.name) + + pick_list.save() + for row in pick_list.locations: + self.assertEqual(row.qty, 1.0) + self.assertFalse(row.warehouse == rejected_warehouse) + self.assertTrue(row.warehouse == warehouse) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index 7259dc00a81..948011cce69 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -16,6 +16,7 @@ "for_qty", "column_break_4", "parent_warehouse", + "consider_rejected_warehouses", "get_item_locations", "section_break_6", "scan_barcode", @@ -184,11 +185,18 @@ "report_hide": 1, "reqd": 1, "search_index": 1 + }, + { + "default": "0", + "description": "Enable it if users want to consider rejected materials to dispatch.", + "fieldname": "consider_rejected_warehouses", + "fieldtype": "Check", + "label": "Consider Rejected Warehouses" } ], "is_submittable": 1, "links": [], - "modified": "2023-01-24 10:33:43.244476", + "modified": "2024-01-24 17:05:20.317180", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 3c5384d4a88..36c877ebc88 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -205,6 +205,7 @@ class PickList(Document): self.item_count_map.get(item_code), self.company, picked_item_details=picked_items_details.get(item_code), + consider_rejected_warehouses=self.consider_rejected_warehouses, ), ) @@ -524,6 +525,7 @@ def get_available_item_locations( company, ignore_validation=False, picked_item_details=None, + consider_rejected_warehouses=False, ): locations = [] total_picked_qty = ( @@ -534,19 +536,39 @@ def get_available_item_locations( if has_batch_no and has_serial_no: locations = get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty, + consider_rejected_warehouses=consider_rejected_warehouses, ) total_qty_available = sum(location.get("qty") for location in locations) @@ -597,7 +619,12 @@ def get_available_item_locations( def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): sn = frappe.qb.DocType("Serial No") query = ( @@ -613,6 +640,10 @@ def get_available_item_locations_for_serialized_item( else: query = query.where(Coalesce(sn.warehouse, "") != "") + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(sn.warehouse.notin(rejected_warehouses)) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() @@ -627,7 +658,12 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): sle = frappe.qb.DocType("Stock Ledger Entry") batch = frappe.qb.DocType("Batch") @@ -653,15 +689,28 @@ def get_available_item_locations_for_batched_item( if from_warehouses: query = query.where(sle.warehouse.isin(from_warehouses)) + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(sle.warehouse.notin(rejected_warehouses)) + return query.run(as_dict=True) def get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, + from_warehouses, + required_qty, + company, + consider_rejected_warehouses=consider_rejected_warehouses, ) if locations: @@ -691,7 +740,12 @@ def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company, total_picked_qty=0 + item_code, + from_warehouses, + required_qty, + company, + total_picked_qty=0, + consider_rejected_warehouses=False, ): bin = frappe.qb.DocType("Bin") query = ( @@ -708,6 +762,10 @@ def get_available_item_locations_for_other_item( wh = frappe.qb.DocType("Warehouse") query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) + if not consider_rejected_warehouses: + if rejected_warehouses := get_rejected_warehouses(): + query = query.where(bin.warehouse.notin(rejected_warehouses)) + item_locations = query.run(as_dict=True) return item_locations @@ -1028,3 +1086,15 @@ def update_common_item_properties(item, location): item.serial_no = location.serial_no item.batch_no = location.batch_no item.material_request_item = location.material_request_item + + +def get_rejected_warehouses(): + if not hasattr(frappe.local, "rejected_warehouses"): + frappe.local.rejected_warehouses = [] + + if not frappe.local.rejected_warehouses: + frappe.local.rejected_warehouses = frappe.get_all( + "Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name" + ) + + return frappe.local.rejected_warehouses diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 43b2ad2a69b..7b0cade3ca4 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -13,6 +13,7 @@ "column_break_3", "is_group", "parent_warehouse", + "is_rejected_warehouse", "column_break_4", "account", "company", @@ -249,13 +250,20 @@ { "fieldname": "column_break_qajx", "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If yes, then this warehouse will be used to store rejected materials", + "fieldname": "is_rejected_warehouse", + "fieldtype": "Check", + "label": "Is Rejected Warehouse" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-05-29 13:10:43.333160", + "modified": "2024-01-24 16:27:28.299520", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", From 3ee05551150fc5dd28f02e548ad2e505bfc2a29f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:44:03 +0530 Subject: [PATCH 65/65] fix: perf issue while submitting stock entry (backport #39634) (#39641) fix: perf issue while submitting stock entry (#39634) (cherry picked from commit b14886b227730813c83b746f440616508f678392) Co-authored-by: rohitwaghchaure --- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 6 +++++- erpnext/stock/doctype/stock_entry/stock_entry.py | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 0fc04c5f83e..d8141f93e68 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1594,9 +1594,10 @@ class TestPurchaseReceipt(FrappeTestCase): make_stock_entry( purpose="Material Receipt", item_code=item.name, - qty=15, + qty=20, company=company, to_warehouse=from_warehouse, + posting_date=add_days(today(), -3), ) # Step 3: Create Delivery Note with Internal Customer @@ -1619,6 +1620,8 @@ class TestPurchaseReceipt(FrappeTestCase): from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt pr = make_inter_company_purchase_receipt(dn.name) + pr.set_posting_time = 1 + pr.posting_date = today() pr.items[0].qty = 15 pr.items[0].from_warehouse = target_warehouse pr.items[0].warehouse = to_warehouse @@ -1637,6 +1640,7 @@ class TestPurchaseReceipt(FrappeTestCase): company=company, from_warehouse=from_warehouse, to_warehouse=target_warehouse, + posting_date=add_days(pr.posting_date, -1), ) pr.reload() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 61f99ee715a..ca31a9a3d31 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -159,7 +159,6 @@ class StockEntry(StockController): set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() - self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity()