From f7ba7361ca6ebfc0876334e3ecdd54da564cf352 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 18 Jan 2024 14:33:10 +0530 Subject: [PATCH 001/138] fix: set unallocated amount after base tax (cherry picked from commit e9bc63aacf20dd83cb7e15494bb8f2f025491991) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6b3f46d3833a..6d9d69201051 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -919,7 +919,7 @@ frappe.ui.form.on('Payment Entry', { if(frm.doc.payment_type == "Receive" && frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) { - unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges) + unallocated_amount = (frm.doc.base_received_amount + total_deductions - flt(frm.doc.base_total_taxes_and_charges) - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; } else if (frm.doc.payment_type == "Pay" && frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions From 6895b74ecc952f059e4eb05e67863bafd7139ce0 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 18 Jan 2024 15:05:19 +0530 Subject: [PATCH 002/138] fix: linting issue (cherry picked from commit 99b94af49ffd34bccb76b73c2f06187540444a44) --- .../gross_and_net_profit_report/gross_and_net_profit_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index f0ca405401d3..5ccd4f0f16fe 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -134,7 +134,7 @@ def get_revenue(data, period_list, include_in_gross=1): def remove_parent_with_no_child(data): data_to_be_removed = False - for parent in data: + for parent in list(data): if "is_group" in parent and parent.get("is_group") == 1: have_child = False for child in data: From b840eb90ebf558d91471c8bd1c629bc15214b94c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 18 Jan 2024 19:06:41 +0530 Subject: [PATCH 003/138] 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 e5e022802d20..b90bf19b4292 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -10,6 +10,7 @@ from frappe.utils import ( add_days, add_months, + add_years, cint, date_diff, flt, @@ -23,6 +24,7 @@ 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 @@ def _make_depreciation_schedule( 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 @@ def _make_depreciation_schedule( depreciation_amount = get_depreciation_amount( self, value_after_depreciation, + yearly_opening_wdv, finance_book, n, prev_depreciation_amount, @@ -494,7 +505,10 @@ def _make_depreciation_schedule( 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 2b8666d21b6c..e0292098e7ab 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 004/138] 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 6e946f746606..82147f4f6ab0 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 21afd5df8519..708172b29803 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -845,7 +845,7 @@ def test_pro_rata_depreciation_entry_for_wdv(self): ["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 @@ def test_get_depreciation_amount(self): }, ) - 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 837fff4533cb6bdf49b2debded62f98aafa6ad2c Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:25:03 +0530 Subject: [PATCH 005/138] Revert "fix(minor): financial statements period end date" (cherry picked from commit 73625a2622f29a83c98179c0b5048a29e61ce243) --- erpnext/accounts/report/financial_statements.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7355c4b8a165..096bb107069a 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -8,17 +8,7 @@ import frappe from frappe import _ -from frappe.utils import ( - add_days, - add_months, - cint, - cstr, - flt, - formatdate, - get_first_day, - getdate, - today, -) +from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -53,8 +43,6 @@ def get_period_list( year_start_date = getdate(period_start_date) year_end_date = getdate(period_end_date) - year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date - months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_list = [] From 3bdff18467a575e4378fd625eebf6a2a666031cb Mon Sep 17 00:00:00 2001 From: David Arnold Date: Mon, 8 Jan 2024 22:07:46 +0100 Subject: [PATCH 006/138] fix: use most reliable section reference per report line (cherry picked from commit b5be17c6dfc9ab10e9eb57669f697fbb5576489c) --- .../tds_payable_monthly.py | 108 ++++++++++-------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index e4953bb1815f..b96bfbeb8178 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -46,12 +46,10 @@ def get_result( out = [] for name, details in gle_map.items(): - tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 - bill_no, bill_date = "", "" - tax_withholding_category = tax_category_map.get(name) - rate = tax_rate_map.get(tax_withholding_category) - for entry in details: + tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 + tax_withholding_category, rate = None, None + bill_no, bill_date = "", "" party = entry.party or entry.against posting_date = entry.posting_date voucher_type = entry.voucher_type @@ -61,12 +59,19 @@ def get_result( if party_list: party = party_list[0] - if not tax_withholding_category: - tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) - - if entry.account in tds_accounts: + if entry.account in tds_accounts.keys(): tax_amount += entry.credit - entry.debit + # infer tax withholding category from the account if it's the single account for this category + tax_withholding_category = tds_accounts.get(entry.account) + rate = tax_rate_map.get(tax_withholding_category) + # or else the consolidated value from the voucher document + if not tax_withholding_category: + # or else from the party default + tax_withholding_category = tax_category_map.get(name) + rate = tax_rate_map.get(tax_withholding_category) + if not tax_withholding_category: + tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") + rate = tax_rate_map.get(tax_withholding_category) if net_total_map.get(name): if voucher_type == "Journal Entry" and tax_amount and rate: @@ -80,41 +85,41 @@ def get_result( else: total_amount += entry.credit - if tax_amount: - if party_map.get(party, {}).get("party_type") == "Supplier": - party_name = "supplier_name" - party_type = "supplier_type" - else: - party_name = "customer_name" - party_type = "customer_type" - - row = { - "pan" - if frappe.db.has_column(filters.party_type, "pan") - else "tax_id": party_map.get(party, {}).get("pan"), - "party": party_map.get(party, {}).get("name"), - } - - if filters.naming_series == "Naming Series": - row.update({"party_name": party_map.get(party, {}).get(party_name)}) - - row.update( - { - "section_code": tax_withholding_category or "", - "entity_type": party_map.get(party, {}).get(party_type), - "rate": rate, - "total_amount": total_amount, - "grand_total": grand_total, - "base_total": base_total, - "tax_amount": tax_amount, - "transaction_date": posting_date, - "transaction_type": voucher_type, - "ref_no": name, - "supplier_invoice_no": bill_no, - "supplier_invoice_date": bill_date, + if tax_amount: + if party_map.get(party, {}).get("party_type") == "Supplier": + party_name = "supplier_name" + party_type = "supplier_type" + else: + party_name = "customer_name" + party_type = "customer_type" + + row = { + "pan" + if frappe.db.has_column(filters.party_type, "pan") + else "tax_id": party_map.get(party, {}).get("pan"), + "party": party_map.get(party, {}).get("name"), } - ) - out.append(row) + + if filters.naming_series == "Naming Series": + row.update({"party_name": party_map.get(party, {}).get(party_name)}) + + row.update( + { + "section_code": tax_withholding_category or "", + "entity_type": party_map.get(party, {}).get(party_type), + "rate": rate, + "total_amount": total_amount, + "grand_total": grand_total, + "base_total": base_total, + "tax_amount": tax_amount, + "transaction_date": posting_date, + "transaction_type": voucher_type, + "ref_no": name, + "supplier_invoice_no": bill_no, + "supplier_invoice_date": bill_date, + } + ) + out.append(row) out.sort(key=lambda x: x["section_code"]) @@ -282,11 +287,20 @@ def get_tds_docs(filters): journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") - tds_accounts = frappe.get_all( - "Tax Withholding Account", {"company": filters.get("company")}, pluck="account" + _tds_accounts = frappe.get_all( + "Tax Withholding Account", + {"company": filters.get("company")}, + ["account", "parent"], ) + tds_accounts = {} + for tds_acc in _tds_accounts: + # if it turns out not to be the only tax withholding category, then don't include in the map + if tds_accounts.get(tds_acc["account"]): + tds_accounts[tds_acc["account"]] = None + else: + tds_accounts[tds_acc["account"]] = tds_acc["parent"] - tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True) + tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True) for d in tds_docs: if d.voucher_type == "Purchase Invoice": From a19b41d8c85a0f364d7f3704851b79bd9b88da5a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 19 Jan 2024 17:08:02 +0530 Subject: [PATCH 007/138] fix: party field in pdf html (cherry picked from commit b2d9380596dd8ca134777148337ef039f10cc2ca) --- .../accounts_receivable/accounts_receivable.html | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index ed3b9915591f..7d8d33c46b43 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -10,10 +10,8 @@

{%= __(report.report_name) %}

- {% if (filters.customer_name) { %} - {%= filters.customer_name %} - {% } else { %} - {%= filters.customer || filters.supplier %} + {% if (filters.party) { %} + {%= __(filters.party) %} {% } %}

@@ -141,7 +139,7 @@
{%= __("Reference") %} {% } %} {% if(!filters.show_future_payments) { %} - {%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %} + {%= (filters.party) ? __("Remarks"): __("Party") %} {% } %} {%= __("Invoiced Amount") %} {% if(!filters.show_future_payments) { %} @@ -158,7 +156,7 @@
{%= __("Remaining Balance") %} {% } %} {% } else { %} - {%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %} + {%= (filters.party) ? __("Remarks"): __("Party") %} {%= __("Total Invoiced Amount") %} {%= __("Total Paid Amount") %} {%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %} @@ -187,7 +185,7 @@
{% if(!filters.show_future_payments) { %} - {% if(!(filters.customer || filters.supplier)) { %} + {% if(!(filters.party)) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
{%= data[i]["customer_name"] %} @@ -260,7 +258,7 @@
{% if(data[i]["party"]|| " ") { %} {% if(!data[i]["is_total_row"]) { %} - {% if(!(filters.customer || filters.supplier)) { %} + {% if(!(filters.party)) { %} {%= data[i]["party"] %} {% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
{%= data[i]["customer_name"] %} From 55c9cc3f26409faf80298a6e69305db145d6bd01 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Jan 2024 16:43:54 +0530 Subject: [PATCH 008/138] fix: UOM needs to be whole number not being checked in quotations (cherry picked from commit aaf83da3e9a28244c0f57f01a86b98257bdf503b) --- erpnext/selling/doctype/quotation/quotation.py | 3 ++- .../selling/doctype/quotation/test_quotation.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 539960a508f4..d3832172545f 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -24,7 +24,8 @@ def set_indicator(self): def validate(self): super(Quotation, self).validate() self.set_status() - self.validate_uom_is_integer("stock_uom", "qty") + self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_uom_is_integer("uom", "qty") self.validate_valid_till() self.validate_shopping_cart_items() self.set_customer_name() diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 5623a12cdda7..691c5b03d2d6 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -590,6 +590,22 @@ def test_alternative_items_sales_order_mapping_with_stock_items(self): quotation.reload() self.assertEqual(quotation.status, "Ordered") + def test_uom_validation(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = "_Test Item FOR UOM Validation" + make_item(item, {"is_stock_item": 1}) + + if not frappe.db.exists("UOM", "lbs"): + frappe.get_doc({"doctype": "UOM", "uom_name": "lbs", "must_be_whole_number": 1}).insert() + else: + frappe.db.set_value("UOM", "lbs", "must_be_whole_number", 1) + + quotation = make_quotation(item_code=item, qty=1, rate=100, do_not_submit=1) + quotation.items[0].uom = "lbs" + quotation.items[0].conversion_factor = 2.23 + self.assertRaises(frappe.ValidationError, quotation.save) + test_records = frappe.get_test_records("Quotation") From 0a6af795c48439cf10fc3b08b7ab531f38b586d8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 4 May 2023 16:27:43 +0530 Subject: [PATCH 009/138] 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 2af11159298d..25e1e59f094c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -671,9 +671,7 @@ def make_supplier_gl_entry(self, gl_entries): "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 66d9022d08f6..a6cf9646b4df 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1069,9 +1069,7 @@ def make_customer_gl_entry(self, gl_entries): "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 010/138] 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 2b98baf2210e..442df539f74b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -228,15 +228,12 @@ def get_dr_or_cr_notes(self): 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 011/138] 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 d050299912db..45a01ab04aa6 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 @@ def test_return_sales_invoice(self): 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 012/138] 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 fc6dbba7e7f2..ce9579ed613e 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 @@ def test_cr_note_against_invoice(self): 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 @@ def test_cr_note_against_invoice(self): .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 @@ def test_cr_note_against_invoice(self): "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 013/138] 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 a7f1f08cbc26..d035149ff93d 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 @@ def test_payment_against_negative_sales_invoice(self): 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 014/138] 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 25e1e59f094c..802ac8080a9f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1538,12 +1538,8 @@ def set_status(self, update=False, status=None, update_modified=True): 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 a6cf9646b4df..fc37e5ec67f7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1696,12 +1696,8 @@ def set_status(self, update=False, status=None, update_modified=True): 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 015/138] 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 0add6c57da61..665fc6edcc94 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 1e8428d9e299..2ac7370f4ef8 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 26ca27a431dcd34e568b28c2407ad2599f59179d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 23 Jan 2024 11:54:25 +0000 Subject: [PATCH 016/138] chore(release): Bumped to Version 14.61.1 ## [14.61.1](https://github.com/frappe/erpnext/compare/v14.61.0...v14.61.1) (2024-01-23) ### Bug Fixes * linting issue ([6895b74](https://github.com/frappe/erpnext/commit/6895b74ecc952f059e4eb05e67863bafd7139ce0)) * party field in pdf html ([a19b41d](https://github.com/frappe/erpnext/commit/a19b41d8c85a0f364d7f3704851b79bd9b88da5a)) * set unallocated amount after base tax ([f7ba736](https://github.com/frappe/erpnext/commit/f7ba7361ca6ebfc0876334e3ecdd54da564cf352)) * UOM needs to be whole number not being checked in quotations ([55c9cc3](https://github.com/frappe/erpnext/commit/55c9cc3f26409faf80298a6e69305db145d6bd01)) * use most reliable section reference per report line ([3bdff18](https://github.com/frappe/erpnext/commit/3bdff18467a575e4378fd625eebf6a2a666031cb)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index e640a99fdf8b..c4d42e2b11c5 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.61.0" +__version__ = "14.61.1" def get_default_company(user=None): From 5c9ad21a3f993ee8127947ac9cde5da8c8611ab6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 30 Nov 2023 13:11:11 +0530 Subject: [PATCH 017/138] 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 491c67818dff..cbfd9b2d8b98 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 018/138] 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 481b740d775a..5a4e8539bf6b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -548,6 +548,8 @@ "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 019/138] 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 e8985de4e1ee..a14a74a1f47e 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 020/138] 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 a14a74a1f47e..948bae3dfe6e 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 cbfd9b2d8b98..3f85b2135005 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 021/138] 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 2b98baf2210e..b8096843d6a8 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.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 @@ def validate_allocation(self): 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 @@ def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=Fal 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 022/138] 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 b8096843d6a8..f0e381b822d2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -113,6 +113,15 @@ def get_payment_entries(self): 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 cc8ee9226a49..d5bc69fe5db9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,11 @@ 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 023/138] 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 5cdedb73c092..16d1839679c7 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 f0e381b822d2..45620bae59fd 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 024/138] 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 45620bae59fd..b7be45a7ebda 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -29,6 +29,7 @@ def __init__(self, *args, **kwargs): 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 @@ def get_payment_entries(self): # 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 @@ def reconcile(self): 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 @@ def get_payment_details(self, row, dr_or_cr): } ) + 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 @@ def validate_allocation(self): 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 d9fb75a86d2e..74284d9c1d96 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 d5bc69fe5db9..9da716542272 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1178,6 +1178,7 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: 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 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: 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 025/138] 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 b7be45a7ebda..5fcb35e78469 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -136,67 +136,68 @@ def get_payment_entries(self): 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}%%")) - limit = f"limit {self.payment_limit}" if self.payment_limit else " " - - # 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_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) ) + if self.payment_limit: + journal_query = journal_query.limit(self.payment_limit) + + journal_entries = journal_query.run(as_dict=True) + return list(journal_entries) def get_return_invoices(self): @@ -642,37 +643,25 @@ def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=Fal 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)) - ) + conditions.append(je.total_debit.gte(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.lte(self.maximumb_payment_amount)) - return condition + 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 026/138] 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 6d9d69201051..b7a4e854ff23 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 027/138] 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 5fcb35e78469..30250caddb25 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -147,7 +147,7 @@ def get_jv_entries(self): 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 @@ def get_journal_filter_conditions(self): 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 74284d9c1d96..9af81168e595 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 028/138] 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 30250caddb25..f8408b4b13d7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -402,8 +402,15 @@ def allocate_entries(self, args): 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 @@ def get_allocated_entry(self, pay, inv, allocated_amount): } ) + 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 @@ def reconcile_allocations(self, skip_ref_details_update_for_pe=False): 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 @@ def get_payment_details(self, row, dr_or_cr): } ) - 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 @@ def get_journal_filter_conditions(self): 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 9af81168e595..07ba01fe5296 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 029/138] 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 f8408b4b13d7..d400dcdceb99 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -253,6 +253,7 @@ def get_dr_or_cr_notes(self): 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 030/138] 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 97d3c5c32de3..3a3e6def48af 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 @@ def test_42_cost_center_from_cr_note(self): ) 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 031/138] 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 3a3e6def48af..a448ad4a572f 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1273,7 +1273,7 @@ def setup_dimensions(self): 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 @@ def test_50_dimensions_filter(self): 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 032/138] 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 d400dcdceb99..79897e0803a1 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -452,7 +452,7 @@ def reconcile_allocations(self, skip_ref_details_update_for_pe=False): 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 07ba01fe5296..c207d9338e6d 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 9da716542272..a7baddb9b0af 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1123,7 +1123,9 @@ def gain_loss_journal_already_booked( 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 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: 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 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: 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 033/138] 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 a448ad4a572f..331599f78773 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1342,7 +1342,7 @@ def test_50_dimensions_filter(self): 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 @@ def test_51_cr_note_should_inherit_dimension_from_payment(self): [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 034/138] 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 a7baddb9b0af..c3de4e1241ae 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -31,6 +31,7 @@ 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 @@ def update_against_document_in_jv(self): 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 035/138] 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 331599f78773..fad216d5a43e 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1389,18 +1389,18 @@ def test_52_dimension_inhertiance_exc_gain_loss(self): 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 @@ def test_52_dimension_inhertiance_exc_gain_loss(self): 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( + [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( - [dimension, dimension], + [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 036/138] 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 b7a4e854ff23..309141fe4c27 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 a3ebf045fc9e..acb6c9244083 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.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 3f70c09f6676..27d00bacb882 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 037/138] 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 c207d9338e6d..2c0d370c04cd 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 038/138] 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 acb6c9244083..66c82be596ca 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 039/138] 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 3b07e4e80c15..3b5b7b03cc58 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 040/138] 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 3b5b7b03cc58..38d3d1cc3764 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 041/138] 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 2f5e180731af..4e2d8e36c2cb 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 042/138] 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 830ed649f5f4..a4f10cc643ee 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 646210e65f1a..dd9adad66f24 100644 --- a/erpnext/templates/includes/cart/address_picker_card.html +++ b/erpnext/templates/includes/cart/address_picker_card.html @@ -7,6 +7,6 @@
{{ address.title }}

{{ address.display }}

- {{ _('Edit') }} + {{ _('Edit') }} diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index cf6001737315..525a56af1f02 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -186,3 +186,80 @@ } }); + + \ No newline at end of file diff --git a/erpnext/templates/pages/cart.js b/erpnext/templates/pages/cart.js index fb2d159dcf93..1cf8a1a28e84 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 043/138] 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 03f58c664d33..b5817c750e28 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 @@ def test_customer_provided_parts_mr(self): 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 3c429a2c22db..e172cecb236f 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -145,6 +145,7 @@ def _log_exception(mr): 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 _log_exception(mr): 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 _log_exception(mr): 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)) + + 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"], + ) - frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg) + 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 044/138] 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 2c0d370c04cd..b3e9996b515c 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 c3de4e1241ae..cd7b7b69529d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,11 +7,6 @@ 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 045/138] 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 3682c5fd62e1..c6e464753877 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 046/138] 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 b90bf19b4292..3fcb9720dd11 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -392,7 +392,9 @@ def _make_depreciation_schedule( 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 047/138] 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 403f71be5ebe..5768026df2b2 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -402,11 +402,6 @@ def delete_auto_created_batches(self): 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 b444e864e3d9..3a81c4a5462f 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 @@ def test_batched_serial_no_purchase(self): 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 @@ def non_internal_transfer_purchase_receipt(self): 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 c4f26c4baaf5..8afe23a1d7ae 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 @@ def test_serial_batch_item_stock_entry(self): 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 19c04afe9096..9499566f341f 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 @@ def test_stock_reco_for_serial_and_batch_item(self): 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 048/138] 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 6de5f00ece84..fe6e83eddade 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 049/138] fix: fetch correct quantity and amount for grouped asset (cherry picked from commit 06f48c678be51c85a98bd0c6175fa210a47788f0) --- erpnext/assets/doctype/asset/asset.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f711577abbe4..120ca44cd233 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 050/138] 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 5922af258792..e501fbba0e4b 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 051/138] 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 772fbd54512e..5c069b1e2755 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -343,7 +343,7 @@ def __remove_changed_rows(self): 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 @@ def __add_supplied_item(self, item_row, bom_item, qty): 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 @@ def __add_supplied_item(self, item_row, bom_item, qty): ) 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 052/138] 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 2405aba38a33..383b9dab24a2 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.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 @@ def validate(self): 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 @@ def reset_mode_of_payments(self): 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 fc37e5ec67f7..19f52ad1e77e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -245,7 +245,8 @@ def set_tax_withholding(self): 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 @@ def update_time_sheet_detail(self, timesheet, args, sales_invoice): ): 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 @@ def set_paid_amount(self): 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 053/138] 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 030b439ae4b8..029506b7a1e2 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 e729972987ddcb2e78edad2ae729b765fda12266 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 27 Jan 2024 10:12:37 +0530 Subject: [PATCH 054/138] fix: incorrect amount in the material request item (backport #39567) (backport #39568) (#39586) 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 (cherry picked from commit c26f7bbed0160a533c782d44410001c5fcab9913) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- 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 5922af258792..e501fbba0e4b 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 33b21a54f7cf6990a965a91736b58c2bef99e65f Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Sat, 27 Jan 2024 04:43:52 +0000 Subject: [PATCH 055/138] chore(release): Bumped to Version 14.61.2 ## [14.61.2](https://github.com/frappe/erpnext/compare/v14.61.1...v14.61.2) (2024-01-27) ### Bug Fixes * incorrect amount in the material request item (backport [#39567](https://github.com/frappe/erpnext/issues/39567)) (backport [#39568](https://github.com/frappe/erpnext/issues/39568)) ([#39586](https://github.com/frappe/erpnext/issues/39586)) ([e729972](https://github.com/frappe/erpnext/commit/e729972987ddcb2e78edad2ae729b765fda12266)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index c4d42e2b11c5..15f872b033df 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.61.1" +__version__ = "14.61.2" def get_default_company(user=None): From 4626ea79ef2cf1b0b0a13874f1e548bd51ceb0f2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 27 Jan 2024 09:55:26 +0530 Subject: [PATCH 056/138] 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 8f7b99aef16a..9219cc5e3839 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 @@ def get_future_payments(self): 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 @@ def get_future_payments_from_payment_entry(self): (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 @@ def get_future_payments_from_journal_entry(self): 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 @@ def allocate_future_payments(self, row): 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 057/138] 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 976935b99f6b..6ff81be0ab73 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 @@ def test_report_output_if_party_is_missing(self): # 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 058/138] 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 5768026df2b2..f6ccb824aea2 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -6,7 +6,7 @@ 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 @@ def validate_internal_transfer(self): 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 @@ def validate_packed_items(self): 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 d4a574da73f2..2440701af985 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 3a81c4a5462f..0fc04c5f83e9 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 @@ def test_validate_received_qty_for_internal_pr(self): 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 @@ def test_validate_received_qty_for_internal_pr(self): 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 059/138] 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 82a9c0f15246..d6400c52b950 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -78,6 +78,20 @@ def validate(self): 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 060/138] 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 d6400c52b950..6e3019bb8f07 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -83,14 +83,14 @@ def submit(self): 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 061/138] 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 e501fbba0e4b..675a3e978c01 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 062/138] 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 79897e0803a1..3246327864da 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -112,7 +112,7 @@ def get_nonreconciled_payment_entries(self): 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 @@ def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=Fal 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 063/138] 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 d3832172545f..de2cef909299 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -309,7 +309,6 @@ def update_item(obj, target, source_parent): 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 064/138] 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 691c5b03d2d6..4dba030c20e1 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -87,7 +87,6 @@ def test_make_sales_order(self): 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 @@ def test_make_sales_order_with_terms(self): 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 065/138] 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 3246327864da..435837f44606 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -114,14 +114,6 @@ def get_payment_entries(self): 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 @@ def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=Fal 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 cd7b7b69529d..bb87a776a1d8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,8 @@ 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 066/138] 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 bb87a776a1d8..e4a079c58825 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 d794502681d29f628cc51789f32565ab749313e3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:57:40 +0530 Subject: [PATCH 067/138] fix amount not updated when change rate in material request (backport #39606) (backport #39614) (#39621) 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> (cherry picked from commit 2389b41f51d61bd3b79b6b2014be20865737e130) Co-authored-by: mergify[bot] <37929162+mergify[bot]@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 e501fbba0e4b..675a3e978c01 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 1966ea15ba830699804aff2721aa62c260676aa0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 29 Jan 2024 21:05:21 +0530 Subject: [PATCH 068/138] 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 e4a079c58825..ac197259679c 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 069/138] 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 fad216d5a43e..5e3077ea8c86 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1258,18 +1258,53 @@ def test_42_cost_center_from_cr_note(self): 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() + + 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() - 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 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 @@ def test_50_dimensions_filter(self): 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 @@ def test_51_cr_note_should_inherit_dimension(self): [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 @@ def test_52_dimension_inhertiance_exc_gain_loss(self): pluck="department", ), ) + self.disable_dimensions() def test_53_dimension_inheritance_on_advance(self): self.setup_dimensions() @@ -1467,3 +1505,4 @@ def test_53_dimension_inheritance_on_advance(self): 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 070/138] 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 ac197259679c..f1385c721d8a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -187,6 +187,7 @@ def validate(self): 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 @@ def on_trash(self): (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 071/138] 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 45a01ab04aa6..abf0fc5c0f35 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 @@ def test_return_sales_invoice(self): 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 072/138] 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 dc2b37291e85..f17c60bcd096 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 @@ def test_default_cost_center_for_purchase(self): 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 073/138] 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 f17c60bcd096..6942e28726ff 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 @@ def test_default_cost_center_for_purchase(self): 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 @@ def test_debit_note_with_account_mismatch(self): 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 f1385c721d8a..a4597da8e277 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -321,17 +321,6 @@ def on_trash(self): (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 @@ def validate_return_against_account(self): ) ) ->>>>>>> 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 074/138] 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 3d4c035fdca6..2de0168812d3 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.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 @@ def test_expired_rate_for_packed_item(self): 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 7259dc00a81b..948011cce692 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 3c5384d4a885..36c877ebc884 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -205,6 +205,7 @@ def set_item_locations(self, save=False): 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 43b2ad2a69b0..7b0cade3ca46 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 075/138] 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 0fc04c5f83e9..d8141f93e683 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 @@ def test_validate_received_qty_for_internal_pr(self): 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 @@ def test_validate_received_qty_for_internal_pr(self): 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 @@ def test_validate_received_qty_for_internal_pr(self): 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 61f99ee715a6..ca31a9a3d31c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -159,7 +159,6 @@ def validate(self): set_batch_nos(self, "s_warehouse") self.validate_serialized_batch() - self.set_actual_qty() self.calculate_rate_and_amount() self.validate_putaway_capacity() From b7d8bfc58c4e7fd7ab7e32814730493fcaa91781 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 30 Jan 2024 14:14:00 +0000 Subject: [PATCH 076/138] chore(release): Bumped to Version 14.61.3 ## [14.61.3](https://github.com/frappe/erpnext/compare/v14.61.2...v14.61.3) (2024-01-30) ### Bug Fixes * Asset Depreciation WDV as per Income Tax Act ([b840eb9](https://github.com/frappe/erpnext/commit/b840eb90ebf558d91471c8bd1c629bc15214b94c)) * asset module test cases ([f604798](https://github.com/frappe/erpnext/commit/f604798a45fce39cd5e41c6a0aca8d7240848cdb)) * **Batch:** reload doc after splitting ([c759406](https://github.com/frappe/erpnext/commit/c759406ebbe225fc4f1bb7d7b4050e0386055cb8)) * default enable closing stock balance (backport [#39551](https://github.com/frappe/erpnext/issues/39551)) ([#39553](https://github.com/frappe/erpnext/issues/39553)) ([1e32c62](https://github.com/frappe/erpnext/commit/1e32c6207e2d344b2ba43f8be42f952340eacba5)) * do not auto-populate item delivery date ([1e341f0](https://github.com/frappe/erpnext/commit/1e341f0ff62bfcef778d4f792da6c611ac041372)) * do not consider rejected warehouses in pick list ([#39539](https://github.com/frappe/erpnext/issues/39539)) ([f6725e4](https://github.com/frappe/erpnext/commit/f6725e43425043eaba7dcdd3cf3768a857a39ee6)) * do not delete batches implicitly ([9a5995a](https://github.com/frappe/erpnext/commit/9a5995a3e5f1ff806855f8b88a4dc0b0a3ce73a8)) * **ecom:** do not create a new contact if a contact already exists ([#39290](https://github.com/frappe/erpnext/issues/39290)) ([47c591c](https://github.com/frappe/erpnext/commit/47c591ccf16565ad20ac88d7ba7a45bd6c990690)) * email list for auto reorder material request ([780c069](https://github.com/frappe/erpnext/commit/780c069268084e6cb1b4bacde8cad98485e925d1)) * enqueue JV submission when more than 100 accounts ([66be3c5](https://github.com/frappe/erpnext/commit/66be3c551f4666651b32d78a4fb15cdeed75b189)) * fetch correct quantity and amount for grouped asset ([1dacb79](https://github.com/frappe/erpnext/commit/1dacb794415cdf138be3707dd95cabe5bff3e9f2)) * incorrect amount in the material request item (backport [#39567](https://github.com/frappe/erpnext/issues/39567)) ([#39568](https://github.com/frappe/erpnext/issues/39568)) ([c26f7bb](https://github.com/frappe/erpnext/commit/c26f7bbed0160a533c782d44410001c5fcab9913)) * linter issue ([941f882](https://github.com/frappe/erpnext/commit/941f8824e5f31f42b0f54182d790e8178c9511ab)) * not able to edit / change address from portal ([e3fdb6f](https://github.com/frappe/erpnext/commit/e3fdb6f55cabd34ee97b47b7f50c4aabac59ac06)) * not able to edit address through portal ([b310a55](https://github.com/frappe/erpnext/commit/b310a55727e16ce511173eef1acd6db49f720227)) * Payment Terms Status for Sales Order report should show all payment terms from order not only this comming from template ([2953959](https://github.com/frappe/erpnext/commit/295395918c3515f1ce6757a9ababab42a87d4800)) * perf issue while submitting stock entry (backport [#39634](https://github.com/frappe/erpnext/issues/39634)) ([#39641](https://github.com/frappe/erpnext/issues/39641)) ([3ee0555](https://github.com/frappe/erpnext/commit/3ee05551150fc5dd28f02e548ad2e505bfc2a29f)) * prevent extra transfer against inter transfer transaction (backport [#39213](https://github.com/frappe/erpnext/issues/39213)) ([#39595](https://github.com/frappe/erpnext/issues/39595)) ([bf61030](https://github.com/frappe/erpnext/commit/bf61030dab65e8fff4d6752e320257dacc555f75)) * qtn tests using delivery date ([c50988b](https://github.com/frappe/erpnext/commit/c50988b1bc8b6dbfbec80763e4ecb8a5bb45c38d)) * return doc obj after submit ([0472879](https://github.com/frappe/erpnext/commit/04728792f588cf84cdf53082b92cf0eff4a19f37)) * RM valuation rate in SCR ([#39541](https://github.com/frappe/erpnext/issues/39541)) ([9fd1692](https://github.com/frappe/erpnext/commit/9fd1692db2c1cf45b34273bdd984c37bab13d403)) * typo's and parameter changes ([41c074d](https://github.com/frappe/erpnext/commit/41c074d0bbca82b21fcd27b66adc84fa8a03c6b0)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 15f872b033df..a8d71f538f70 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.61.2" +__version__ = "14.61.3" def get_default_company(user=None): From 4dc5d9a6cad5f353867a96ccd3b4d6eaee34dcf9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:49:22 +0530 Subject: [PATCH 077/138] fix: Exchange rate on MR to PO creation for muticurrency POs (#39646) fix: Exchange rate on MR to PO creation for muticurrency POs (#39646) (cherry picked from commit cfd1666181ffdff8ae79bbbb7863e1b12b3c6090) Co-authored-by: Deepesh Garg --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index b84ccf770b21..b0a0158abf4c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -417,6 +417,7 @@ def select_item(d): postprocess, ) + doclist.set_onload("load_after_mapping", False) return doclist From 350b2cdde39c2c318f286bc01c54ebdb681e8a2d Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:13:29 +0530 Subject: [PATCH 078/138] fix: correctly calculate diff amount for included taxes (#39655) (cherry picked from commit 772f540bef28117c008512ead6558db801d395cd) --- .../accounts/doctype/payment_entry/payment_entry.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 66c82be596ca..b89854b172a9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -874,19 +874,19 @@ def set_difference_amount(self): ) base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount + self.difference_amount = base_party_amount - self.base_received_amount + included_taxes elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes else: - self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes total_deductions = sum(flt(d.amount) for d in self.get("deductions")) - included_taxes = self.get_included_taxes() self.difference_amount = flt( - self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") + self.difference_amount - total_deductions, self.precision("difference_amount") ) def get_included_taxes(self): From 2fbd11d646a9a3b1f18ff2ea0b250957f3aa216f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 14:53:01 +0530 Subject: [PATCH 079/138] refactor: move ignore ERR filters from SOA to General Ledger (cherry picked from commit c077eda64e8b57011c42fbf7b4fe41af9ab2bc2f) # Conflicts: # erpnext/accounts/report/general_ledger/general_ledger.py --- .../process_statement_of_accounts.py | 16 ++-------------- .../report/general_ledger/general_ledger.js | 6 ++++++ .../report/general_ledger/general_ledger.py | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index d4b4b37b4ee4..222c9628018c 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -64,18 +64,6 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" - err_journals = None - if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals: - err_journals = frappe.db.get_all( - "Journal Entry", - filters={ - "company": doc.company, - "docstatus": 1, - "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), - }, - as_list=True, - ) - for entry in doc.customers: if doc.include_ageing: ageing = set_ageing(doc, entry) @@ -88,8 +76,8 @@ def get_statement_dict(doc, get_statement_dict=False): ) filters = get_common_filters(doc) - if err_journals: - filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + if doc.ignore_exchange_rate_revaluation_journals: + filters.update({"ignore_err": True}) if doc.report == "General Ledger": filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index f0ac3d0ffdb8..2b5abcb72409 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -193,8 +193,14 @@ frappe.query_reports["General Ledger"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check" + }, + { + "fieldname": "ignore_err", + "label": __("Ignore Exchange Rate Revaluation Journals"), + "fieldtype": "Check" } + ] } diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 95397452b019..ae772377f302 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -231,6 +231,25 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") +<<<<<<< HEAD +======= + if filters.get("against_voucher_no"): + conditions.append("against_voucher=%(against_voucher_no)s") + + if filters.get("ignore_err"): + err_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "company": filters.get("company"), + "docstatus": 1, + "voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]), + }, + as_list=True, + ) + if err_journals: + filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) + +>>>>>>> c077eda64e (refactor: move ignore ERR filters from SOA to General Ledger) if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") From 686da470fa37674633b80c533b4c9f42f42929b7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 15:03:02 +0530 Subject: [PATCH 080/138] test: ignore_err filter out in General Ledger (cherry picked from commit affca3a519c626a2c3273a82f00ee1579853b0e1) --- .../general_ledger/test_general_ledger.py | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index a8c362e78c1c..c3ed7f2a23f7 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -3,7 +3,7 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import flt, today from erpnext.accounts.report.general_ledger.general_ledger import execute @@ -148,3 +148,105 @@ def test_foreign_account_balance_after_exchange_rate_revaluation(self): self.assertEqual(data[2]["credit"], 900) self.assertEqual(data[3]["debit"], 100) self.assertEqual(data[3]["credit"], 100) + + def test_ignore_exchange_rate_journals_filter(self): + # create a new account with USD currency + account_name = "Test Debtors USD" + company = "_Test Company" + account = frappe.get_doc( + { + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "parent_account": "Accounts Receivable - _TC", + "account_type": "Receivable", + "doctype": "Account", + } + ) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "party_type": "Customer", + "party": "_Test Customer", + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + accounts = revaluation.get_accounts_data() + revaluation.extend("accounts", accounts) + row = revaluation.accounts[0] + row.new_exchange_rate = 83 + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + revaluation.set_total_gain_loss() + revaluation = revaluation.save().submit() + + # post journal entry for Revaluation doc + frappe.db.set_value( + "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) + revaluation_jv = revaluation.make_jv_for_revaluation() + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # With ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": True, + } + ) + ) + self.assertNotIn(revaluation_jv.name, set([x.voucher_no for x in data])) + + # Without ignore_err enabled + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + "ignore_err": False, + } + ) + ) + self.assertIn(revaluation_jv.name, set([x.voucher_no for x in data])) From 6284553f23fab6c3d2152680e6a530ffb1a03722 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 15:46:57 +0530 Subject: [PATCH 081/138] refactor(test): use party with USD billing currency (cherry picked from commit beff566c8267104b97f7e4b80c36715e4eb91832) --- erpnext/accounts/report/general_ledger/test_general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index c3ed7f2a23f7..75f94309bccd 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -179,7 +179,7 @@ def test_ignore_exchange_rate_journals_filter(self): { "account": account.name, "party_type": "Customer", - "party": "_Test Customer", + "party": "_Test Customer USD", "debit_in_account_currency": 1000, "credit_in_account_currency": 0, "exchange_rate": 75, From 68c5a6e86f38522681837ff45a816555d6c923bf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 16:23:26 +0530 Subject: [PATCH 082/138] chore: resolve conflict --- erpnext/accounts/report/general_ledger/general_ledger.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index ae772377f302..269d25b99db0 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -231,11 +231,6 @@ def get_conditions(filters): if filters.get("voucher_no"): conditions.append("voucher_no=%(voucher_no)s") -<<<<<<< HEAD -======= - if filters.get("against_voucher_no"): - conditions.append("against_voucher=%(against_voucher_no)s") - if filters.get("ignore_err"): err_journals = frappe.db.get_all( "Journal Entry", @@ -249,7 +244,6 @@ def get_conditions(filters): if err_journals: filters.update({"voucher_no_not_in": [x[0] for x in err_journals]}) ->>>>>>> c077eda64e (refactor: move ignore ERR filters from SOA to General Ledger) if filters.get("voucher_no_not_in"): conditions.append("voucher_no not in %(voucher_no_not_in)s") From 8035f5b951db8c9eee392340afd8a21e380df7c6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 1 Feb 2024 16:05:27 +0530 Subject: [PATCH 083/138] refactor: use pop up to inform of possible data issue and leave a comment in communcation trail as well (cherry picked from commit 78483e2ee6fdef9e36b63408f6796f0a1e78dac1) --- erpnext/accounts/doctype/account/account.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 22ddc2ffae3b..283e9d2f4efe 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -58,6 +58,7 @@ def validate(self): self.validate_balance_must_be_debit_or_credit() self.validate_account_currency() self.validate_root_company_and_sync_account_to_children() + self.validate_receivable_payable_account_type() def validate_parent(self): """Fetch Parent Details and validate parent account""" @@ -114,6 +115,24 @@ def set_root_and_report_type(self): "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss" ) + def validate_receivable_payable_account_type(self): + doc_before_save = self.get_doc_before_save() + receivable_payable_types = ["Receivable", "Payable"] + if ( + doc_before_save + and doc_before_save.account_type in receivable_payable_types + and doc_before_save.account_type != self.account_type + ): + # check for ledger entries + if frappe.db.get_all("GL Entry", filters={"account": self.name, "is_cancelled": 0}, limit=1): + msg = _( + "There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report" + ).format( + frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type + ) + frappe.msgprint(msg) + self.add_comment("Comment", msg) + def validate_root_details(self): # does not exists parent if frappe.db.exists("Account", self.name): From f8ca5c5c8bf0783ffd525571d157d4e2d597c8e4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 2 Feb 2024 13:23:54 +0530 Subject: [PATCH 084/138] chore: flag for barcode scanner --- erpnext/public/js/utils/barcode_scanner.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index f1b53cb07215..aadbb24cc04b 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -1,6 +1,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { constructor(opts) { this.frm = opts.frm; + // frappe.flags.trigger_from_barcode_scanner is used for custom scripts // field from which to capture input of scanned data this.scan_field_name = opts.scan_field_name || "scan_barcode"; @@ -84,6 +85,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { update_table(data) { return new Promise(resolve => { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; + frappe.flags.trigger_from_barcode_scanner = true; const {item_code, barcode, batch_no, serial_no, uom} = data; @@ -143,12 +145,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { revert_selector_flag() { frappe.flags.hide_serial_batch_dialog = false; + frappe.flags.trigger_from_barcode_scanner = false; } set_item(row, item_code, barcode, batch_no, serial_no) { return new Promise(resolve => { const increment = async (value = 1) => { const item_data = {item_code: item_code}; + frappe.flags.trigger_from_barcode_scanner = true; item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); await frappe.model.set_value(row.doctype, row.name, item_data); return value; From 57cfce08ef2b24c4ef3f6629f0584e6e08c99991 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 2 Feb 2024 13:23:54 +0530 Subject: [PATCH 085/138] chore: flag for barcode scanner (cherry picked from commit f8ca5c5c8bf0783ffd525571d157d4e2d597c8e4) --- erpnext/public/js/utils/barcode_scanner.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index f1b53cb07215..aadbb24cc04b 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -1,6 +1,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { constructor(opts) { this.frm = opts.frm; + // frappe.flags.trigger_from_barcode_scanner is used for custom scripts // field from which to capture input of scanned data this.scan_field_name = opts.scan_field_name || "scan_barcode"; @@ -84,6 +85,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { update_table(data) { return new Promise(resolve => { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; + frappe.flags.trigger_from_barcode_scanner = true; const {item_code, barcode, batch_no, serial_no, uom} = data; @@ -143,12 +145,14 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { revert_selector_flag() { frappe.flags.hide_serial_batch_dialog = false; + frappe.flags.trigger_from_barcode_scanner = false; } set_item(row, item_code, barcode, batch_no, serial_no) { return new Promise(resolve => { const increment = async (value = 1) => { const item_data = {item_code: item_code}; + frappe.flags.trigger_from_barcode_scanner = true; item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value); await frappe.model.set_value(row.doctype, row.name, item_data); return value; From 58ea7f41058ebad832e37e131cad84d4e71a57d0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 Feb 2024 17:50:19 +0530 Subject: [PATCH 086/138] refactor: add 'disabled' field to Bank Account --- erpnext/accounts/doctype/bank_account/bank_account.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index 41d79479ca59..b1d53dc1e708 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -13,6 +13,7 @@ "account_type", "account_subtype", "column_break_7", + "disabled", "is_default", "is_company_account", "company", @@ -199,10 +200,16 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Branch Code" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "links": [], - "modified": "2022-05-04 15:49:42.620630", + "modified": "2024-02-02 17:50:09.768835", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", From 3808ddbf860d9b5e88f544bb6a2108b22aaaca5c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 09:10:28 +0530 Subject: [PATCH 087/138] feat: New financial views - Growth and margin views for P&L and balance sheet (backport #39588) (#39601) feat: New financial views - Growth and margin views for P&L and balance sheet (cherry picked from commit 92649de5c647b1a03689fd77e6a01c29175c65a9) # Conflicts: # erpnext/accounts/report/balance_sheet/balance_sheet.js # erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js Co-authored-by: nitmit --- .../report/balance_sheet/balance_sheet.js | 29 +++++++++++ .../profit_and_loss_statement.js | 23 +++++++++ erpnext/public/js/financial_statements.js | 51 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index f1f8e5f6e7c2..1d5d870cbfcb 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -6,6 +6,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.utils.add_dimensions('Balance Sheet', 10); +<<<<<<< HEAD frappe.query_reports["Balance Sheet"]["filters"].push({ "fieldname": "accumulated_values", "label": __("Accumulated Values"), @@ -19,4 +20,32 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldtype": "Check", "default": 1 }); +======= +frappe.query_reports["Balance Sheet"]["filters"].push( + { + "fieldname": "selected_view", + "label": __("Select View"), + "fieldtype": "Select", + "options": [ + { "value": "Report", "label": __("Report View") }, + { "value": "Growth", "label": __("Growth View") } + ], + "default": "Report", + "reqd": 1 + }, +); + +frappe.query_reports["Balance Sheet"]["filters"].push({ + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + default: 1, +}); + +frappe.query_reports["Balance Sheet"]["filters"].push({ + fieldname: "include_default_book_entries", + label: __("Include Default FB Entries"), + fieldtype: "Check", + default: 1, +>>>>>>> 92649de5c6 (Adding growth and margin views for P&L and balance sheet financial reports in collaboration with Sapcon Instruments Pvt Ltd) }); diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index e794f270c2bc..d2a2c4b46f5e 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -6,6 +6,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements); +<<<<<<< HEAD erpnext.utils.add_dimensions('Profit and Loss Statement', 10); frappe.query_reports["Profit and Loss Statement"]["filters"].push( @@ -16,4 +17,26 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "default": 1 } ); +======= +frappe.query_reports["Profit and Loss Statement"]["filters"].push( + { + "fieldname": "selected_view", + "label": __("Select View"), + "fieldtype": "Select", + "options": [ + { "value": "Report", "label": __("Report View") }, + { "value": "Growth", "label": __("Growth View") }, + { "value": "Margin", "label": __("Margin View") }, + ], + "default": "Report", + "reqd": 1 + }, +); + +frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + default: 1, +>>>>>>> 92649de5c6 (Adding growth and margin views for P&L and balance sheet financial reports in collaboration with Sapcon Instruments Pvt Ltd) }); diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 5e1974299ee1..14b99aa6308b 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -2,7 +2,58 @@ frappe.provide("erpnext.financial_statements"); erpnext.financial_statements = { "filters": get_filters(), + "baseData": null, "formatter": function(value, row, column, data, default_formatter, filter) { + if(frappe.query_report.get_filter_value("selected_view") == "Growth" && data && column.colIndex >= 3){ + //Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns. + const lastAnnualValue = row[column.colIndex - 1].content; + const currentAnnualvalue = data[column.fieldname]; + if(currentAnnualvalue == undefined) return 'NA'; //making this not applicable for undefined/null values + let annualGrowth = 0; + if(lastAnnualValue == 0 && currentAnnualvalue > 0){ + //If the previous year value is 0 and the current value is greater than 0 + annualGrowth = 1; + } + else if(lastAnnualValue > 0){ + annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue; + } + + const growthPercent = (Math.round(annualGrowth*10000)/100); //calculating the rounded off percentage + + value = $(`${((growthPercent >=0)? '+':'' )+growthPercent+'%'}`); + if(growthPercent < 0){ + value = $(value).addClass("text-danger"); + } + else{ + value = $(value).addClass("text-success"); + } + value = $(value).wrap("

").parent().html(); + + return value; + } + else if(frappe.query_report.get_filter_value("selected_view") == "Margin" && data){ + if(column.fieldname =="account" && data.account_name == __("Income")){ + //Taking the total income from each column (for all the financial years) as the base (100%) + this.baseData = row; + } + if(column.colIndex >= 2){ + //Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns. + const currentAnnualvalue = data[column.fieldname]; + const baseValue = this.baseData[column.colIndex].content; + if(currentAnnualvalue == undefined || baseValue <= 0) return 'NA'; + const marginPercent = Math.round((currentAnnualvalue/baseValue)*10000)/100; + + value = $(`${marginPercent+'%'}`); + if(marginPercent < 0) + value = $(value).addClass("text-danger"); + else + value = $(value).addClass("text-success"); + value = $(value).wrap("

").parent().html(); + return value; + } + + } + if (data && column.fieldname=="account") { value = data.account_name || value; From f01308b97288ac630bf9221df897f13071183b24 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 3 Feb 2024 09:11:12 +0530 Subject: [PATCH 088/138] Revert "feat: New financial views - Growth and margin views for P&L and balance sheet (#39588)" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert "feat: New financial views - Growth and margin views for P&L and balan…" This reverts commit 3808ddbf860d9b5e88f544bb6a2108b22aaaca5c. --- .../report/balance_sheet/balance_sheet.js | 29 ----------- .../profit_and_loss_statement.js | 23 --------- erpnext/public/js/financial_statements.js | 51 ------------------- 3 files changed, 103 deletions(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index 1d5d870cbfcb..f1f8e5f6e7c2 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -6,7 +6,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { erpnext.utils.add_dimensions('Balance Sheet', 10); -<<<<<<< HEAD frappe.query_reports["Balance Sheet"]["filters"].push({ "fieldname": "accumulated_values", "label": __("Accumulated Values"), @@ -20,32 +19,4 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldtype": "Check", "default": 1 }); -======= -frappe.query_reports["Balance Sheet"]["filters"].push( - { - "fieldname": "selected_view", - "label": __("Select View"), - "fieldtype": "Select", - "options": [ - { "value": "Report", "label": __("Report View") }, - { "value": "Growth", "label": __("Growth View") } - ], - "default": "Report", - "reqd": 1 - }, -); - -frappe.query_reports["Balance Sheet"]["filters"].push({ - fieldname: "accumulated_values", - label: __("Accumulated Values"), - fieldtype: "Check", - default: 1, -}); - -frappe.query_reports["Balance Sheet"]["filters"].push({ - fieldname: "include_default_book_entries", - label: __("Include Default FB Entries"), - fieldtype: "Check", - default: 1, ->>>>>>> 92649de5c6 (Adding growth and margin views for P&L and balance sheet financial reports in collaboration with Sapcon Instruments Pvt Ltd) }); diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index d2a2c4b46f5e..e794f270c2bc 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -6,7 +6,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements); -<<<<<<< HEAD erpnext.utils.add_dimensions('Profit and Loss Statement', 10); frappe.query_reports["Profit and Loss Statement"]["filters"].push( @@ -17,26 +16,4 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "default": 1 } ); -======= -frappe.query_reports["Profit and Loss Statement"]["filters"].push( - { - "fieldname": "selected_view", - "label": __("Select View"), - "fieldtype": "Select", - "options": [ - { "value": "Report", "label": __("Report View") }, - { "value": "Growth", "label": __("Growth View") }, - { "value": "Margin", "label": __("Margin View") }, - ], - "default": "Report", - "reqd": 1 - }, -); - -frappe.query_reports["Profit and Loss Statement"]["filters"].push({ - fieldname: "accumulated_values", - label: __("Accumulated Values"), - fieldtype: "Check", - default: 1, ->>>>>>> 92649de5c6 (Adding growth and margin views for P&L and balance sheet financial reports in collaboration with Sapcon Instruments Pvt Ltd) }); diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 14b99aa6308b..5e1974299ee1 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -2,58 +2,7 @@ frappe.provide("erpnext.financial_statements"); erpnext.financial_statements = { "filters": get_filters(), - "baseData": null, "formatter": function(value, row, column, data, default_formatter, filter) { - if(frappe.query_report.get_filter_value("selected_view") == "Growth" && data && column.colIndex >= 3){ - //Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns. - const lastAnnualValue = row[column.colIndex - 1].content; - const currentAnnualvalue = data[column.fieldname]; - if(currentAnnualvalue == undefined) return 'NA'; //making this not applicable for undefined/null values - let annualGrowth = 0; - if(lastAnnualValue == 0 && currentAnnualvalue > 0){ - //If the previous year value is 0 and the current value is greater than 0 - annualGrowth = 1; - } - else if(lastAnnualValue > 0){ - annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue; - } - - const growthPercent = (Math.round(annualGrowth*10000)/100); //calculating the rounded off percentage - - value = $(`${((growthPercent >=0)? '+':'' )+growthPercent+'%'}`); - if(growthPercent < 0){ - value = $(value).addClass("text-danger"); - } - else{ - value = $(value).addClass("text-success"); - } - value = $(value).wrap("

").parent().html(); - - return value; - } - else if(frappe.query_report.get_filter_value("selected_view") == "Margin" && data){ - if(column.fieldname =="account" && data.account_name == __("Income")){ - //Taking the total income from each column (for all the financial years) as the base (100%) - this.baseData = row; - } - if(column.colIndex >= 2){ - //Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns. - const currentAnnualvalue = data[column.fieldname]; - const baseValue = this.baseData[column.colIndex].content; - if(currentAnnualvalue == undefined || baseValue <= 0) return 'NA'; - const marginPercent = Math.round((currentAnnualvalue/baseValue)*10000)/100; - - value = $(`${marginPercent+'%'}`); - if(marginPercent < 0) - value = $(value).addClass("text-danger"); - else - value = $(value).addClass("text-success"); - value = $(value).wrap("

").parent().html(); - return value; - } - - } - if (data && column.fieldname=="account") { value = data.account_name || value; From e49f8d5f55b0cafda012475e94d48e11577a86e4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 3 Feb 2024 14:05:47 +0530 Subject: [PATCH 089/138] fix: don't overwrite existing terms in transaction (#39519) * fix: don't overwrite existing terms in transaction (cherry picked from commit 77b044f1a6e7273b21ce5a884429875b10d3bb2e) * refactor: keep the diff small --------- Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- erpnext/public/js/controllers/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 48bde9dbc98f..d5bc76476475 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -746,14 +746,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"]; if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + selling_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_selling_terms); } let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order", "Material Request", "Purchase Receipt"]; // Purchase Invoice is excluded as per issue #3345 if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") && - buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) { + buying_doctypes_for_tc.includes(me.frm.doc.doctype) && !me.frm.doc.tc_name) { me.frm.set_value("tc_name", company_doc.default_buying_terms); } From 4a609d8fa8b8f0688e883382181c80063a7cdc1f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 1 Feb 2024 19:29:06 +0530 Subject: [PATCH 090/138] fix: incorrect landed cost voucher amount (cherry picked from commit d78a1e78148a3702990ae347a2b1c31b580c308e) --- .../doctype/landed_cost_voucher/landed_cost_voucher.py | 7 +++++++ .../stock/doctype/purchase_receipt/purchase_receipt.py | 10 +++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 3511cec2fd73..c73dc65f8a8e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -122,6 +122,13 @@ def set_applicable_charges_on_item(self): self.get("items")[item_count - 1].applicable_charges += diff def validate_applicable_charges_for_item(self): + if self.distribute_charges_based_on == "Distribute Manually" and len(self.taxes) > 1: + frappe.throw( + _( + "Please keep one Applicable Charges, when 'Distribute Charges Based On' is 'Distribute Manually'. For more charges, please create another Landed Cost Voucher." + ) + ) + based_on = self.distribute_charges_based_on.lower() if based_on != "distribute manually": diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d44bb4d26d85..274073b638ea 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1200,16 +1200,16 @@ def get_item_account_wise_additional_cost(purchase_document): for lcv in landed_cost_vouchers: landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent) + based_on_field = None # Use amount field for total item cost for manually cost distributed LCVs - if landed_cost_voucher_doc.distribute_charges_based_on == "Distribute Manually": - based_on_field = "amount" - else: + if landed_cost_voucher_doc.distribute_charges_based_on != "Distribute Manually": based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) total_item_cost = 0 - for item in landed_cost_voucher_doc.items: - total_item_cost += item.get(based_on_field) + if based_on_field: + for item in landed_cost_voucher_doc.items: + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: From 85e6b39e23ae468e88eb49af9f5981ac854a5ac3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 31 Jan 2024 11:32:17 +0530 Subject: [PATCH 091/138] perf: timeout for auto material request through reorder level (cherry picked from commit 951023f434a36ef03f874b3dcbd4f995168b7b5a) # Conflicts: # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- erpnext/controllers/selling_controller.py | 2 +- .../doctype/stock_entry/test_stock_entry.py | 84 +++++++++ erpnext/stock/get_item_details.py | 7 +- erpnext/stock/reorder_item.py | 165 +++++++++++++----- 4 files changed, 215 insertions(+), 43 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d55aeeacc084..c9b12f4f7236 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -586,7 +586,7 @@ def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - item.valuation_rate) * item.stock_qty), self.precision("amount", item) + ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item) ) def set_customer_address(self): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 8afe23a1d7ae..92430861c05a 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1734,6 +1734,90 @@ def test_enqueue_action(self): self.assertFalse(doc.is_enqueue_action()) frappe.flags.in_test = True +<<<<<<< HEAD +======= + def test_negative_batch(self): + item_code = "Test Negative Batch Item - 001" + make_item( + item_code, + {"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"}, + ) + + se1 = make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + qty=100, + target="_Test Warehouse - _TC", + ) + + se1.reload() + + batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) + + se2 = make_stock_entry( + item_code=item_code, + purpose="Material Issue", + batch_no=batch_no, + qty=10, + source="_Test Warehouse - _TC", + ) + + se2.reload() + + se3 = make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + qty=100, + target="_Test Warehouse - _TC", + ) + + se3.reload() + + self.assertRaises(frappe.ValidationError, se1.cancel) + + def test_auto_reorder_level(self): + from erpnext.stock.reorder_item import reorder_item + + item_doc = make_item( + "Test Auto Reorder Item - 001", + properties={"stock_uom": "Kg", "purchase_uom": "Nos", "is_stock_item": 1}, + uoms=[{"uom": "Nos", "conversion_factor": 5}], + ) + + if not frappe.db.exists("Item Reorder", {"parent": item_doc.name}): + item_doc.append( + "reorder_levels", + { + "warehouse_reorder_level": 0, + "warehouse_reorder_qty": 10, + "warehouse": "_Test Warehouse - _TC", + "material_request_type": "Purchase", + }, + ) + + item_doc.save(ignore_permissions=True) + + frappe.db.set_single_value("Stock Settings", "auto_indent", 1) + + mr_list = reorder_item() + + frappe.db.set_single_value("Stock Settings", "auto_indent", 0) + mrs = frappe.get_all( + "Material Request Item", + fields=["qty", "stock_uom", "stock_qty"], + filters={"item_code": item_doc.name, "uom": "Nos"}, + ) + + for mri in mrs: + self.assertEqual(mri.stock_uom, "Kg") + self.assertEqual(mri.stock_qty, 10) + self.assertEqual(mri.qty, 2) + + for mr in mr_list: + mr.cancel() + mr.delete() + +>>>>>>> 951023f434 (perf: timeout for auto material request through reorder level) def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 10d3ef4b7a96..37a6a0d6892d 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -87,7 +87,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru get_party_item_code(args, item, out) - set_valuation_rate(out, args) + if args.get("doctype") in ["Sales Order", "Quotation"]: + set_valuation_rate(out, args) update_party_blanket_order(args, out) @@ -303,7 +304,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): if not item: item = frappe.get_doc("Item", args.get("item_code")) - if item.variant_of and not item.taxes: + if ( + item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of}) + ): item.update_template_tables() item_defaults = get_item_defaults(item.name, args.company) diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py index e172cecb236f..de4242893c6c 100644 --- a/erpnext/stock/reorder_item.py +++ b/erpnext/stock/reorder_item.py @@ -34,73 +34,157 @@ def _reorder_item(): erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0] ) - items_to_consider = frappe.db.sql_list( - """select name from `tabItem` item - where is_stock_item=1 and has_variants=0 - and disabled=0 - and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %(today)s) - and (exists (select name from `tabItem Reorder` ir where ir.parent=item.name) - or (variant_of is not null and variant_of != '' - and exists (select name from `tabItem Reorder` ir where ir.parent=item.variant_of)) - )""", - {"today": nowdate()}, - ) + items_to_consider = get_items_for_reorder() if not items_to_consider: return item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider) - def add_to_material_request( - item_code, warehouse, reorder_level, reorder_qty, material_request_type, warehouse_group=None - ): - if warehouse not in warehouse_company: + def add_to_material_request(**kwargs): + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + if kwargs.warehouse not in warehouse_company: # a disabled warehouse return - reorder_level = flt(reorder_level) - reorder_qty = flt(reorder_qty) + reorder_level = flt(kwargs.reorder_level) + reorder_qty = flt(kwargs.reorder_qty) # projected_qty will be 0 if Bin does not exist - if warehouse_group: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse_group)) + if kwargs.warehouse_group: + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group) + ) else: - projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse)) + projected_qty = flt( + item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse) + ) if (reorder_level or reorder_qty) and projected_qty < reorder_level: deficiency = reorder_level - projected_qty if deficiency > reorder_qty: reorder_qty = deficiency - company = warehouse_company.get(warehouse) or default_company + company = warehouse_company.get(kwargs.warehouse) or default_company - material_requests[material_request_type].setdefault(company, []).append( - {"item_code": item_code, "warehouse": warehouse, "reorder_qty": reorder_qty} + material_requests[kwargs.material_request_type].setdefault(company, []).append( + { + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "reorder_qty": reorder_qty, + "item_details": kwargs.item_details, + } ) - for item_code in items_to_consider: - item = frappe.get_doc("Item", item_code) - - if item.variant_of and not item.get("reorder_levels"): - item.update_template_tables() - - if item.get("reorder_levels"): - for d in item.get("reorder_levels"): - add_to_material_request( - item_code, - d.warehouse, - d.warehouse_reorder_level, - d.warehouse_reorder_qty, - d.material_request_type, - warehouse_group=d.warehouse_group, - ) + for item_code, reorder_levels in items_to_consider.items(): + for d in reorder_levels: + if d.has_variants: + continue + + add_to_material_request( + item_code=item_code, + warehouse=d.warehouse, + reorder_level=d.warehouse_reorder_level, + reorder_qty=d.warehouse_reorder_qty, + material_request_type=d.material_request_type, + warehouse_group=d.warehouse_group, + item_details=frappe._dict( + { + "item_code": item_code, + "name": item_code, + "item_name": d.item_name, + "item_group": d.item_group, + "brand": d.brand, + "description": d.description, + "stock_uom": d.stock_uom, + "purchase_uom": d.purchase_uom, + } + ), + ) if material_requests: return create_material_request(material_requests) +def get_items_for_reorder() -> dict[str, list]: + reorder_table = frappe.qb.DocType("Item Reorder") + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(reorder_table) + .inner_join(item_table) + .on(reorder_table.parent == item_table.name) + .select( + reorder_table.warehouse, + reorder_table.warehouse_group, + reorder_table.material_request_type, + reorder_table.warehouse_reorder_level, + reorder_table.warehouse_reorder_qty, + item_table.name, + item_table.stock_uom, + item_table.purchase_uom, + item_table.description, + item_table.item_name, + item_table.item_group, + item_table.brand, + item_table.variant_of, + item_table.has_variants, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + ) + ) + + data = query.run(as_dict=True) + itemwise_reorder = frappe._dict({}) + for d in data: + itemwise_reorder.setdefault(d.name, []).append(d) + + itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder) + + return itemwise_reorder + + +def get_reorder_levels_for_variants(itemwise_reorder): + item_table = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(item_table) + .select( + item_table.name, + item_table.variant_of, + ) + .where( + (item_table.disabled == 0) + & (item_table.is_stock_item == 1) + & ( + (item_table.end_of_life.isnull()) + | (item_table.end_of_life > nowdate()) + | (item_table.end_of_life == "0000-00-00") + ) + & (item_table.variant_of.notnull()) + ) + ) + + variants_item = query.run(as_dict=True) + for row in variants_item: + if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of): + itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, [])) + + return itemwise_reorder + + def get_item_warehouse_projected_qty(items_to_consider): item_warehouse_projected_qty = {} + items_to_consider = list(items_to_consider.keys()) for item_code, warehouse, projected_qty in frappe.db.sql( """select item_code, warehouse, projected_qty @@ -164,7 +248,7 @@ def _log_exception(mr): for d in items: d = frappe._dict(d) - item = frappe.get_doc("Item", d.item_code) + item = d.get("item_details") uom = item.stock_uom conversion_factor = 1.0 @@ -190,6 +274,7 @@ def _log_exception(mr): "item_code": d.item_code, "schedule_date": add_days(nowdate(), cint(item.lead_time_days)), "qty": qty, + "conversion_factor": conversion_factor, "uom": uom, "stock_uom": item.stock_uom, "warehouse": d.warehouse, From b32848d69dfb6614b013eca0df328e7923523a21 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 5 Feb 2024 11:46:39 +0530 Subject: [PATCH 092/138] perf: memory consumption for the stock balance report (#39626) (cherry picked from commit b70f3de16be169a723842a3a99c046a3809d0768) # Conflicts: # erpnext/stock/report/stock_balance/stock_balance.py --- .../report/stock_balance/stock_balance.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 7a5a8615d0c5..4e6f9865acab 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -90,8 +90,7 @@ def prepare_opening_data_from_closing_balance(self) -> None: self.opening_data.setdefault(group_by_key, entry) def prepare_new_data(self): - if not self.sle_entries: - return + self.item_warehouse_map = self.get_item_warehouse_map() if self.filters.get("show_stock_ageing_data"): self.filters["show_warehouse_wise_stock"] = True @@ -99,7 +98,13 @@ def prepare_new_data(self): _func = itemgetter(1) +<<<<<<< HEAD self.item_warehouse_map = self.get_item_warehouse_map() +======= + del self.sle_entries + + sre_details = self.get_sre_reserved_qty_details() +>>>>>>> b70f3de16b (perf: memory consumption for the stock balance report (#39626)) variant_values = {} if self.filters.get("show_variant_attributes"): @@ -139,15 +144,22 @@ def get_item_warehouse_map(self): item_warehouse_map = {} self.opening_vouchers = self.get_opening_vouchers() - for entry in self.sle_entries: - group_by_key = self.get_group_by_key(entry) - if group_by_key not in item_warehouse_map: - self.initialize_data(item_warehouse_map, group_by_key, entry) + if self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True) + + with frappe.db.unbuffered_cursor(): + if not self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) + + for entry in self.sle_entries: + group_by_key = self.get_group_by_key(entry) + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) - self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) - if self.opening_data.get(group_by_key): - del self.opening_data[group_by_key] + if self.opening_data.get(group_by_key): + del self.opening_data[group_by_key] for group_by_key, entry in self.opening_data.items(): if group_by_key not in item_warehouse_map: @@ -236,7 +248,8 @@ def get_closing_balance(self) -> List[Dict[str, Any]]: .where( (table.docstatus == 1) & (table.company == self.filters.company) - & ((table.to_date <= self.from_date)) + & (table.to_date <= self.from_date) + & (table.status == "Completed") ) .orderby(table.to_date, order=Order.desc) .limit(1) @@ -289,7 +302,7 @@ def prepare_stock_ledger_entries(self): if self.filters.get("company"): query = query.where(sle.company == self.filters.get("company")) - self.sle_entries = query.run(as_dict=True) + self.sle_query = query def apply_inventory_dimensions_filters(self, query, sle) -> str: inventory_dimension_fields = self.get_inventory_dimension_fields() From 26dfbb7a641a4a3342030daad6e1c21ff3175103 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 5 Feb 2024 12:13:14 +0530 Subject: [PATCH 093/138] chore: fix conflicts --- erpnext/stock/report/stock_balance/stock_balance.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 4e6f9865acab..32a2b302d7b2 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -98,14 +98,8 @@ def prepare_new_data(self): _func = itemgetter(1) -<<<<<<< HEAD - self.item_warehouse_map = self.get_item_warehouse_map() -======= del self.sle_entries - sre_details = self.get_sre_reserved_qty_details() ->>>>>>> b70f3de16b (perf: memory consumption for the stock balance report (#39626)) - variant_values = {} if self.filters.get("show_variant_attributes"): variant_values = self.get_variant_values_for() From e38bb836a5c4b58f8973e5389b63348006a49f86 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 5 Feb 2024 13:20:03 +0530 Subject: [PATCH 094/138] chore: fix conflicts --- .../doctype/stock_entry/test_stock_entry.py | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 92430861c05a..1d7e4da26d53 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1734,47 +1734,6 @@ def test_enqueue_action(self): self.assertFalse(doc.is_enqueue_action()) frappe.flags.in_test = True -<<<<<<< HEAD -======= - def test_negative_batch(self): - item_code = "Test Negative Batch Item - 001" - make_item( - item_code, - {"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"}, - ) - - se1 = make_stock_entry( - item_code=item_code, - purpose="Material Receipt", - qty=100, - target="_Test Warehouse - _TC", - ) - - se1.reload() - - batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle) - - se2 = make_stock_entry( - item_code=item_code, - purpose="Material Issue", - batch_no=batch_no, - qty=10, - source="_Test Warehouse - _TC", - ) - - se2.reload() - - se3 = make_stock_entry( - item_code=item_code, - purpose="Material Receipt", - qty=100, - target="_Test Warehouse - _TC", - ) - - se3.reload() - - self.assertRaises(frappe.ValidationError, se1.cancel) - def test_auto_reorder_level(self): from erpnext.stock.reorder_item import reorder_item @@ -1817,7 +1776,6 @@ def test_auto_reorder_level(self): mr.cancel() mr.delete() ->>>>>>> 951023f434 (perf: timeout for auto material request through reorder level) def make_serialized_item(**args): args = frappe._dict(args) From 7952bf43187c88b1c5ce5fd4a84ec45f526d72a2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:05:24 +0530 Subject: [PATCH 095/138] feat: copy emails from lead to customer (#38647) feat: copy emails from lead to customer (cherry picked from commit 906ac093e37694c4616cffb961b131ce9002315b) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- erpnext/selling/doctype/customer/customer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index b7fcadb1eca2..74f68ea69227 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -171,6 +171,7 @@ def on_update(self): if self.flags.is_new_doc: self.link_lead_address_and_contact() + self.copy_communication() self.update_customer_groups() @@ -224,6 +225,17 @@ def link_lead_address_and_contact(self): linked_doc.append("links", dict(link_doctype="Customer", link_name=self.name)) linked_doc.save(ignore_permissions=self.flags.ignore_permissions) + def copy_communication(self): + if not self.lead_name or not frappe.db.get_single_value( + "CRM Settings", "carry_forward_communication_and_comments" + ): + return + + from erpnext.crm.utils import copy_comments, link_communications + + copy_comments("Lead", self.lead_name, self) + link_communications("Lead", self.lead_name, self) + def validate_name_with_customer_group(self): if frappe.db.exists("Customer Group", self.name): frappe.throw( From 7691256f4df404ca94f826feb2bc05f280b2857a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:34:16 +0530 Subject: [PATCH 096/138] perf: Move dimension validation out of GL Entry doctype (#39730) perf: Move dimension validation out of GL Entry doctype (#39730) (cherry picked from commit b834ed10d6b6faa1f76e1343ca8b347ca393b7dd) Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 46 +------------------ erpnext/accounts/general_ledger.py | 42 +++++++++++++++++ 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 3a564825b55a..6e07b0ec430c 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -13,16 +13,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( - get_dimension_filter_map, -) from erpnext.accounts.party import validate_party_frozen_disabled, validate_party_gle_currency from erpnext.accounts.utils import get_account_currency, get_fiscal_year -from erpnext.exceptions import ( - InvalidAccountCurrency, - InvalidAccountDimensionError, - MandatoryAccountDimensionError, -) +from erpnext.exceptions import InvalidAccountCurrency exclude_from_linked_with = True @@ -54,7 +47,6 @@ def on_update(self): if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher": self.validate_account_details(adv_adj) self.validate_dimensions_for_pl_and_bs() - self.validate_allowed_dimensions() validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) @@ -164,42 +156,6 @@ def validate_dimensions_for_pl_and_bs(self): ) ) - def validate_allowed_dimensions(self): - dimension_filter_map = get_dimension_filter_map() - for key, value in dimension_filter_map.items(): - dimension = key[0] - account = key[1] - - if self.account == account: - if value["is_mandatory"] and not self.get(dimension): - frappe.throw( - _("{0} is mandatory for account {1}").format( - frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account) - ), - MandatoryAccountDimensionError, - ) - - if value["allow_or_restrict"] == "Allow": - if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - else: - if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]: - frappe.throw( - _("Invalid value {0} for {1} against account {2}").format( - frappe.bold(self.get(dimension)), - frappe.bold(frappe.unscrub(dimension)), - frappe.bold(self.account), - ), - InvalidAccountDimensionError, - ) - def check_pl_account(self): if ( self.is_opening == "Yes" diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 134ddddf9e02..38fcd976ad4a 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -13,9 +13,13 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) +from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( + get_dimension_filter_map, +) from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.utils import create_payment_ledger_entry +from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError def make_gl_entries( @@ -354,6 +358,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): process_debit_credit_difference(gl_map) + dimension_filter_map = get_dimension_filter_map() if gl_map: check_freezing_date(gl_map[0]["posting_date"], adv_adj) is_opening = any(d.get("is_opening") == "Yes" for d in gl_map) @@ -361,6 +366,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"]) for entry in gl_map: + validate_allowed_dimensions(entry, dimension_filter_map) make_entry(entry, adv_adj, update_outstanding, from_repost) @@ -672,3 +678,39 @@ def set_as_cancel(voucher_type, voucher_no): where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no), ) + + +def validate_allowed_dimensions(gl_entry, dimension_filter_map): + for key, value in dimension_filter_map.items(): + dimension = key[0] + account = key[1] + + if gl_entry.account == account: + if value["is_mandatory"] and not gl_entry.get(dimension): + frappe.throw( + _("{0} is mandatory for account {1}").format( + frappe.bold(frappe.unscrub(dimension)), frappe.bold(gl_entry.account) + ), + MandatoryAccountDimensionError, + ) + + if value["allow_or_restrict"] == "Allow": + if gl_entry.get(dimension) and gl_entry.get(dimension) not in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) + else: + if gl_entry.get(dimension) and gl_entry.get(dimension) in value["allowed_dimensions"]: + frappe.throw( + _("Invalid value {0} for {1} against account {2}").format( + frappe.bold(gl_entry.get(dimension)), + frappe.bold(frappe.unscrub(dimension)), + frappe.bold(gl_entry.account), + ), + InvalidAccountDimensionError, + ) From 46ac4f471423ff652510e582f7a3e59141db2f02 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:09:09 +0530 Subject: [PATCH 097/138] fix: remove applied pricing rule on qty change (backport #39688) (#39736) fix: remove pricing rule (cherry picked from commit 7c6a5a0f23b948953815870b726c30b0fd076338) Co-authored-by: s-aga-r --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a4597da8e277..a3f48141aaca 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -671,7 +671,7 @@ def set_missing_item_details(self, for_validate=False): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) + ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: From 44c09de7298005651c09e62ba578b7a8126e80ef Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:52:01 +0530 Subject: [PATCH 098/138] fix: Blanket Order Ordered Quantity (backport #39725) (#39738) * fix: disable no-copy for blanket order in PO (cherry picked from commit 5ce5c352e4ab2293b3f5b5dac9bc1a2a1912e620) # Conflicts: # erpnext/buying/doctype/purchase_order_item/purchase_order_item.json * fix: update BO Ordered Quantity on PO Close/Open (cherry picked from commit 61ded697a7384d8ef133a42424d8a14763bb6061) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py * test: BO on PO Close/Open (cherry picked from commit 27d6c8b6d52ada292eef5c42506e95bcf933eec8) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../doctype/purchase_order/purchase_order.py | 1 + .../purchase_order/test_purchase_order.py | 25 +++++++++++++++++++ .../purchase_order_item.json | 4 +-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 8131104b825c..5d1dfafc3c9c 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -317,6 +317,7 @@ def update_status(self, status): self.update_requested_qty() self.update_ordered_qty() self.update_reserved_qty_for_subcontract() + self.update_blanket_order() self.notify_update() clear_doctype_notifications(self) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index b0bbc5d0c71a..739a989c79e1 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -814,6 +814,30 @@ def test_po_optional_blanket_order(self): # To test if the PO does NOT have a Blanket Order self.assertEqual(po_doc.items[0].blanket_order, None) + def test_blanket_order_on_po_close_and_open(self): + # Step - 1: Create Blanket Order + bo = make_blanket_order(blanket_order_type="Purchasing", quantity=10, rate=10) + + # Step - 2: Create Purchase Order + po = create_purchase_order( + item_code="_Test Item", qty=5, against_blanket_order=1, against_blanket=bo.name + ) + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + + # Step - 3: Close Purchase Order + po.update_status("Closed") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 0) + + # Step - 4: Re-Open Purchase Order + po.update_status("Re-open") + + bo.load_from_db() + self.assertEqual(bo.items[0].ordered_qty, 5) + def test_payment_terms_are_fetched_when_creating_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import ( create_payment_terms_template, @@ -1016,6 +1040,7 @@ def create_purchase_order(**args): "schedule_date": add_days(nowdate(), 1), "include_exploded_items": args.get("include_exploded_items", 1), "against_blanket_order": args.against_blanket_order, + "against_blanket": args.against_blanket, "material_request": args.material_request, "material_request_item": args.material_request_item, }, diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 1a9035c33279..feb1a9b8828c 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -546,7 +546,6 @@ "fieldname": "blanket_order", "fieldtype": "Link", "label": "Blanket Order", - "no_copy": 1, "options": "Blanket Order" }, { @@ -554,7 +553,6 @@ "fieldname": "blanket_order_rate", "fieldtype": "Currency", "label": "Blanket Order Rate", - "no_copy": 1, "print_hide": 1, "read_only": 1 }, @@ -918,7 +916,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-24 19:07:34.921094", + "modified": "2024-02-05 11:23:24.859435", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 33ae0fa2f4a7fefff85198f3a1faf62be6e0e833 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:38:00 +0530 Subject: [PATCH 099/138] fix: correctly calculate diff amount for included taxes (#39655) fix: correctly calculate diff amount for included taxes (#39655) (cherry picked from commit 772f540bef28117c008512ead6558db801d395cd) (cherry picked from commit 350b2cdde39c2c318f286bc01c54ebdb681e8a2d) Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> --- .../accounts/doctype/payment_entry/payment_entry.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 66c82be596ca..b89854b172a9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -874,19 +874,19 @@ def set_difference_amount(self): ) base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount) + included_taxes = self.get_included_taxes() if self.payment_type == "Receive": - self.difference_amount = base_party_amount - self.base_received_amount + self.difference_amount = base_party_amount - self.base_received_amount + included_taxes elif self.payment_type == "Pay": - self.difference_amount = self.base_paid_amount - base_party_amount + self.difference_amount = self.base_paid_amount - base_party_amount - included_taxes else: - self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) + self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - included_taxes total_deductions = sum(flt(d.amount) for d in self.get("deductions")) - included_taxes = self.get_included_taxes() self.difference_amount = flt( - self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount") + self.difference_amount - total_deductions, self.precision("difference_amount") ) def get_included_taxes(self): From 99929e94342a6ee2337abed6572b7853f10c716d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 5 Feb 2024 12:09:13 +0000 Subject: [PATCH 100/138] chore(release): Bumped to Version 14.61.4 ## [14.61.4](https://github.com/frappe/erpnext/compare/v14.61.3...v14.61.4) (2024-02-05) ### Bug Fixes * correctly calculate diff amount for included taxes ([#39655](https://github.com/frappe/erpnext/issues/39655)) ([33ae0fa](https://github.com/frappe/erpnext/commit/33ae0fa2f4a7fefff85198f3a1faf62be6e0e833)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index a8d71f538f70..01144b8e1099 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.61.3" +__version__ = "14.61.4" def get_default_company(user=None): From a1a70bbae0723f3543f44daeaf675a0989061d76 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 5 Feb 2024 19:26:31 +0530 Subject: [PATCH 101/138] perf: timeout while submitting the purchase receipt entry (cherry picked from commit 1fa62333773a59feb5a0a39d274ebcae362ed286) --- erpnext/buying/doctype/purchase_order/purchase_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 5d1dfafc3c9c..9d4846056eaf 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -457,6 +457,7 @@ def update_ordered_qty_in_so_for_removed_items(self, removed_items): ) +@frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" From 0163e13aa30aaebd65a598ad38322226bbd4bdfa Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 2 Feb 2024 17:45:50 +0530 Subject: [PATCH 102/138] refactor: ensure unique accounts for each Bank Account's (cherry picked from commit 2caa2d677c09793f7097c3e76ebb4180a3f2a336) --- .../accounts/doctype/bank_account/bank_account.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py index addcf62e5b60..ece27e77ee97 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.py +++ b/erpnext/accounts/doctype/bank_account/bank_account.py @@ -9,6 +9,7 @@ load_address_and_contact, ) from frappe.model.document import Document +from frappe.utils import comma_and, get_link_to_form class BankAccount(Document): @@ -25,6 +26,17 @@ def on_trash(self): def validate(self): self.validate_company() self.validate_iban() + self.validate_account() + + def validate_account(self): + if self.account: + if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1): + frappe.throw( + _("'{0}' account is already used by {1}. Use another account.").format( + frappe.bold(self.account), + frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])), + ) + ) def validate_company(self): if self.is_company_account and not self.company: From f64f0437aecbc23d319cafb2a282e56b6a5efc08 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 10:58:31 +0530 Subject: [PATCH 103/138] refactor(test): generate uniq GL acc and Bank acc for each test case (cherry picked from commit a9a2ec81de73cde0995c837e12cd5dc79a584841) --- .../bank_transaction/test_bank_transaction.py | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index eb0dc74825d1..b35ed01cd9ab 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -32,8 +32,16 @@ def setUp(self): frappe.db.delete(dt) make_pos_profile() - add_transactions() - add_vouchers() + + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + gl_account = create_gl_account("_Test Bank " + uniq_identifier) + bank_account = create_bank_account( + gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier + ) + + add_transactions(bank_account=bank_account) + add_vouchers(gl_account=gl_account) # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): @@ -213,7 +221,9 @@ def test_matching_loan_repayment(self): self.assertEqual(linked_payments[0][2], repayment_entry.name) -def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): +def create_bank_account( + bank_name="Citi Bank", gl_account="_Test Bank - _TC", bank_account_name="Checking Account" +): try: frappe.get_doc( { @@ -225,21 +235,35 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass try: - frappe.get_doc( + bank_account = frappe.get_doc( { "doctype": "Bank Account", - "account_name": "Checking Account", + "account_name": bank_account_name, "bank": bank_name, - "account": account_name, + "account": gl_account, } ).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass + return bank_account.name -def add_transactions(): - create_bank_account() +def create_gl_account(gl_account_name="_Test Bank - _TC"): + gl_account = frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "parent_account": "Current Assets - _TC", + "account_type": "Bank", + "is_group": 0, + "account_name": gl_account_name, + } + ).insert() + return gl_account.name + + +def add_transactions(bank_account="_Test Bank - _TC"): doc = frappe.get_doc( { "doctype": "Bank Transaction", @@ -247,7 +271,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1200, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -259,7 +283,7 @@ def add_transactions(): "date": "2018-10-23", "deposit": 1700, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -271,7 +295,7 @@ def add_transactions(): "date": "2018-10-26", "withdrawal": 690, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -283,7 +307,7 @@ def add_transactions(): "date": "2018-10-27", "deposit": 3900, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() @@ -295,13 +319,13 @@ def add_transactions(): "date": "2018-10-27", "withdrawal": 109080, "currency": "INR", - "bank_account": "Checking Account - Citi Bank", + "bank_account": bank_account, } ).insert() doc.submit() -def add_vouchers(): +def add_vouchers(gl_account="_Test Bank - _TC"): try: frappe.get_doc( { @@ -317,7 +341,7 @@ def add_vouchers(): pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Conrad Oct 18" pe.reference_date = "2018-10-24" pe.insert() @@ -336,14 +360,14 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1200) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Oct 18" pe.reference_date = "2018-10-24" pe.insert() pe.submit() pi = make_purchase_invoice(supplier="Mr G", qty=1, rate=1700) - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Herr G Nov 18" pe.reference_date = "2018-11-01" pe.insert() @@ -374,10 +398,10 @@ def add_vouchers(): pass pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1) - pi.cash_bank_account = "_Test Bank - _TC" + pi.cash_bank_account = gl_account pi.insert() pi.submit() - pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.paid_amount = 690 @@ -386,7 +410,7 @@ def add_vouchers(): pe.submit() si = create_sales_invoice(customer="Poore Simon's", qty=1, rate=3900) - pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Sales Invoice", si.name, bank_account=gl_account) pe.reference_no = "Poore Simon's Oct 18" pe.reference_date = "2018-10-28" pe.insert() @@ -409,16 +433,12 @@ def add_vouchers(): if not frappe.db.get_value( "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"} ): - mode_of_payment.append( - "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} - ) + mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account}) mode_of_payment.save() si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si.is_pos = 1 - si.append( - "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080} - ) + si.append("payments", {"mode_of_payment": "Cash", "account": gl_account, "amount": 109080}) si.insert() si.submit() From 8267fee9b8666cecda4884a1c8743549ec406245 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 3 Feb 2024 12:08:01 +0530 Subject: [PATCH 104/138] refactor(test): make use of test fixtures in Payment Order (cherry picked from commit 322cdbaccf0b8697000aae4e56efa659a34fa8e5) --- .../payment_order/test_payment_order.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py index 0dcb1794b9ad..60f288e1f070 100644 --- a/erpnext/accounts/doctype/payment_order/test_payment_order.py +++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py @@ -4,9 +4,13 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import getdate -from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import ( + create_bank_account, + create_gl_account, +) from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_payment_entry, make_payment_order, @@ -14,28 +18,32 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -class TestPaymentOrder(unittest.TestCase): +class TestPaymentOrder(FrappeTestCase): def setUp(self): - create_bank_account() + # generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error + uniq_identifier = frappe.generate_hash(length=10) + self.gl_account = create_gl_account("_Test Bank " + uniq_identifier) + self.bank_account = create_bank_account( + gl_account=self.gl_account, bank_account_name="Checking Account " + uniq_identifier + ) def tearDown(self): - for bt in frappe.get_all("Payment Order"): - doc = frappe.get_doc("Payment Order", bt.name) - doc.cancel() - doc.delete() + frappe.db.rollback() def test_payment_order_creation_against_payment_entry(self): purchase_invoice = make_purchase_invoice() payment_entry = get_payment_entry( - "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC" + "Purchase Invoice", purchase_invoice.name, bank_account=self.gl_account ) payment_entry.reference_no = "_Test_Payment_Order" payment_entry.reference_date = getdate() - payment_entry.party_bank_account = "Checking Account - Citi Bank" + payment_entry.party_bank_account = self.bank_account payment_entry.insert() payment_entry.submit() - doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") + doc = create_payment_order_against_payment_entry( + payment_entry, "Payment Entry", self.bank_account + ) reference_doc = doc.get("references")[0] self.assertEqual(reference_doc.reference_name, payment_entry.name) self.assertEqual(reference_doc.reference_doctype, "Payment Entry") @@ -43,13 +51,13 @@ def test_payment_order_creation_against_payment_entry(self): self.assertEqual(reference_doc.amount, 250) -def create_payment_order_against_payment_entry(ref_doc, order_type): +def create_payment_order_against_payment_entry(ref_doc, order_type, bank_account): payment_order = frappe.get_doc( dict( doctype="Payment Order", company="_Test Company", payment_order_type=order_type, - company_bank_account="Checking Account - Citi Bank", + company_bank_account=bank_account, ) ) doc = make_payment_order(ref_doc.name, payment_order) From a6067c623957123753dd64b5a1b6581280587a99 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:45:43 +0530 Subject: [PATCH 105/138] fix: show warehouse title field in sales docs (backport #39746) (#39754) fix: show warehouse title field in sales docs (cherry picked from commit ee14faaa39bcb0fb8d813aee7e00eedc8d3a38c1) Co-authored-by: s-aga-r --- erpnext/controllers/queries.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 06ea8336bd64..303548cca426 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -679,17 +679,24 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - query = """select `tabWarehouse`.name, + warehouse_field = "name" + meta = frappe.get_meta("Warehouse") + if meta.get("show_title_field_in_link") and meta.get("title_field"): + searchfield = meta.get("title_field") + warehouse_field = meta.get("title_field") + + query = """select `tabWarehouse`.`{warehouse_field}`, CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty from `tabWarehouse` left join `tabBin` on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by ifnull(`tabBin`.actual_qty, 0) desc + order by ifnull(`tabBin`.actual_qty, 0) desc, `tabWarehouse`.`{warehouse_field}` asc limit {page_len} offset {start} """.format( + warehouse_field=warehouse_field, bin_conditions=get_filters_cond( doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True ), From 9a8a9568b1944ed473e54a8dff753ae062a758bb Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 6 Feb 2024 18:44:23 +0000 Subject: [PATCH 106/138] chore(release): Bumped to Version 14.62.0 # [14.62.0](https://github.com/frappe/erpnext/compare/v14.61.4...v14.62.0) (2024-02-06) ### Bug Fixes * Blanket Order Ordered Quantity (backport [#39725](https://github.com/frappe/erpnext/issues/39725)) ([#39738](https://github.com/frappe/erpnext/issues/39738)) ([44c09de](https://github.com/frappe/erpnext/commit/44c09de7298005651c09e62ba578b7a8126e80ef)) * correctly calculate diff amount for included taxes ([#39655](https://github.com/frappe/erpnext/issues/39655)) ([350b2cd](https://github.com/frappe/erpnext/commit/350b2cdde39c2c318f286bc01c54ebdb681e8a2d)) * don't overwrite existing terms in transaction ([#39519](https://github.com/frappe/erpnext/issues/39519)) ([e49f8d5](https://github.com/frappe/erpnext/commit/e49f8d5f55b0cafda012475e94d48e11577a86e4)) * Exchange rate on MR to PO creation for muticurrency POs ([#39646](https://github.com/frappe/erpnext/issues/39646)) ([4dc5d9a](https://github.com/frappe/erpnext/commit/4dc5d9a6cad5f353867a96ccd3b4d6eaee34dcf9)) * incorrect landed cost voucher amount ([4a609d8](https://github.com/frappe/erpnext/commit/4a609d8fa8b8f0688e883382181c80063a7cdc1f)) * remove applied pricing rule on qty change (backport [#39688](https://github.com/frappe/erpnext/issues/39688)) ([#39736](https://github.com/frappe/erpnext/issues/39736)) ([46ac4f4](https://github.com/frappe/erpnext/commit/46ac4f471423ff652510e582f7a3e59141db2f02)) * show warehouse title field in sales docs (backport [#39746](https://github.com/frappe/erpnext/issues/39746)) ([#39754](https://github.com/frappe/erpnext/issues/39754)) ([a6067c6](https://github.com/frappe/erpnext/commit/a6067c623957123753dd64b5a1b6581280587a99)) ### Features * copy emails from lead to customer ([#38647](https://github.com/frappe/erpnext/issues/38647)) ([7952bf4](https://github.com/frappe/erpnext/commit/7952bf43187c88b1c5ce5fd4a84ec45f526d72a2)) * New financial views - Growth and margin views for P&L and balance sheet (backport [#39588](https://github.com/frappe/erpnext/issues/39588)) ([#39601](https://github.com/frappe/erpnext/issues/39601)) ([3808ddb](https://github.com/frappe/erpnext/commit/3808ddbf860d9b5e88f544bb6a2108b22aaaca5c)) ### Performance Improvements * memory consumption for the stock balance report ([#39626](https://github.com/frappe/erpnext/issues/39626)) ([b32848d](https://github.com/frappe/erpnext/commit/b32848d69dfb6614b013eca0df328e7923523a21)) * Move dimension validation out of GL Entry doctype ([#39730](https://github.com/frappe/erpnext/issues/39730)) ([7691256](https://github.com/frappe/erpnext/commit/7691256f4df404ca94f826feb2bc05f280b2857a)) * timeout for auto material request through reorder level ([85e6b39](https://github.com/frappe/erpnext/commit/85e6b39e23ae468e88eb49af9f5981ac854a5ac3)) * timeout while submitting the purchase receipt entry ([a1a70bb](https://github.com/frappe/erpnext/commit/a1a70bbae0723f3543f44daeaf675a0989061d76)) ### Reverts * Revert "feat: New financial views - Growth and margin views for P&L and balance sheet (#39588)" ([f01308b](https://github.com/frappe/erpnext/commit/f01308b97288ac630bf9221df897f13071183b24)), closes [#39588](https://github.com/frappe/erpnext/issues/39588) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 01144b8e1099..181e366135b4 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.61.4" +__version__ = "14.62.0" def get_default_company(user=None): From e3591270c689fa262f757feb9413654fa8fb54b6 Mon Sep 17 00:00:00 2001 From: Babuuu <59009890+aynugek@users.noreply.github.com> Date: Wed, 7 Feb 2024 13:12:14 +0000 Subject: [PATCH 107/138] refactor: Clean up code used to fetch website item stock details --- erpnext/utilities/product.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index 49066cd0c8af..963cd2bf8f0e 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -11,7 +11,6 @@ def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): - in_stock, stock_qty = 0, "" template_item_code, is_stock_item = frappe.db.get_value( "Item", item_code, ["variant_of", "is_stock_item"] ) @@ -52,10 +51,11 @@ def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): .where((BIN.item_code == item_code) & (BIN.warehouse == warehouse)) ).run() + stock_qty = stock_qty[0][0] if stock_qty: total_stock += adjust_qty_for_expired_items(item_code, stock_qty, warehouse) - in_stock = total_stock > 0 and 1 or 0 + in_stock = int(total_stock > 0) return frappe._dict( {"in_stock": in_stock, "stock_qty": total_stock, "is_stock_item": is_stock_item} @@ -63,20 +63,16 @@ def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): def adjust_qty_for_expired_items(item_code, stock_qty, warehouse): - batches = frappe.get_all("Batch", filters=[{"item": item_code}], fields=["expiry_date", "name"]) + batches = frappe.get_all("Batch", filters={"item": item_code}, fields=["expiry_date", "name"]) expired_batches = get_expired_batches(batches) - stock_qty = [list(item) for item in stock_qty] for batch in expired_batches: if warehouse: - stock_qty[0][0] = max(0, stock_qty[0][0] - get_batch_qty(batch, warehouse)) + stock_qty = max(0, stock_qty - get_batch_qty(batch, warehouse)) else: - stock_qty[0][0] = max(0, stock_qty[0][0] - qty_from_all_warehouses(get_batch_qty(batch))) + stock_qty = max(0, stock_qty - qty_from_all_warehouses(get_batch_qty(batch))) - if not stock_qty[0][0]: - break - - return stock_qty[0][0] if stock_qty else 0 + return stock_qty def get_expired_batches(batches): From c56f3a58ab31aaecee2b4007aac53d10f6e73473 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 7 Feb 2024 18:46:07 +0530 Subject: [PATCH 108/138] fix: remove duplicates from tax category map --- .../tds_payable_monthly.py | 10 +-- .../test_tds_payable_monthly.py | 89 +++++++++++++++---- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index b96bfbeb8178..ba5cdbe6567c 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -63,16 +63,14 @@ def get_result( tax_amount += entry.credit - entry.debit # infer tax withholding category from the account if it's the single account for this category tax_withholding_category = tds_accounts.get(entry.account) - rate = tax_rate_map.get(tax_withholding_category) # or else the consolidated value from the voucher document if not tax_withholding_category: - # or else from the party default tax_withholding_category = tax_category_map.get(name) - rate = tax_rate_map.get(tax_withholding_category) + # or else from the party default if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) + rate = tax_rate_map.get(tax_withholding_category) if net_total_map.get(name): if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount @@ -295,7 +293,7 @@ def get_tds_docs(filters): tds_accounts = {} for tds_acc in _tds_accounts: # if it turns out not to be the only tax withholding category, then don't include in the map - if tds_accounts.get(tds_acc["account"]): + if tds_acc["account"] in tds_accounts: tds_accounts[tds_acc["account"]] = None else: tds_accounts[tds_acc["account"]] = tds_acc["parent"] @@ -408,7 +406,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): "paid_amount_after_tax", "base_paid_amount", ], - "Journal Entry": ["tax_withholding_category", "total_amount"], + "Journal Entry": ["total_amount"], } entries = frappe.get_all( diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index 89ecef1904c0..f36b7ef1906a 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -5,7 +5,6 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import today -from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -22,18 +21,42 @@ def setUp(self): self.create_company() self.clear_old_entries() create_tax_accounts() - create_tcs_category() def test_tax_withholding_for_customers(self): + create_tax_category(cumulative_threshold=300) + frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS") si = create_sales_invoice(rate=1000) pe = create_tcs_payment_entry() + jv = create_tcs_journal_entry() + filters = frappe._dict( company="_Test Company", party_type="Customer", from_date=today(), to_date=today() ) result = execute(filters)[1] expected_values = [ + # Check for JV totals using back calculation logic + [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 0.53, 1000.53], + [si.name, "TCS", 0.075, 1000, 0.52, 1000.52], + ] + self.check_expected_values(result, expected_values) + + def test_single_account_for_multiple_categories(self): + create_tax_category("TDS - 1", rate=10, account="TDS - _TC") + inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1.tax_withholding_category = "TDS - 1" + inv_1.submit() + + create_tax_category("TDS - 2", rate=20, account="TDS - _TC") + inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_2.tax_withholding_category = "TDS - 2" + inv_2.submit() + result = execute( + frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today()) + )[1] + expected_values = [ + [inv_1.name, "TDS - 1", 10, 5000, 500, 5500], + [inv_2.name, "TDS - 2", 20, 5000, 1000, 6000], ] self.check_expected_values(result, expected_values) @@ -41,12 +64,15 @@ def check_expected_values(self, result, expected_values): for i in range(len(result)): voucher = frappe._dict(result[i]) voucher_expected_values = expected_values[i] - self.assertEqual(voucher.ref_no, voucher_expected_values[0]) - self.assertEqual(voucher.section_code, voucher_expected_values[1]) - self.assertEqual(voucher.rate, voucher_expected_values[2]) - self.assertEqual(voucher.base_total, voucher_expected_values[3]) - self.assertEqual(voucher.tax_amount, voucher_expected_values[4]) - self.assertEqual(voucher.grand_total, voucher_expected_values[5]) + voucher_actual_values = ( + voucher.ref_no, + voucher.section_code, + voucher.rate, + voucher.base_total, + voucher.tax_amount, + voucher.grand_total, + ) + self.assertSequenceEqual(voucher_actual_values, voucher_expected_values) def tearDown(self): self.clear_old_entries() @@ -67,24 +93,20 @@ def create_tax_accounts(): ).insert(ignore_if_duplicate=True) -def create_tcs_category(): +def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0): fiscal_year = get_fiscal_year(today(), company="_Test Company") from_date = fiscal_year[1] to_date = fiscal_year[2] - tax_category = create_tax_withholding_category( - category_name="TCS", - rate=0.075, + create_tax_withholding_category( + category_name=category, + rate=rate, from_date=from_date, to_date=to_date, - account="TCS - _TC", - cumulative_threshold=300, + account=account, + cumulative_threshold=cumulative_threshold, ) - customer = frappe.get_doc("Customer", "_Test Customer") - customer.tax_withholding_category = "TCS" - customer.save() - def create_tcs_payment_entry(): payment_entry = create_payment_entry( @@ -109,3 +131,32 @@ def create_tcs_payment_entry(): ) payment_entry.submit() return payment_entry + + +def create_tcs_journal_entry(): + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = "_Test Company" + jv.set( + "accounts", + [ + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "credit_in_account_currency": 10000, + }, + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "debit_in_account_currency": 9992.5, + }, + { + "account": "TCS - _TC", + "debit_in_account_currency": 7.5, + }, + ], + ) + jv.insert() + return jv.submit() From de47e67dfacd7dabb4c935728a9d95df736343c9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:03:38 +0530 Subject: [PATCH 109/138] fix: set rate for PO created against BO (backport #39765) (#39766) * fix: set rate for PO created against BO (cherry picked from commit 0e5b4e5f07d15fe04855f1c836c5412d3644035a) # Conflicts: # erpnext/manufacturing/doctype/blanket_order/blanket_order.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- erpnext/manufacturing/doctype/blanket_order/blanket_order.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 0135a4f9712a..029b4b720a77 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -65,6 +65,7 @@ def update_doc(source_doc, target_doc, source_parent): def update_item(source, target, source_parent): target_qty = source.get("qty") - source.get("ordered_qty") target.qty = target_qty if not flt(target_qty) < 0 else 0 + target.rate = source.get("rate") item = get_item_defaults(target.item_code, source_parent.company) if item: target.item_name = item.get("item_name") @@ -86,6 +87,10 @@ def update_item(source, target, source_parent): }, }, ) + + if target_doc.doctype == "Purchase Order": + target_doc.set_missing_values() + return target_doc From d6a758d1f4c751f7be83197e677638bb0db80596 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 7 Feb 2024 19:12:32 +0530 Subject: [PATCH 110/138] fix: accommodate for default rounding method in v14 --- .../report/tds_payable_monthly/test_tds_payable_monthly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index f36b7ef1906a..742d2f37c45d 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -37,7 +37,7 @@ def test_tax_withholding_for_customers(self): # Check for JV totals using back calculation logic [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 0.52, 1000.52], + [si.name, "TCS", 0.075, 1000.0, 0.53, 1000.53], ] self.check_expected_values(result, expected_values) From df9d52d3ce7155fb7f917383c7d795769aed1c15 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:26:14 +0530 Subject: [PATCH 111/138] fix: incorrect planned qty in PP (backport #39785) (#39792) fix: incorrect planned qty in PP (cherry picked from commit a8ebc94a366e8f15e9bbeab3da064cf5f22dd1b9) Co-authored-by: s-aga-r --- .../doctype/production_plan/production_plan.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c98d639663ac..6ccd11bf9ff0 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -236,9 +236,10 @@ def get_so_items(self): so_item.parent, so_item.item_code, so_item.warehouse, - ( - (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor - ).as_("pending_qty"), + so_item.qty, + so_item.work_order_qty, + so_item.delivered_qty, + so_item.conversion_factor, so_item.description, so_item.name, so_item.bom_no, @@ -261,6 +262,11 @@ def get_so_items(self): items = items_query.run(as_dict=True) + for item in items: + item.pending_qty = ( + flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor + ) + pi = frappe.qb.DocType("Packed Item") packed_items_query = ( From a4e6c388cb7838ad1f3c497c315a0fefa4e10f06 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 7 Feb 2024 18:46:07 +0530 Subject: [PATCH 112/138] fix: remove duplicates from tax category map (cherry picked from commit c56f3a58ab31aaecee2b4007aac53d10f6e73473) --- .../tds_payable_monthly.py | 10 +-- .../test_tds_payable_monthly.py | 89 +++++++++++++++---- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index b96bfbeb8178..ba5cdbe6567c 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -63,16 +63,14 @@ def get_result( tax_amount += entry.credit - entry.debit # infer tax withholding category from the account if it's the single account for this category tax_withholding_category = tds_accounts.get(entry.account) - rate = tax_rate_map.get(tax_withholding_category) # or else the consolidated value from the voucher document if not tax_withholding_category: - # or else from the party default tax_withholding_category = tax_category_map.get(name) - rate = tax_rate_map.get(tax_withholding_category) + # or else from the party default if not tax_withholding_category: tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") - rate = tax_rate_map.get(tax_withholding_category) + rate = tax_rate_map.get(tax_withholding_category) if net_total_map.get(name): if voucher_type == "Journal Entry" and tax_amount and rate: # back calcalute total amount from rate and tax_amount @@ -295,7 +293,7 @@ def get_tds_docs(filters): tds_accounts = {} for tds_acc in _tds_accounts: # if it turns out not to be the only tax withholding category, then don't include in the map - if tds_accounts.get(tds_acc["account"]): + if tds_acc["account"] in tds_accounts: tds_accounts[tds_acc["account"]] = None else: tds_accounts[tds_acc["account"]] = tds_acc["parent"] @@ -408,7 +406,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): "paid_amount_after_tax", "base_paid_amount", ], - "Journal Entry": ["tax_withholding_category", "total_amount"], + "Journal Entry": ["total_amount"], } entries = frappe.get_all( diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index 89ecef1904c0..f36b7ef1906a 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -5,7 +5,6 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import today -from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -22,18 +21,42 @@ def setUp(self): self.create_company() self.clear_old_entries() create_tax_accounts() - create_tcs_category() def test_tax_withholding_for_customers(self): + create_tax_category(cumulative_threshold=300) + frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS") si = create_sales_invoice(rate=1000) pe = create_tcs_payment_entry() + jv = create_tcs_journal_entry() + filters = frappe._dict( company="_Test Company", party_type="Customer", from_date=today(), to_date=today() ) result = execute(filters)[1] expected_values = [ + # Check for JV totals using back calculation logic + [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 0.53, 1000.53], + [si.name, "TCS", 0.075, 1000, 0.52, 1000.52], + ] + self.check_expected_values(result, expected_values) + + def test_single_account_for_multiple_categories(self): + create_tax_category("TDS - 1", rate=10, account="TDS - _TC") + inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_1.tax_withholding_category = "TDS - 1" + inv_1.submit() + + create_tax_category("TDS - 2", rate=20, account="TDS - _TC") + inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True) + inv_2.tax_withholding_category = "TDS - 2" + inv_2.submit() + result = execute( + frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today()) + )[1] + expected_values = [ + [inv_1.name, "TDS - 1", 10, 5000, 500, 5500], + [inv_2.name, "TDS - 2", 20, 5000, 1000, 6000], ] self.check_expected_values(result, expected_values) @@ -41,12 +64,15 @@ def check_expected_values(self, result, expected_values): for i in range(len(result)): voucher = frappe._dict(result[i]) voucher_expected_values = expected_values[i] - self.assertEqual(voucher.ref_no, voucher_expected_values[0]) - self.assertEqual(voucher.section_code, voucher_expected_values[1]) - self.assertEqual(voucher.rate, voucher_expected_values[2]) - self.assertEqual(voucher.base_total, voucher_expected_values[3]) - self.assertEqual(voucher.tax_amount, voucher_expected_values[4]) - self.assertEqual(voucher.grand_total, voucher_expected_values[5]) + voucher_actual_values = ( + voucher.ref_no, + voucher.section_code, + voucher.rate, + voucher.base_total, + voucher.tax_amount, + voucher.grand_total, + ) + self.assertSequenceEqual(voucher_actual_values, voucher_expected_values) def tearDown(self): self.clear_old_entries() @@ -67,24 +93,20 @@ def create_tax_accounts(): ).insert(ignore_if_duplicate=True) -def create_tcs_category(): +def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0): fiscal_year = get_fiscal_year(today(), company="_Test Company") from_date = fiscal_year[1] to_date = fiscal_year[2] - tax_category = create_tax_withholding_category( - category_name="TCS", - rate=0.075, + create_tax_withholding_category( + category_name=category, + rate=rate, from_date=from_date, to_date=to_date, - account="TCS - _TC", - cumulative_threshold=300, + account=account, + cumulative_threshold=cumulative_threshold, ) - customer = frappe.get_doc("Customer", "_Test Customer") - customer.tax_withholding_category = "TCS" - customer.save() - def create_tcs_payment_entry(): payment_entry = create_payment_entry( @@ -109,3 +131,32 @@ def create_tcs_payment_entry(): ) payment_entry.submit() return payment_entry + + +def create_tcs_journal_entry(): + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = "_Test Company" + jv.set( + "accounts", + [ + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "credit_in_account_currency": 10000, + }, + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "debit_in_account_currency": 9992.5, + }, + { + "account": "TCS - _TC", + "debit_in_account_currency": 7.5, + }, + ], + ) + jv.insert() + return jv.submit() From 79479b633fd9255aacddd60de456b7ee55439c01 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 7 Feb 2024 19:12:32 +0530 Subject: [PATCH 113/138] fix: accommodate for default rounding method in v14 (cherry picked from commit d6a758d1f4c751f7be83197e677638bb0db80596) --- .../report/tds_payable_monthly/test_tds_payable_monthly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index f36b7ef1906a..742d2f37c45d 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -37,7 +37,7 @@ def test_tax_withholding_for_customers(self): # Check for JV totals using back calculation logic [jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0], [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], - [si.name, "TCS", 0.075, 1000, 0.52, 1000.52], + [si.name, "TCS", 0.075, 1000.0, 0.53, 1000.53], ] self.check_expected_values(result, expected_values) From 33777a4d4cab5a207d984a040ddf0c3f0b237245 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 16:32:23 +0530 Subject: [PATCH 114/138] refactor: cancel Cr/Dr JE's on Sales/Purchase return cancel (cherry picked from commit 0549535603cae6a7afbd3375c8dd62b517af3c6e) --- erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a3f48141aaca..2420d987c233 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1374,6 +1374,24 @@ def update_against_document_in_jv(self): x.update({dim.fieldname: self.get(dim.fieldname)}) reconcile_against_document(lst, active_dimensions=active_dimensions) + def cancel_system_generated_credit_debit_notes(self): + # Cancel 'Credit/Debit' Note Journal Entries, if found. + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "reference_type": self.doctype, + "reference_name": self.name, + "voucher_type": voucher_type, + "docstatus": 1, + }, + pluck="name", + ) + for x in journals: + frappe.get_doc("Journal Entry", x).cancel() + def on_cancel(self): from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( remove_from_bank_transaction, @@ -1386,6 +1404,8 @@ def on_cancel(self): remove_from_bank_transaction(self.doctype, self.name) if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + self.cancel_system_generated_credit_debit_notes() + # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) From e97f30d8e227fe3455d63873f7c84c1a5e9c000e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:21:29 +0530 Subject: [PATCH 115/138] test: Invoice status on Cr/Dr note cancellation (cherry picked from commit 31a8c3bdc45f0e32a51e43260db8484e5f17aa75) --- .../test_payment_reconciliation.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index d7a73f0ce714..89240ac0d8ad 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -591,6 +591,66 @@ def test_cr_note_against_invoice(self): self.assertEqual(si.status, "Paid") self.assertEqual(si.outstanding_amount, 0) + def test_invoice_status_after_cr_note_cancellation(self): + # This test case is made after the 'always standalone Credit/Debit notes' feature is introduced + transaction_date = nowdate() + amount = 100 + + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + cr_note = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note.is_return = 1 + cr_note.return_against = si.name + cr_note = cr_note.save().submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pr.get_unreconciled_entries() + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 1) + + # assert status outstanding + si.reload() + self.assertEqual(si.status, "Credit Note Issued") + self.assertEqual(si.outstanding_amount, 0) + + cr_note.reload() + cr_note.cancel() + # 'Credit Note' Journal should be auto cancelled + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 0) + def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() amount = 100 From dd7472856837d5eba22c26a3c9e11619cfd8b7be Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:33:55 +0530 Subject: [PATCH 116/138] refactor(test): assert Invoice status as well (cherry picked from commit 33efe0d12d85484e551dc9ebebf8840d427ecc67) --- .../payment_reconciliation/test_payment_reconciliation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 89240ac0d8ad..fb75a0f7caf9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -630,7 +630,7 @@ def test_invoice_status_after_cr_note_cancellation(self): ) self.assertEqual(len(journals), 1) - # assert status outstanding + # assert status and outstanding si.reload() self.assertEqual(si.status, "Credit Note Issued") self.assertEqual(si.outstanding_amount, 0) @@ -650,6 +650,10 @@ def test_invoice_status_after_cr_note_cancellation(self): pluck="name", ) self.assertEqual(len(journals), 0) + # assert status and outstanding + si.reload() + self.assertEqual(si.status, "Unpaid") + self.assertEqual(si.outstanding_amount, 100) def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() From 8d9b5764dd827ae92affbdf5d089a525c21f8fcb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 19:59:33 +0530 Subject: [PATCH 117/138] refactor(test): Forex Credit Note cancellation against Invoice (cherry picked from commit 2f676ced5c712823c5737f40230ec8b1994cd2dd) --- .../controllers/tests/test_accounts_controller.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 5e3077ea8c86..0c9d34d82a22 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1108,18 +1108,18 @@ def test_30_cr_note_against_sales_invoice(self): cr_note.reload() cr_note.cancel() - # Exchange Gain/Loss Journal should've been created. + # with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller + # JE(Credit Note) will be cancelled once the parent is cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 0) self.assertEqual(len(exc_je_for_cr), 0) - # The Credit Note JE is still active and is referencing the sales invoice - # So, outstanding stays the same + # No references, full outstanding si.reload() - self.assertEqual(si.outstanding_amount, 1) - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) def test_40_cost_center_from_payment_entry(self): """ From 596c9fd5076163b82aa35a03caab56c510fd17ca Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:00:18 +0530 Subject: [PATCH 118/138] fix: incorrect planned qty in PP (backport #39785) (backport #39792) (#39795) fix: incorrect planned qty in PP (backport #39785) (#39792) fix: incorrect planned qty in PP (cherry picked from commit a8ebc94a366e8f15e9bbeab3da064cf5f22dd1b9) Co-authored-by: s-aga-r (cherry picked from commit df9d52d3ce7155fb7f917383c7d795769aed1c15) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- .../doctype/production_plan/production_plan.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c98d639663ac..6ccd11bf9ff0 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -236,9 +236,10 @@ def get_so_items(self): so_item.parent, so_item.item_code, so_item.warehouse, - ( - (so_item.qty - so_item.work_order_qty - so_item.delivered_qty) * so_item.conversion_factor - ).as_("pending_qty"), + so_item.qty, + so_item.work_order_qty, + so_item.delivered_qty, + so_item.conversion_factor, so_item.description, so_item.name, so_item.bom_no, @@ -261,6 +262,11 @@ def get_so_items(self): items = items_query.run(as_dict=True) + for item in items: + item.pending_qty = ( + flt(item.qty) - max(item.work_order_qty, item.delivered_qty, 0) * item.conversion_factor + ) + pi = frappe.qb.DocType("Packed Item") packed_items_query = ( From 7413c8b0a9e3ee0c692bcf70502af1899e031129 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 7 Feb 2024 15:31:34 +0000 Subject: [PATCH 119/138] chore(release): Bumped to Version 14.62.1 ## [14.62.1](https://github.com/frappe/erpnext/compare/v14.62.0...v14.62.1) (2024-02-07) ### Bug Fixes * incorrect planned qty in PP (backport [#39785](https://github.com/frappe/erpnext/issues/39785)) (backport [#39792](https://github.com/frappe/erpnext/issues/39792)) ([#39795](https://github.com/frappe/erpnext/issues/39795)) ([596c9fd](https://github.com/frappe/erpnext/commit/596c9fd5076163b82aa35a03caab56c510fd17ca)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 181e366135b4..1b177e1e0cce 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.62.0" +__version__ = "14.62.1" def get_default_company(user=None): From 5c59ab5975229ce36cecd6f655f61a5f246bae34 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 16:32:23 +0530 Subject: [PATCH 120/138] refactor: cancel Cr/Dr JE's on Sales/Purchase return cancel (cherry picked from commit 0549535603cae6a7afbd3375c8dd62b517af3c6e) --- erpnext/controllers/accounts_controller.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a3f48141aaca..2420d987c233 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1374,6 +1374,24 @@ def update_against_document_in_jv(self): x.update({dim.fieldname: self.get(dim.fieldname)}) reconcile_against_document(lst, active_dimensions=active_dimensions) + def cancel_system_generated_credit_debit_notes(self): + # Cancel 'Credit/Debit' Note Journal Entries, if found. + if self.doctype in ["Sales Invoice", "Purchase Invoice"]: + voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "reference_type": self.doctype, + "reference_name": self.name, + "voucher_type": voucher_type, + "docstatus": 1, + }, + pluck="name", + ) + for x in journals: + frappe.get_doc("Journal Entry", x).cancel() + def on_cancel(self): from erpnext.accounts.doctype.bank_transaction.bank_transaction import ( remove_from_bank_transaction, @@ -1386,6 +1404,8 @@ def on_cancel(self): remove_from_bank_transaction(self.doctype, self.name) if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: + self.cancel_system_generated_credit_debit_notes() + # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) From c419c1de06114ef8fa442d49dadb3cbc7dfd8b04 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:21:29 +0530 Subject: [PATCH 121/138] test: Invoice status on Cr/Dr note cancellation (cherry picked from commit 31a8c3bdc45f0e32a51e43260db8484e5f17aa75) --- .../test_payment_reconciliation.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index d7a73f0ce714..89240ac0d8ad 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -591,6 +591,66 @@ def test_cr_note_against_invoice(self): self.assertEqual(si.status, "Paid") self.assertEqual(si.outstanding_amount, 0) + def test_invoice_status_after_cr_note_cancellation(self): + # This test case is made after the 'always standalone Credit/Debit notes' feature is introduced + transaction_date = nowdate() + amount = 100 + + si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date) + + cr_note = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note.is_return = 1 + cr_note.return_against = si.name + cr_note = cr_note.save().submit() + + pr = self.create_payment_reconciliation() + + pr.get_unreconciled_entries() + invoices = [x.as_dict() for x in pr.get("invoices")] + payments = [x.as_dict() for x in pr.get("payments")] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + pr.get_unreconciled_entries() + self.assertEqual(pr.get("invoices"), []) + self.assertEqual(pr.get("payments"), []) + + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 1) + + # assert status outstanding + si.reload() + self.assertEqual(si.status, "Credit Note Issued") + self.assertEqual(si.outstanding_amount, 0) + + cr_note.reload() + cr_note.cancel() + # 'Credit Note' Journal should be auto cancelled + journals = frappe.db.get_all( + "Journal Entry", + filters={ + "is_system_generated": 1, + "docstatus": 1, + "voucher_type": "Credit Note", + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="name", + ) + self.assertEqual(len(journals), 0) + def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() amount = 100 From 129ab38fba27f915c47ccce1eecbcbe29c7d0e12 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 17:33:55 +0530 Subject: [PATCH 122/138] refactor(test): assert Invoice status as well (cherry picked from commit 33efe0d12d85484e551dc9ebebf8840d427ecc67) --- .../payment_reconciliation/test_payment_reconciliation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 89240ac0d8ad..fb75a0f7caf9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -630,7 +630,7 @@ def test_invoice_status_after_cr_note_cancellation(self): ) self.assertEqual(len(journals), 1) - # assert status outstanding + # assert status and outstanding si.reload() self.assertEqual(si.status, "Credit Note Issued") self.assertEqual(si.outstanding_amount, 0) @@ -650,6 +650,10 @@ def test_invoice_status_after_cr_note_cancellation(self): pluck="name", ) self.assertEqual(len(journals), 0) + # assert status and outstanding + si.reload() + self.assertEqual(si.status, "Unpaid") + self.assertEqual(si.outstanding_amount, 100) def test_cr_note_partial_against_invoice(self): transaction_date = nowdate() From 8888ce196df86dc850fe9287cc84611a2aedcced Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 7 Feb 2024 19:59:33 +0530 Subject: [PATCH 123/138] refactor(test): Forex Credit Note cancellation against Invoice (cherry picked from commit 2f676ced5c712823c5737f40230ec8b1994cd2dd) --- .../controllers/tests/test_accounts_controller.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 5e3077ea8c86..0c9d34d82a22 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -1108,18 +1108,18 @@ def test_30_cr_note_against_sales_invoice(self): cr_note.reload() cr_note.cancel() - # Exchange Gain/Loss Journal should've been created. + # with the introduction of 'cancel_system_generated_credit_debit_notes' in accounts controller + # JE(Credit Note) will be cancelled once the parent is cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 0) self.assertEqual(len(exc_je_for_cr), 0) - # The Credit Note JE is still active and is referencing the sales invoice - # So, outstanding stays the same + # No references, full outstanding si.reload() - self.assertEqual(si.outstanding_amount, 1) - self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) def test_40_cost_center_from_payment_entry(self): """ From 0822a2b40af8f2052c037e552e5d8ce8140df75d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 8 Feb 2024 07:28:09 +0000 Subject: [PATCH 124/138] chore(release): Bumped to Version 14.62.2 ## [14.62.2](https://github.com/frappe/erpnext/compare/v14.62.1...v14.62.2) (2024-02-08) ### Bug Fixes * accommodate for default rounding method in v14 ([79479b6](https://github.com/frappe/erpnext/commit/79479b633fd9255aacddd60de456b7ee55439c01)) * remove duplicates from tax category map ([a4e6c38](https://github.com/frappe/erpnext/commit/a4e6c388cb7838ad1f3c497c315a0fefa4e10f06)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 1b177e1e0cce..5b40c6d60fdc 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.62.1" +__version__ = "14.62.2" def get_default_company(user=None): From f79e0d1e37d167ac817c2ef48c35343978c2c3b7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 8 Feb 2024 13:22:55 +0530 Subject: [PATCH 125/138] fix: broken route option in Profitability report --- .../report/profitability_analysis/profitability_analysis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js index 4dbc687366de..1ac52fe5206d 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.js @@ -105,7 +105,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "to_fiscal_year": data.fiscal_year }; - if(data.based_on == 'cost_center'){ + if(data.based_on == 'Cost Center'){ frappe.route_options["cost_center"] = data.account } else { frappe.route_options["project"] = data.account From 2885b8fa44b70270be92706b45b02f36405868ea Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:18:50 +0530 Subject: [PATCH 126/138] fix: do not throw validation for canceled SLE (backport #39769) (#39810) fix: do not throw validation for cancelled sle (cherry picked from commit 32ccf3524ad973b1bc8c5c7eefa4c79f5d8b4444) Co-authored-by: Rohit Waghchaure --- erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 1d52b17149b4..9580e83ed956 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -53,6 +53,9 @@ def validate(self): self.validate_inventory_dimension_negative_stock() def validate_inventory_dimension_negative_stock(self): + if self.is_cancelled: + return + extra_cond = "" kwargs = {} From d6054dbdbda69ac830e6123a3bc5ca220c108d0e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:18:50 +0530 Subject: [PATCH 127/138] fix: do not throw validation for canceled SLE (backport #39769) (#39810) fix: do not throw validation for cancelled sle (cherry picked from commit 32ccf3524ad973b1bc8c5c7eefa4c79f5d8b4444) Co-authored-by: Rohit Waghchaure (cherry picked from commit 2885b8fa44b70270be92706b45b02f36405868ea) --- erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 1d52b17149b4..9580e83ed956 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -53,6 +53,9 @@ def validate(self): self.validate_inventory_dimension_negative_stock() def validate_inventory_dimension_negative_stock(self): + if self.is_cancelled: + return + extra_cond = "" kwargs = {} From 8caf6555294f2799fc8fa89ed14e5c9309b8a0dc Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 8 Feb 2024 09:12:55 +0000 Subject: [PATCH 128/138] chore(release): Bumped to Version 14.62.3 ## [14.62.3](https://github.com/frappe/erpnext/compare/v14.62.2...v14.62.3) (2024-02-08) ### Bug Fixes * do not throw validation for canceled SLE (backport [#39769](https://github.com/frappe/erpnext/issues/39769)) ([#39810](https://github.com/frappe/erpnext/issues/39810)) ([d6054db](https://github.com/frappe/erpnext/commit/d6054dbdbda69ac830e6123a3bc5ca220c108d0e)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5b40c6d60fdc..3847833443ad 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.62.2" +__version__ = "14.62.3" def get_default_company(user=None): From f2d094d1ab8df6af4bd07410f9a1e933e0cce98a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 8 Feb 2024 15:47:14 +0530 Subject: [PATCH 129/138] fix: Handling circular linking while cancelling asset capitalization --- erpnext/assets/doctype/asset/asset.js | 2 +- erpnext/assets/doctype/asset/depreciation.py | 2 +- .../doctype/asset_capitalization/asset_capitalization.js | 1 + .../doctype/asset_capitalization/asset_capitalization.py | 6 +++++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 120ca44cd233..9356484e7a40 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -322,7 +322,7 @@ frappe.ui.form.on('Asset', { }, make_schedules_editable: function(frm) { - if (frm.doc.finance_books.length) { + if (frm.doc.finance_books && frm.doc.finance_books.length) { var is_manual_hence_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 ? true : false; var is_shift_hence_editable = frm.doc.finance_books.filter(d => d.shift_based).length > 0 diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 7eb600025e01..6523713ece00 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -508,7 +508,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset): def reverse_depreciation_entry_made_after_disposal(asset, date): - if not asset.calculate_depreciation: + if not asset.calculate_depreciation or not asset.get("schedules"): return row = -1 diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 67f8421fbde2..e3e57ec87b22 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -7,6 +7,7 @@ frappe.provide("erpnext.assets"); erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { setup() { this.setup_posting_date_time_check(); + this.frm.ignore_doctypes_on_cancel_all = ["Asset Movement"]; } onload() { diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index d1bf15eb4590..9cfa4294fb04 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -77,17 +77,21 @@ def on_cancel(self): "Stock Ledger Entry", "Repost Item Valuation", "Asset", + "Asset Movement" ) self.cancel_target_asset() self.update_stock_ledger() self.make_gl_entries() self.restore_consumed_asset_items() - + def cancel_target_asset(self): if self.entry_type == "Capitalization" and self.target_asset: asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.db_set("capitalized_in", None) if asset_doc.docstatus == 1: asset_doc.cancel() + elif asset_doc.docstatus == 0: + asset_doc.delete() def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code From 79a16bad156803bbf0298b6794b7e1301aad49bc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 11 Feb 2024 17:19:59 +0530 Subject: [PATCH 130/138] fix: set route options to cost center (#37235) fix: set route to cost center cost center mapping is not correct (cherry picked from commit 5e4b73918db809afe0c3880a63537d8e4743f3bf) Co-authored-by: NIYAZ RAZAK <76736615+niyazrazak@users.noreply.github.com> From de6e8c74c5873393385441deb1bffac8006c1f45 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 11 Feb 2024 17:38:37 +0530 Subject: [PATCH 131/138] fix(ux): set rate as price list rate on uom change in MR (backport #39816) (#39817) * fix: add price list rate field in MR Item (cherry picked from commit 61a29eb5fbd0535a4d91f2fd4c4e7fbb0edf6ace) # Conflicts: # erpnext/stock/doctype/material_request_item/material_request_item.py * fix: set rate as price list rate on uom change (cherry picked from commit 5cf0759b0c6c41740b19197aa4e13722a806af97) * chore: linter (cherry picked from commit 1745371cd67939bf2d8bac9e0aac6478332c17de) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../material_request/material_request.js | 20 +++++++++++++++---- .../material_request_item.json | 12 ++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 675a3e978c01..4dc8e6b9cbd3 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -228,9 +228,17 @@ frappe.ui.form.on('Material Request', { const qty_fields = ['actual_qty', 'projected_qty', 'min_order_qty']; if(!r.exc) { - $.each(r.message, function(k, v) { - if(!d[k] || in_list(qty_fields, k)) d[k] = v; + $.each(r.message, function(key, value) { + if(!d[key] || qty_fields.includes(key)) { + d[key] = value; + } }); + + if (d.price_list_rate != r.message.price_list_rate) { + d.price_list_rate = r.message.price_list_rate; + + frappe.model.set_value(d.doctype, d.name, "rate", d.price_list_rate); + } } } }); @@ -432,7 +440,6 @@ frappe.ui.form.on("Material Request Item", { 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); }, item_code: function(frm, doctype, name) { @@ -452,7 +459,12 @@ frappe.ui.form.on("Material Request Item", { set_schedule_date(frm); } } - } + }, + + conversion_factor: function(frm, doctype, name) { + const item = locals[doctype][name]; + frm.events.get_item_data(frm, item, false); + }, }); erpnext.buying.MaterialRequestController = class MaterialRequestController extends erpnext.buying.BuyingController { diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.json b/erpnext/stock/doctype/material_request_item/material_request_item.json index ed4a7e7cf640..d03a356c28a2 100644 --- a/erpnext/stock/doctype/material_request_item/material_request_item.json +++ b/erpnext/stock/doctype/material_request_item/material_request_item.json @@ -35,6 +35,7 @@ "received_qty", "rate_and_amount_section_break", "rate", + "price_list_rate", "col_break3", "amount", "accounting_details_section", @@ -474,13 +475,22 @@ "fieldtype": "Link", "label": "WIP Composite Asset", "options": "Asset" + }, + { + "fieldname": "price_list_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Price List Rate", + "options": "currency", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:37:59.599115", + "modified": "2024-02-08 16:30:56.137858", "modified_by": "Administrator", "module": "Stock", "name": "Material Request Item", From 24386006d695123bbb02f3a976790f7fcffe7635 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 12 Feb 2024 11:44:41 +0530 Subject: [PATCH 132/138] fix: cancel asset capitalization --- .../assets/doctype/asset_capitalization/asset_capitalization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 9cfa4294fb04..a347ece5f5b5 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -90,8 +90,6 @@ def cancel_target_asset(self): asset_doc.db_set("capitalized_in", None) if asset_doc.docstatus == 1: asset_doc.cancel() - elif asset_doc.docstatus == 0: - asset_doc.delete() def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code From 08e02710cda1057846bead48021548a537779fc4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:54:03 +0530 Subject: [PATCH 133/138] perf: cached get_last_purchase_details to fix performance issue (backport #39854) (#39855) perf: cached get_last_purchase_details to fix performance issue (#39854) (cherry picked from commit b966c06a4f7ec1d64e475a626ee934695c77a2a4) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 3 +++ erpnext/stock/doctype/item/item.py | 1 + 2 files changed, 4 insertions(+) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index f6ccb824aea2..bc3ec7f93c25 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -831,6 +831,9 @@ def validate_putaway_capacity(self): "Stock Reconciliation", ) + if not frappe.get_all("Putaway Rule", limit=1): + return + if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0: valid_doctype = False diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 96cec1dd0a2f..ac1b3f278486 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1047,6 +1047,7 @@ def validate_cancelled_item(item_code, docstatus=None): frappe.throw(_("Item {0} is cancelled").format(item_code)) +@frappe.request_cache def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): """returns last purchase details in stock uom""" # get last purchase order item details From a61cffd7c20f7b3d0daf068c013bab09b0266e7a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:17:49 +0530 Subject: [PATCH 134/138] perf: production plan submission (backport #39846) (#39859) perf: production plan submission (cherry picked from commit aa1c69dd7aa7731b699853bafcfc40d19d5ab70a) Co-authored-by: s-aga-r --- .../material_request_plan_item.json | 11 +++++++---- .../doctype/production_plan/production_plan.json | 5 +++-- .../doctype/production_plan/production_plan.py | 8 ++++---- .../manufacturing/doctype/work_order/work_order.json | 5 +++-- .../doctype/work_order_item/work_order_item.json | 5 +++-- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index d07bf0fa66bb..06c1b497551b 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -38,7 +38,8 @@ "in_list_view": 1, "label": "Item Code", "options": "Item", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "item_name", @@ -53,7 +54,8 @@ "in_standard_filter": 1, "label": "For Warehouse", "options": "Warehouse", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "columns": 1, @@ -141,7 +143,8 @@ "fieldname": "from_warehouse", "fieldtype": "Link", "label": "From Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "search_index": 1 }, { "fetch_from": "item_code.safety_stock", @@ -199,7 +202,7 @@ ], "istable": 1, "links": [], - "modified": "2023-09-12 12:09:08.358326", + "modified": "2024-02-11 16:21:11.977018", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 257b60c48694..54c3893928b7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -298,7 +298,8 @@ "no_copy": 1, "options": "\nDraft\nSubmitted\nNot Started\nIn Process\nCompleted\nClosed\nCancelled\nMaterial Requested", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "amended_from", @@ -436,7 +437,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-26 16:31:13.740777", + "modified": "2024-02-11 15:42:47.642481", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 6ccd11bf9ff0..f8c18e8d8bad 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1691,23 +1691,23 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +@frappe.request_cache def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") - query = ( + return ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) .select(table.name) + .distinct() .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) & (child.planned_qty > child.ordered_qty) ) - ).run(as_dict=True) - - return list(set([d.name for d in query])) + ).run(pluck="name") def get_raw_materials_of_sub_assembly_items( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index fb44dfdffbb6..1c9a14927986 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -448,7 +448,8 @@ "no_copy": 1, "options": "Production Plan", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "production_plan_item", @@ -600,7 +601,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-11 18:35:49.852069", + "modified": "2024-02-11 15:47:13.454422", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index f354d45381c4..0f4d693544eb 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -36,7 +36,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "search_index": 1 }, { "fieldname": "source_warehouse", @@ -141,7 +142,7 @@ ], "istable": 1, "links": [], - "modified": "2022-09-28 10:50:43.512562", + "modified": "2024-02-11 15:45:32.318374", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Item", From 8f58b613e4585623087d59585016057b05a71579 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:05:03 +0100 Subject: [PATCH 135/138] fix: calculate `stock_value_diff` `d.item_tax_amount` is already in base currency. (cherry picked from commit 5df585179812c9f6f4b8f9593c24ef29529c8258) --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 274073b638ea..bf5f9f4d1d47 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -572,9 +572,7 @@ def make_divisional_loss_gl_entry(item, outgoing_amount): ) stock_value_diff = ( - flt(d.base_net_amount) - + flt(d.item_tax_amount / self.conversion_rate) - + flt(d.landed_cost_voucher_amount) + flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount) ) elif warehouse_account.get(d.warehouse): stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse) From d0b9c568d3645ffa53dfe3e6589c5f2e3b6eff30 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:57:48 +0530 Subject: [PATCH 136/138] fix: landed cost voucher not submitting because of incorrect reference (backport #39898) (#39899) fix: landed cost voucher not submitting because of incorrect reference (#39898) (cherry picked from commit 6239fd704b7d7a60c54b8042a8cc83b5c9e75eab) Co-authored-by: rohitwaghchaure --- erpnext/controllers/buying_controller.py | 4 ++-- .../landed_cost_voucher.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index a2b7a65fce07..4a627696032b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -190,8 +190,8 @@ def set_landed_cost_voucher_amount(self): lc_voucher_data = frappe.db.sql( """select sum(applicable_charges), cost_center from `tabLanded Cost Item` - where docstatus = 1 and purchase_receipt_item = %s""", - d.name, + where docstatus = 1 and purchase_receipt_item = %s and receipt_document = %s""", + (d.name, self.name), ) d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0 if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]: diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index c73dc65f8a8e..11a001ccc70a 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -38,6 +38,7 @@ def get_items_from_purchase_receipts(self): def validate(self): self.check_mandatory() self.validate_receipt_documents() + self.validate_line_items() init_landed_taxes_and_totals(self) self.set_total_taxes_and_charges() if not self.get("items"): @@ -45,6 +46,26 @@ def validate(self): self.set_applicable_charges_on_item() + def validate_line_items(self): + for d in self.get("items"): + if ( + d.docstatus == 0 + and d.purchase_receipt_item + and not frappe.db.exists( + d.receipt_document_type + " Item", + {"name": d.purchase_receipt_item, "parent": d.receipt_document}, + ) + ): + frappe.throw( + _("Row {0}: {2} Item {1} does not exist in {2} {3}").format( + d.idx, + frappe.bold(d.purchase_receipt_item), + d.receipt_document_type, + frappe.bold(d.receipt_document), + ), + title=_("Incorrect Reference Document (Purchase Receipt Item)"), + ) + def check_mandatory(self): if not self.get("purchase_receipts"): frappe.throw(_("Please enter Receipt Document")) From ab7e323648169e5115b05b28b66d5b6859ff3925 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:28:50 +0530 Subject: [PATCH 137/138] fix: production plan issue with sales order (backport #39901) (#39903) fix: production plan issue with sales order (#39901) (cherry picked from commit d0df5df4a609a04d98ee97c9e56931672143587d) Co-authored-by: rohitwaghchaure --- .../manufacturing/doctype/production_plan/production_plan.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f8c18e8d8bad..aea6c9871770 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -576,7 +576,10 @@ def get_production_items(self): "project": self.project, } - key = (d.item_code, d.sales_order, d.warehouse) + key = (d.item_code, d.sales_order, d.sales_order_item, d.warehouse) + if self.combine_items: + key = (d.item_code, d.sales_order, d.warehouse) + if not d.sales_order: key = (d.name, d.item_code, d.warehouse) From 22ace5cb5a5b2350df685587f20b3a7b948c5548 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 14 Feb 2024 12:06:19 +0000 Subject: [PATCH 138/138] chore(release): Bumped to Version 14.62.4 ## [14.62.4](https://github.com/frappe/erpnext/compare/v14.62.3...v14.62.4) (2024-02-14) ### Bug Fixes * production plan issue with sales order (backport [#39901](https://github.com/frappe/erpnext/issues/39901)) ([#39903](https://github.com/frappe/erpnext/issues/39903)) ([ab7e323](https://github.com/frappe/erpnext/commit/ab7e323648169e5115b05b28b66d5b6859ff3925)) * accommodate for default rounding method in v14 ([d6a758d](https://github.com/frappe/erpnext/commit/d6a758d1f4c751f7be83197e677638bb0db80596)) * broken route option in Profitability report ([f79e0d1](https://github.com/frappe/erpnext/commit/f79e0d1e37d167ac817c2ef48c35343978c2c3b7)) * calculate `stock_value_diff` ([8f58b61](https://github.com/frappe/erpnext/commit/8f58b613e4585623087d59585016057b05a71579)) * cancel asset capitalization ([2438600](https://github.com/frappe/erpnext/commit/24386006d695123bbb02f3a976790f7fcffe7635)) * do not throw validation for canceled SLE (backport [#39769](https://github.com/frappe/erpnext/issues/39769)) ([#39810](https://github.com/frappe/erpnext/issues/39810)) ([2885b8f](https://github.com/frappe/erpnext/commit/2885b8fa44b70270be92706b45b02f36405868ea)) * Handling circular linking while cancelling asset capitalization ([f2d094d](https://github.com/frappe/erpnext/commit/f2d094d1ab8df6af4bd07410f9a1e933e0cce98a)) * incorrect planned qty in PP (backport [#39785](https://github.com/frappe/erpnext/issues/39785)) ([#39792](https://github.com/frappe/erpnext/issues/39792)) ([df9d52d](https://github.com/frappe/erpnext/commit/df9d52d3ce7155fb7f917383c7d795769aed1c15)) * landed cost voucher not submitting because of incorrect reference (backport [#39898](https://github.com/frappe/erpnext/issues/39898)) ([#39899](https://github.com/frappe/erpnext/issues/39899)) ([d0b9c56](https://github.com/frappe/erpnext/commit/d0b9c568d3645ffa53dfe3e6589c5f2e3b6eff30)) * remove duplicates from tax category map ([c56f3a5](https://github.com/frappe/erpnext/commit/c56f3a58ab31aaecee2b4007aac53d10f6e73473)) * set rate for PO created against BO (backport [#39765](https://github.com/frappe/erpnext/issues/39765)) ([#39766](https://github.com/frappe/erpnext/issues/39766)) ([de47e67](https://github.com/frappe/erpnext/commit/de47e67dfacd7dabb4c935728a9d95df736343c9)) * set route options to cost center ([#37235](https://github.com/frappe/erpnext/issues/37235)) ([79a16ba](https://github.com/frappe/erpnext/commit/79a16bad156803bbf0298b6794b7e1301aad49bc)) * **ux:** set rate as price list rate on uom change in MR (backport [#39816](https://github.com/frappe/erpnext/issues/39816)) ([#39817](https://github.com/frappe/erpnext/issues/39817)) ([de6e8c7](https://github.com/frappe/erpnext/commit/de6e8c74c5873393385441deb1bffac8006c1f45)) ### Performance Improvements * cached get_last_purchase_details to fix performance issue (backport [#39854](https://github.com/frappe/erpnext/issues/39854)) ([#39855](https://github.com/frappe/erpnext/issues/39855)) ([08e0271](https://github.com/frappe/erpnext/commit/08e02710cda1057846bead48021548a537779fc4)) * production plan submission (backport [#39846](https://github.com/frappe/erpnext/issues/39846)) ([#39859](https://github.com/frappe/erpnext/issues/39859)) ([a61cffd](https://github.com/frappe/erpnext/commit/a61cffd7c20f7b3d0daf068c013bab09b0266e7a)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 3847833443ad..a36b9c32636f 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.62.3" +__version__ = "14.62.4" def get_default_company(user=None):