From a65bc77b0295747ffffb092f5e37fec458dc9234 Mon Sep 17 00:00:00 2001 From: Richard Case Date: Mon, 30 Oct 2023 00:15:05 +0000 Subject: [PATCH 01/24] feat: in_party_currency option for AR/AP reports --- .../accounts_payable/accounts_payable.js | 6 ++++- .../accounts_receivable.js | 7 ++++-- .../accounts_receivable.py | 22 +++++++++++-------- 3 files changed, 23 insertions(+), 12 deletions(-) mode change 100755 => 100644 erpnext/accounts/report/accounts_receivable/accounts_receivable.py diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 0979cffbf3..6e2981909d 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -153,8 +153,12 @@ frappe.query_reports["Accounts Payable"] = { "fieldname": "ignore_accounts", "label": __("Group by Voucher"), "fieldtype": "Check", + }, + { + "fieldname": "in_party_currency", + "label": __("In Party Currency"), + "fieldtype": "Check", } - ], "formatter": function(value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index d6e3098e17..dcc4381bae 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -185,9 +185,12 @@ frappe.query_reports["Accounts Receivable"] = { "fieldname": "ignore_accounts", "label": __("Group by Voucher"), "fieldtype": "Check", + }, + { + "fieldname": "in_party_currency", + "label": __("In Party Currency"), + "fieldtype": "Check", } - - ], "formatter": function(value, row, column, data, default_formatter) { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py old mode 100755 new mode 100644 index 6a9f952bfd..eaf9f421e1 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision, get_party_types_from_ # 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters # 7. For overpayment against an invoice with payment terms, there will be an additional row # 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated -# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party -# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" +# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency +# 10. This report is based on Payment Ledger Entries def execute(filters=None): @@ -82,6 +82,9 @@ class ReceivablePayableReport(object): self.total_row_map = {} self.skip_total_row = 1 + if self.filters.get("in_party_currency"): + self.skip_total_row = 1 + def get_data(self): self.get_ple_entries() self.get_sales_invoices_or_customers_based_on_sales_person() @@ -143,7 +146,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.init_subtotal_row(ple.party) - if self.filters.get("group_by_party"): + if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"): self.init_subtotal_row("Total") def get_invoices(self, ple): @@ -222,8 +225,7 @@ class ReceivablePayableReport(object): if not row: return - # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency"): amount = ple.amount_in_account_currency else: amount = ple.amount @@ -254,8 +256,10 @@ class ReceivablePayableReport(object): def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) - for field in self.get_currency_fields(): - total_row[field] += row.get(field, 0.0) + if total_row: + for field in self.get_currency_fields(): + total_row[field] += row.get(field, 0.0) + total_row["currency"] = row.get("currency", "") def append_subtotal_row(self, party): sub_total_row = self.total_row_map.get(party) @@ -316,7 +320,7 @@ class ReceivablePayableReport(object): if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get("Total")) + self.data.append(self.total_row_map.get("Total", {})) def append_row(self, row): self.allocate_future_payments(row) @@ -447,7 +451,7 @@ class ReceivablePayableReport(object): party_details = self.get_party_details(row.party) or {} row.update(party_details) - if self.filters.get("party_type") and self.filters.get("party"): + if self.filters.get("in_party_currency"): row.currency = row.account_currency else: row.currency = self.company_currency From dc9b4de976faf217b3472b0506cd0c87c0403058 Mon Sep 17 00:00:00 2001 From: Richard Case Date: Mon, 30 Oct 2023 00:45:02 +0000 Subject: [PATCH 02/24] chore: update tests --- .../accounts/report/accounts_payable/test_accounts_payable.py | 1 + .../report/accounts_receivable/test_accounts_receivable.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 9f03d92cd5..b4cb25ff1b 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } data = execute(filters) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index f83285a1a7..dd0842df04 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -581,6 +581,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "range2": 60, "range3": 90, "range4": 120, + "in_party_currency": 1, } si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) From d75ac136d7a8a66df2965bb9bb33b3134eb4cae5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 12:32:35 +0530 Subject: [PATCH 03/24] fix: typerror on tree doctypes - Item Group, Customer Group, Supplier Group and Territory (backport #38870) (#38871) fix: typerror on tree doctypes - Item Group, Customer Group, Supplier Group and Territory (#38870) * refactor: typerror on set_root_readonly * refactor: remove 'cur_frm' usage in supplier_group * refactor: remove 'cur_frm' usage in territory.js * refactor: remove 'cur_frm' from sales_person.js (cherry picked from commit 6d5bdc6c68dfda915972f3747de752b8fad7cdb5) Co-authored-by: ruthra kumar --- .../doctype/customer_group/customer_group.js | 29 +++++++++---------- .../doctype/sales_person/sales_person.js | 23 +++++++-------- .../doctype/supplier_group/supplier_group.js | 27 ++++++++--------- erpnext/setup/doctype/territory/territory.js | 27 +++++++++-------- 4 files changed, 48 insertions(+), 58 deletions(-) diff --git a/erpnext/setup/doctype/customer_group/customer_group.js b/erpnext/setup/doctype/customer_group/customer_group.js index 3c81b0283c..e3528189dc 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.js +++ b/erpnext/setup/doctype/customer_group/customer_group.js @@ -1,21 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - cur_frm.cscript.set_root_readonly(doc); -} - -cur_frm.cscript.set_root_readonly = function(doc) { - // read-only for root customer group - if(!doc.parent_customer_group && !doc.__islocal) { - cur_frm.set_read_only(); - cur_frm.set_intro(__("This is a root customer group and cannot be edited.")); - } else { - cur_frm.set_intro(null); - } -} - frappe.ui.form.on("Customer Group", { setup: function(frm){ frm.set_query('parent_customer_group', function (doc) { @@ -48,5 +33,17 @@ frappe.ui.form.on("Customer Group", { } } }); - } + }, + refresh: function(frm) { + frm.trigger("set_root_readonly"); + }, + set_root_readonly: function(frm) { + // read-only for root customer group + if(!frm.doc.parent_customer_group && !frm.doc.__islocal) { + frm.set_read_only(); + frm.set_intro(__("This is a root customer group and cannot be edited.")); + } else { + frm.set_intro(null); + } + }, }); diff --git a/erpnext/setup/doctype/sales_person/sales_person.js b/erpnext/setup/doctype/sales_person/sales_person.js index d86a8f3d98..f0d9aa87bc 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.js +++ b/erpnext/setup/doctype/sales_person/sales_person.js @@ -11,6 +11,7 @@ frappe.ui.form.on('Sales Person', { frm.dashboard.add_indicator(__('Total Contribution Amount Against Invoices: {0}', [format_currency(info.allocated_amount_against_invoice, info.currency)]), 'blue'); } + frm.trigger("set_root_readonly"); }, setup: function(frm) { @@ -27,22 +28,18 @@ frappe.ui.form.on('Sales Person', { 'Sales Order': () => frappe.new_doc("Sales Order") .then(() => frm.add_child("sales_team", {"sales_person": frm.doc.name})) } + }, + set_root_readonly: function(frm) { + // read-only for root + if(!frm.doc.parent_sales_person && !frm.doc.__islocal) { + frm.set_read_only(); + frm.set_intro(__("This is a root sales person and cannot be edited.")); + } else { + frm.set_intro(null); + } } }); -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - cur_frm.cscript.set_root_readonly(doc); -} - -cur_frm.cscript.set_root_readonly = function(doc) { - // read-only for root - if(!doc.parent_sales_person && !doc.__islocal) { - cur_frm.set_read_only(); - cur_frm.set_intro(__("This is a root sales person and cannot be edited.")); - } else { - cur_frm.set_intro(null); - } -} //get query select sales person cur_frm.fields_dict['parent_sales_person'].get_query = function(doc, cdt, cdn) { diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.js b/erpnext/setup/doctype/supplier_group/supplier_group.js index 33629297ff..c697a99cb4 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.js +++ b/erpnext/setup/doctype/supplier_group/supplier_group.js @@ -1,21 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -cur_frm.cscript.refresh = function(doc) { - cur_frm.set_intro(doc.__islocal ? "" : __("There is nothing to edit.")); - cur_frm.cscript.set_root_readonly(doc); -}; - -cur_frm.cscript.set_root_readonly = function(doc) { - // read-only for root customer group - if(!doc.parent_supplier_group && !doc.__islocal) { - cur_frm.set_read_only(); - cur_frm.set_intro(__("This is a root supplier group and cannot be edited.")); - } else { - cur_frm.set_intro(null); - } -}; - frappe.ui.form.on("Supplier Group", { setup: function(frm){ frm.set_query('parent_supplier_group', function (doc) { @@ -48,5 +33,17 @@ frappe.ui.form.on("Supplier Group", { } } }); + }, + refresh: function(frm) { + frm.set_intro(frm.doc.__islocal ? "" : __("There is nothing to edit.")); + frm.trigger("set_root_readonly"); + }, + set_root_readonly: function(frm) { + if(!frm.doc.parent_supplier_group && !frm.doc.__islocal) { + frm.trigger("set_read_only"); + frm.set_intro(__("This is a root supplier group and cannot be edited.")); + } else { + frm.set_intro(null); + } } }); diff --git a/erpnext/setup/doctype/territory/territory.js b/erpnext/setup/doctype/territory/territory.js index 3caf814c90..e11d20b7bf 100644 --- a/erpnext/setup/doctype/territory/territory.js +++ b/erpnext/setup/doctype/territory/territory.js @@ -11,23 +11,22 @@ frappe.ui.form.on("Territory", { } } }; + }, + refresh: function(frm) { + frm.trigger("set_root_readonly"); + }, + set_root_readonly: function(frm) { + // read-only for root territory + if(!frm.doc.parent_territory && !frm.doc.__islocal) { + frm.set_read_only(); + frm.set_intro(__("This is a root territory and cannot be edited.")); + } else { + frm.set_intro(null); + } } + }); -cur_frm.cscript.refresh = function(doc, cdt, cdn) { - cur_frm.cscript.set_root_readonly(doc); -} - -cur_frm.cscript.set_root_readonly = function(doc) { - // read-only for root territory - if(!doc.parent_territory && !doc.__islocal) { - cur_frm.set_read_only(); - cur_frm.set_intro(__("This is a root territory and cannot be edited.")); - } else { - cur_frm.set_intro(null); - } -} - //get query select territory cur_frm.fields_dict['parent_territory'].get_query = function(doc,cdt,cdn) { return{ From 32d3d4e571307f1ef4a9bafae84e730077879ba9 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 18 Dec 2023 18:03:53 +0000 Subject: [PATCH 04/24] fix: use party account currency when party account is specified (cherry picked from commit c7b961ffa27c611a1e0a45750b38f1f23b0b0c7f) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index eb7b2f8fb4..53097afb17 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -225,7 +225,7 @@ class ReceivablePayableReport(object): if not row: return - if self.filters.get("in_party_currency"): + if self.filters.get("in_party_currency") or self.filters.get("party_account"): amount = ple.amount_in_account_currency else: amount = ple.amount @@ -455,7 +455,7 @@ class ReceivablePayableReport(object): party_details = self.get_party_details(row.party) or {} row.update(party_details) - if self.filters.get("in_party_currency"): + if self.filters.get("in_party_currency") or self.filters.get("party_account"): row.currency = row.account_currency else: row.currency = self.company_currency From 2d9a0a8e2ebc02d97334fde8fdf314e25735216c Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 18 Dec 2023 19:09:25 +0000 Subject: [PATCH 05/24] fix(test): expect account currency when party account is specified. (cherry picked from commit a09241e3c763882a0a0e06b21ccaa0b06f60bc75) --- .../report/accounts_receivable/test_accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index fbfaed6dfd..976935b99f 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -579,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): filters.update({"party_account": self.debtors_usd}) report = execute(filters)[1] self.assertEqual(len(report), 1) - expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency] + expected_data = [100.0, 100.0, self.debtors_usd, si2.currency] row = report[0] self.assertEqual( expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] From 648f2757972409a4b5608266e753e9e50ae263c9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:58:38 +0530 Subject: [PATCH 06/24] perf: use estimated rows instead of actual rows (backport #38830) (#38876) perf: use estimated rows instead of actual rows (#38830) (cherry picked from commit 9983283f95753e7523cf30cc258df0572f88081d) Co-authored-by: Ankush Menat --- .../batch_wise_balance_history.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 176a21566a..7f2608e0fb 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate +from frappe.utils import cint, flt, get_table_name, getdate from frappe.utils.deprecations import deprecated from pypika import functions as fn @@ -13,11 +13,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter SLE_COUNT_LIMIT = 10_000 +def _estimate_table_row_count(doctype: str): + table = get_table_name(doctype) + return cint( + frappe.db.sql( + f"""select table_rows + from information_schema.tables + where table_name = '{table}' ;""" + )[0][0] + ) + + def execute(filters=None): if not filters: filters = {} - sle_count = frappe.db.count("Stock Ledger Entry") + sle_count = _estimate_table_row_count("Stock Ledger Entry") if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) From e3be9c1da409b39f5130708424560019bfec6cc9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:34:27 +0530 Subject: [PATCH 07/24] fix: local reference error in BOM (backport #38850) (#38877) fix: local reference error in BOM (#38850) fix: local reference error (cherry picked from commit ae353398d96746e9e89640b586d1bbd6afbbce77) Co-authored-by: NIYAZ RAZAK <76736615+niyazrazak@users.noreply.github.com> --- erpnext/manufacturing/doctype/bom/bom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index aa583c364a..575376a39a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1017,6 +1017,8 @@ def get_bom_item_rate(args, bom_doc): item_doc = frappe.get_cached_doc("Item", args.get("item_code")) price_list_data = get_price_list_rate(bom_args, item_doc) rate = price_list_data.price_list_rate + elif bom_doc.rm_cost_as_per == "Manual": + return return flt(rate) From 58f1df50047abc1e0084ce389678adbe85919067 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:47:48 +0530 Subject: [PATCH 08/24] feat: total_asset_cost field (backport #38879) (#38887) * feat: total_asset_cost field (#38879) (cherry picked from commit d370c60a6c840be23ec4094593b9bbf1d1dca88b) # Conflicts: # erpnext/patches.txt * chore: resolve conflicts * chore: remove unnecessary patch --------- Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.json | 11 ++++++++++- erpnext/assets/doctype/asset/asset.py | 1 + .../assets/doctype/asset_repair/asset_repair.py | 6 ++++++ erpnext/patches.txt | 1 + .../v14_0/update_total_asset_cost_field.py | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v14_0/update_total_asset_cost_field.py diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 540a4f5549..ea72b3cf84 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -35,6 +35,7 @@ "purchase_receipt", "purchase_invoice", "available_for_use_date", + "total_asset_cost", "column_break_23", "gross_purchase_amount", "asset_quantity", @@ -529,6 +530,14 @@ "label": "Capitalized In", "options": "Asset Capitalization", "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus > 0", + "fieldname": "total_asset_cost", + "fieldtype": "Currency", + "label": "Total Asset Cost", + "options": "Company:company:default_currency", + "read_only": 1 } ], "idx": 72, @@ -572,7 +581,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2023-11-20 20:57:37.010467", + "modified": "2023-12-20 16:50:21.128595", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 5fb2d36178..2d0b3ac11c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -111,6 +111,7 @@ class Asset(AccountsController): "Decapitalized", ] supplier: DF.Link | None + total_asset_cost: DF.Currency total_number_of_depreciations: DF.Int value_after_depreciation: DF.Currency # end: auto-generated types diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index c0fb3c2923..67bf66cf1a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -93,6 +93,9 @@ class AssetRepair(AccountsController): self.increase_asset_value() + if self.capitalize_repair_cost: + self.asset_doc.total_asset_cost += self.repair_cost + if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() self.decrease_stock_quantity() @@ -128,6 +131,9 @@ class AssetRepair(AccountsController): self.decrease_asset_value() + if self.capitalize_repair_cost: + self.asset_doc.total_asset_cost -= self.repair_cost + if self.get("stock_consumption"): self.increase_stock_quantity() if self.get("capitalize_repair_cost"): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5df11d21ab..2b02562fa7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -350,6 +350,7 @@ erpnext.patches.v15_0.set_reserved_stock_in_bin erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation erpnext.patches.v14_0.update_zero_asset_quantity_field execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction") +erpnext.patches.v14_0.update_total_asset_cost_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index diff --git a/erpnext/patches/v14_0/update_total_asset_cost_field.py b/erpnext/patches/v14_0/update_total_asset_cost_field.py new file mode 100644 index 0000000000..57cf71b613 --- /dev/null +++ b/erpnext/patches/v14_0/update_total_asset_cost_field.py @@ -0,0 +1,17 @@ +import frappe + + +def execute(): + asset = frappe.qb.DocType("Asset") + frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run() + + asset_repair_list = frappe.db.get_all( + "Asset Repair", + filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1}, + fields=["asset", "repair_cost"], + ) + + for asset_repair in asset_repair_list: + frappe.qb.update(asset).set( + asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost + ).where(asset.name == asset_repair.asset).run() From 74606dc927067f5fc500fcd9af797e188711b2b0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:44:20 +0530 Subject: [PATCH 09/24] fix: allow to set rate manually for service item in BOM (backport #38880) (backport #38882) (#38885) fix: allow to set rate manually for service item in BOM (backport #38880) (#38882) fix: allow to set rate manually for service item in BOM (#38880) (cherry picked from commit c2f692a4e4f3dd5089fe4949c6cd74574282fdb1) Co-authored-by: rohitwaghchaure (cherry picked from commit a6ab53236e08cb57fc8a45496ee4c9ccfbc53e10) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++ erpnext/manufacturing/doctype/bom/test_bom.py | 29 +++++++++++++++++++ .../doctype/bom_item/bom_item.json | 13 +++++++-- erpnext/patches.txt | 1 + .../v14_0/set_maintain_stock_for_bom_item.py | 19 ++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 575376a39a..8cb024209c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -744,6 +744,9 @@ class BOM(WebsiteGenerator): base_total_rm_cost = 0 for d in self.get("items"): + if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate": + continue + old_rate = d.rate if self.rm_cost_as_per != "Manual": d.rate = self.get_rm_rate( diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 051b475bcc..2debf9191e 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -698,6 +698,35 @@ class TestBOM(FrappeTestCase): bom.update_cost() self.assertFalse(bom.flags.cost_updated) + def test_bom_with_service_item_cost(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 1000.0}).name + + service_item = make_item(properties={"is_stock_item": 0}).name + + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item, service_item], do_not_save=True) + bom.rm_cost_as_per = "Valuation Rate" + + for row in bom.items: + if row.item_code == service_item: + row.rate = 566.00 + else: + row.rate = 800.00 + + bom.save() + + for row in bom.items: + if row.item_code == service_item: + self.assertEqual(row.is_stock_item, 0) + self.assertEqual(row.rate, 566.00) + else: + self.assertEqual(row.is_stock_item, 1) + def test_do_not_include_manufacturing_and_fixed_items(self): from erpnext.manufacturing.doctype.bom.bom import item_query diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index cb58af1f29..dfd6612098 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -14,6 +14,7 @@ "bom_no", "source_warehouse", "allow_alternative_item", + "is_stock_item", "section_break_5", "description", "col_break1", @@ -185,7 +186,7 @@ "in_list_view": 1, "label": "Rate", "options": "currency", - "read_only": 1, + "read_only_depends_on": "eval:doc.is_stock_item == 1", "reqd": 1 }, { @@ -284,13 +285,21 @@ "fieldname": "do_not_explode", "fieldtype": "Check", "label": "Do Not Explode" + }, + { + "default": "0", + "fetch_from": "item_code.is_stock_item", + "fieldname": "is_stock_item", + "fieldtype": "Check", + "label": "Is Stock Item", + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-14 18:35:51.378513", + "modified": "2023-12-20 16:21:55.477883", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2b02562fa7..f76220537b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -354,3 +354,4 @@ erpnext.patches.v14_0.update_total_asset_cost_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index +erpnext.patches.v14_0.set_maintain_stock_for_bom_item \ No newline at end of file diff --git a/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py new file mode 100644 index 0000000000..f0b618f32d --- /dev/null +++ b/erpnext/patches/v14_0/set_maintain_stock_for_bom_item.py @@ -0,0 +1,19 @@ +import frappe + + +def execute(): + if not frappe.db.exists("BOM", {"docstatus": 1}): + return + + # Added is_stock_item to handle Read Only based on condition for the rate field + frappe.db.sql( + """ + UPDATE + `tabBOM Item` boi, + `tabItem` i + SET + boi.is_stock_item = i.is_stock_item + WHERE + boi.item_code = i.name + """ + ) From bf98a8f85522d7c9807bf8b4969f8c2d620077a1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 21 Dec 2023 12:13:33 +0530 Subject: [PATCH 10/24] fix: typeerror on pos order summary to new order screen (cherry picked from commit 6a0a08b59c5b1eadbfdc657a131531d0e7461b11) --- erpnext/selling/page/point_of_sale/pos_item_cart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 193048f676..bd8579203c 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -520,7 +520,7 @@ erpnext.PointOfSale.ItemCart = class { } render_taxes(taxes) { - if (taxes.length) { + if (taxes && taxes.length) { const currency = this.events.get_frm().doc.currency; const taxes_html = taxes.map(t => { if (t.tax_amount_after_discount_amount == 0.0) return; From f6eb2b521d71b61c3ddad5194ff94e0ed3d7ba95 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:22:53 +0530 Subject: [PATCH 11/24] fix: do not reset the basic rate for the material receipt stock entry (backport #38896) (#38899) fix: do not reset the basic rate for the material receipt stock entry (#38896) (cherry picked from commit 98bfcc4c758b61f714c53ea0f00246731a30fdaf) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/stock_entry/stock_entry.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 7af5d1aa37..8da3e8fdd0 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -512,7 +512,12 @@ frappe.ui.form.on('Stock Entry', { }, callback: function(r) { if (!r.exc) { - ["actual_qty", "basic_rate"].forEach((field) => { + let fields = ["actual_qty", "basic_rate"]; + if (frm.doc.purpose == "Material Receipt") { + fields = ["actual_qty"]; + } + + fields.forEach((field) => { frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0)); }); frm.events.calculate_basic_amount(frm, child); From eabb956acadfef7f69e72a160a86e4963d6e7393 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:44:50 +0530 Subject: [PATCH 12/24] =?UTF-8?q?fix:=20reposting=20not=20fixing=20valuati?= =?UTF-8?q?on=20rate=20for=20sales=20return=20using=20movin=E2=80=A6=20(ba?= =?UTF-8?q?ckport=20#38895)=20(backport=20#38897)=20(#38901)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: reposting not fixing valuation rate for sales return using movin… (backport #38895) (#38897) * fix: reposting not fixing valuation rate for sales return using movin… (#38895) fix: reposting not fixing valuation rate for sales return using moving average method (cherry picked from commit 3a668bbe9694fdd6e8265869c6943e42f889ac41) # Conflicts: # erpnext/stock/stock_ledger.py * chore: fix conflicts --------- Co-authored-by: rohitwaghchaure (cherry picked from commit 07175367d8158355e5f699c7c66a2a6743a85c29) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- erpnext/controllers/selling_controller.py | 4 +- .../delivery_note/test_delivery_note.py | 53 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 34 +++++++++--- 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 4489d60131..919e459c9e 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -438,7 +438,9 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get("stock_qty") or d.get("actual_qty")) - if not d.incoming_rate: + if not d.incoming_rate or ( + get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return") + ): d.incoming_rate = get_incoming_rate( { "item_code": d.item_code, diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index da8ee022f9..933be53b07 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1425,6 +1425,59 @@ class TestDeliveryNote(FrappeTestCase): self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0) + def test_sales_return_valuation_for_moving_average_case2(self): + # Make DN return + # Make Bakcdated Purchase Receipt and check DN return valuation rate + # The rate should be recalculate based on the backdated purchase receipt + frappe.flags.print_debug_messages = False + item_code = make_item( + "_Test Item Sales Return with MA Case2", + {"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"}, + ).name + + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=100.0, + posting_date=add_days(nowdate(), -5), + ) + + dn = create_delivery_note( + item_code=item_code, + warehouse="_Test Warehouse - _TC", + qty=5, + rate=500, + posting_date=add_days(nowdate(), -4), + ) + + returned_dn = create_delivery_note( + is_return=1, + item_code=item_code, + return_against=dn.name, + qty=-5, + rate=500, + company=dn.company, + warehouse="_Test Warehouse - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + posting_date=add_days(nowdate(), -1), + ) + + self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0) + + # Make backdated purchase receipt + make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=5, + basic_rate=200.0, + posting_date=add_days(nowdate(), -3), + ) + + returned_dn.reload() + self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0) + def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9142a27f4c..b19a34a354 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -25,6 +25,7 @@ from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry impor ) from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, + get_incoming_rate, get_or_make_bin, get_stock_balance, get_valuation_method, @@ -841,14 +842,33 @@ class update_entries_after(object): get_rate_for_return, # don't move this import to top ) - rate = get_rate_for_return( - sle.voucher_type, - sle.voucher_no, - sle.item_code, - voucher_detail_no=sle.voucher_detail_no, - sle=sle, - ) + if self.valuation_method == "Moving Average": + rate = get_incoming_rate( + { + "item_code": sle.item_code, + "warehouse": sle.warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + "qty": sle.actual_qty, + "serial_no": sle.get("serial_no"), + "batch_no": sle.get("batch_no"), + "serial_and_batch_bundle": sle.get("serial_and_batch_bundle"), + "company": sle.company, + "voucher_type": sle.voucher_type, + "voucher_no": sle.voucher_no, + "allow_zero_valuation": self.allow_zero_rate, + "sle": sle.name, + } + ) + else: + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no From a99470e0177b818bd2b75ff4f44de04605f0e3b0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:55:41 +0530 Subject: [PATCH 13/24] chore: additional_asset_cost field (backport #38904) (#38906) chore: additional_asset_cost field (#38904) (cherry picked from commit 283763dfb2affa6a0b7bb29e19123c3e1fb27f30) Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.json | 11 ++++++++++- erpnext/assets/doctype/asset/asset.py | 2 ++ erpnext/assets/doctype/asset_repair/asset_repair.py | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index ea72b3cf84..ac712d4431 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -36,6 +36,7 @@ "purchase_invoice", "available_for_use_date", "total_asset_cost", + "additional_asset_cost", "column_break_23", "gross_purchase_amount", "asset_quantity", @@ -538,6 +539,14 @@ "label": "Total Asset Cost", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus > 0", + "fieldname": "additional_asset_cost", + "fieldtype": "Currency", + "label": "Additional Asset Cost", + "options": "Company:company:default_currency", + "read_only": 1 } ], "idx": 72, @@ -581,7 +590,7 @@ "link_fieldname": "target_asset" } ], - "modified": "2023-12-20 16:50:21.128595", + "modified": "2023-12-21 16:46:20.732869", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2d0b3ac11c..4b4579b461 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -50,6 +50,7 @@ class Asset(AccountsController): from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook + additional_asset_cost: DF.Currency amended_from: DF.Link | None asset_category: DF.Link | None asset_name: DF.Data @@ -145,6 +146,7 @@ class Asset(AccountsController): ).format(asset_depr_schedules_links) ) + self.total_asset_cost = self.gross_purchase_amount self.status = self.get_status() def on_submit(self): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 67bf66cf1a..10d36e6d48 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -95,6 +95,7 @@ class AssetRepair(AccountsController): if self.capitalize_repair_cost: self.asset_doc.total_asset_cost += self.repair_cost + self.asset_doc.additional_asset_cost += self.repair_cost if self.get("stock_consumption"): self.check_for_stock_items_and_warehouse() @@ -133,6 +134,7 @@ class AssetRepair(AccountsController): if self.capitalize_repair_cost: self.asset_doc.total_asset_cost -= self.repair_cost + self.asset_doc.additional_asset_cost -= self.repair_cost if self.get("stock_consumption"): self.increase_stock_quantity() From 308c6ffb4fb09169ff7541e14057fb179b4c3aee Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 21:39:20 +0530 Subject: [PATCH 14/24] perf: Drop unused/duplicate/sub-optimal indexes (backport #38884) (#38913) perf: Drop unused/duplicate/sub-optimal indexes (#38884) * ci: enable more checks * perf: Drop unused/duplicate indexes (cherry picked from commit 787333896c3710d16b1fa72432db0fe59c74dfa8) Co-authored-by: Ankush Menat --- .pre-commit-config.yaml | 6 +++- .../accounts/doctype/gl_entry/gl_entry.json | 8 ++---- .../purchase_invoice/purchase_invoice.py | 4 --- .../doctype/sales_invoice/sales_invoice.py | 4 --- .../purchase_order_item.json | 3 +- erpnext/patches.txt | 4 +-- erpnext/stock/doctype/bin/bin.json | 3 +- .../drop_unused_return_against_index.py | 28 +++++++++++++------ erpnext/tests/test_perf.py | 24 ++++++++++++++++ 9 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 erpnext/tests/test_perf.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30be903ae8..6ea121f298 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.3.0 hooks: - id: trailing-whitespace files: "erpnext.*" @@ -15,6 +15,10 @@ repos: args: ['--branch', 'develop'] - id: check-merge-conflict - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements - repo: https://github.com/pre-commit/mirrors-eslint rev: v8.44.0 diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index 5063ec6076..de2a9db2c8 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -142,8 +142,7 @@ "label": "Against Voucher Type", "oldfieldname": "against_voucher_type", "oldfieldtype": "Data", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "against_voucher", @@ -162,8 +161,7 @@ "label": "Voucher Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "DocType", - "search_index": 1 + "options": "DocType" }, { "fieldname": "voucher_no", @@ -321,4 +319,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7156fa2682..db33271bcc 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1816,10 +1816,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None): return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) -def on_doctype_update(): - frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"]) - - @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index aa6daa7d42..6aba1faa84 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2549,10 +2549,6 @@ def get_loyalty_programs(customer): return lp_details -def on_doctype_update(): - frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"]) - - @frappe.whitelist() def create_invoice_discounting(source_name, target_doc=None): invoice = frappe.get_doc("Sales Invoice", source_name) 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 98c1b388c1..5a24cc2e92 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -123,8 +123,7 @@ "oldfieldname": "item_code", "oldfieldtype": "Link", "options": "Item", - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "fieldname": "supplier_part_no", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f76220537b..4825e7648f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -353,5 +353,5 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency" erpnext.patches.v14_0.update_total_asset_cost_field # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger -erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index -erpnext.patches.v14_0.set_maintain_stock_for_bom_item \ No newline at end of file +erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 +erpnext.patches.v14_0.set_maintain_stock_for_bom_item diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 312470d50e..10d9511357 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -52,8 +52,7 @@ "oldfieldtype": "Link", "options": "Item", "read_only": 1, - "reqd": 1, - "search_index": 1 + "reqd": 1 }, { "default": "0.00", diff --git a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py index 8fe4ffb58f..cc29e67fa7 100644 --- a/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py +++ b/erpnext/stock/doctype/delivery_note/patches/drop_unused_return_against_index.py @@ -1,15 +1,27 @@ +import click import frappe +UNUSED_INDEXES = [ + ("Delivery Note", ["customer", "is_return", "return_against"]), + ("Sales Invoice", ["customer", "is_return", "return_against"]), + ("Purchase Invoice", ["supplier", "is_return", "return_against"]), + ("Purchase Receipt", ["supplier", "is_return", "return_against"]), +] + def execute(): - """Drop unused return_against index""" + for doctype, index_fields in UNUSED_INDEXES: + table = f"tab{doctype}" + index_name = frappe.db.get_index_name(index_fields) + drop_index_if_exists(table, index_name) + + +def drop_index_if_exists(table: str, index: str): + if not frappe.db.has_index(table, index): + return try: - frappe.db.sql_ddl( - "ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`" - ) - frappe.db.sql_ddl( - "ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`" - ) + frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") + click.echo(f"✓ dropped {index} index from {table}") except Exception: - frappe.log_error("Failed to drop unused index") + frappe.log_error("Failed to drop index") diff --git a/erpnext/tests/test_perf.py b/erpnext/tests/test_perf.py new file mode 100644 index 0000000000..fc17b1dcbd --- /dev/null +++ b/erpnext/tests/test_perf.py @@ -0,0 +1,24 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +INDEXED_FIELDS = { + "Bin": ["item_code"], + "GL Entry": ["voucher_type", "against_voucher_type"], + "Purchase Order Item": ["item_code"], + "Stock Ledger Entry": ["warehouse"], +} + + +class TestPerformance(FrappeTestCase): + def test_ensure_indexes(self): + # These fields are not explicitly indexed BUT they are prefix in some + # other composite index. If those are removed this test should be + # updated accordingly. + for doctype, fields in INDEXED_FIELDS.items(): + for field in fields: + self.assertTrue( + frappe.db.sql( + f"""SHOW INDEX FROM `tab{doctype}` + WHERE Column_name = "{field}" AND Seq_in_index = 1""" + ) + ) From a8f3f2343d3bcf0538469be08179b2bb3eb8d67b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 07:41:00 +0530 Subject: [PATCH 15/24] chore: fixup broken JSON files (backport #38915) (#38917) chore: fixup broken JSON files (#38915) (cherry picked from commit 2dc49c834a0cab620465fafa555d239092107864) Co-authored-by: Ankush Menat --- .../unverified/at_austria_chart_template.json | 69 ++++--- .../doctype/journal_entry/test_records.json | 187 +++++++++--------- erpnext/setup/demo_data/journal_entry.json | 30 +-- 3 files changed, 141 insertions(+), 145 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json b/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json index 58d67beb67..bd7228ec41 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/unverified/at_austria_chart_template.json @@ -26,7 +26,7 @@ "0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"}, "0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"}, "0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"}, - "0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"}, + "0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"}, "0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"}, "0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"}, "0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"}, @@ -65,42 +65,41 @@ "0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"}, "0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"}, "root_type": "Asset" - }, + }, "Klasse 1 Aktiva: Vorr\u00e4te": { "1000 Bezugsverrechnung": {"account_type": "Stock"}, "1100 Rohstoffe": {"account_type": "Stock"}, "1200 Bezogene Teile": {"account_type": "Stock"}, "1300 Hilfsstoffe": {"account_type": "Stock"}, "1350 Betriebsstoffe": {"account_type": "Stock"}, - "1360 Vorrat Energietraeger": {"account_type": "Stock"}, + "1360 Vorrat Energietraeger": {"account_type": "Stock"}, "1400 Unfertige Erzeugnisse": {"account_type": "Stock"}, "1500 Fertige Erzeugnisse": {"account_type": "Stock"}, "1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"}, "1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"}, - "1900 Wertberichtigungen": {"account_type": "Stock"}, "1800 Geleistete Anzahlungen": {"account_type": "Stock"}, "1900 Wertberichtigungen": {"account_type": "Stock"}, "root_type": "Asset" - }, + }, "Klasse 3 Passiva: Verbindlichkeiten": { "3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"}, "3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"}, "3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"}, - "3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"}, + "3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"}, "3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"}, "3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"}, - "3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"}, + "3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"}, "3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": { "account_type": "Payable" }, "3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {}, "3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"}, "3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"}, - "3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"}, - "3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"}, - "3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"}, + "3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"}, + "3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"}, + "3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"}, "3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"}, - "3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"}, + "3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"}, "3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"}, "3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"}, "3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"}, @@ -119,13 +118,13 @@ }, "3515 Umsatzsteuer Inland 10%": { "account_type": "Tax" - }, + }, "3520 Umsatzsteuer aus i.g. Erwerb 20%": { "account_type": "Tax" }, "3525 Umsatzsteuer aus i.g. Erwerb 10%": { "account_type": "Tax" - }, + }, "3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {}, "3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": { "account_type": "Payable" @@ -141,7 +140,7 @@ "account_type": "Tax" }, "root_type": "Liability" - }, + }, "Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": { "2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": { "account_type": "Receivable" @@ -154,7 +153,7 @@ }, "2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": { "account_type": "Receivable" - }, + }, "2100 Forderungen aus Lieferungen und Leistungen EU": { "account_type": "Receivable" }, @@ -192,7 +191,7 @@ "account_type": "Receivable" }, "2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"}, - + "2460 Eingeforderte aber noch nicht eingezahlte Einlagen": { "account_type": "Receivable" }, @@ -243,10 +242,10 @@ }, "2800 Guthaben bei Bank": { "account_type": "Bank" - }, + }, "2801 Guthaben bei Bank - Sparkonto": { "account_type": "Bank" - }, + }, "2810 Guthaben bei Paypal": { "account_type": "Bank" }, @@ -264,19 +263,19 @@ }, "2895 Schwebende Geldbewegugen": { "account_type": "Bank" - }, + }, "2513 Vorsteuer Inland 5%": { "account_type": "Tax" }, "2515 Vorsteuer Inland 20%": { "account_type": "Tax" - }, + }, "2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": { "account_type": "Tax" }, "2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": { "account_type": "Tax" - }, + }, "2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": { "account_type": "Tax" }, @@ -286,16 +285,16 @@ "root_type": "Asset" }, "Klasse 4: Betriebliche Erträge": { - "4000 Erlöse 20 %": {"account_type": "Income Account"}, - "4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"}, + "4000 Erlöse 20 %": {"account_type": "Income Account"}, + "4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"}, "4010 Erl\u00f6se 10 %": {"account_type": "Income Account"}, - "4030 Erl\u00f6se 13 %": {"account_type": "Income Account"}, - "4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"}, - "4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"}, + "4030 Erl\u00f6se 13 %": {"account_type": "Income Account"}, + "4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"}, + "4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"}, "4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"}, "4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"}, - "4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"}, - "4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"}, + "4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"}, + "4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"}, "4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"}, "4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"}, "4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"}, @@ -304,15 +303,15 @@ "4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"}, "4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"}, "root_type": "Income" - }, + }, "Klasse 5: Aufwand f\u00fcr Material und Leistungen": { - "5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"}, + "5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"}, "5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"}, "5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"}, "5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"}, "5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"}, "5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"}, - "5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"}, + "5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"}, "5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"}, "5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"}, "5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"}, @@ -340,7 +339,7 @@ "6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"}, "6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"}, "root_type": "Expense" - }, + }, "Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": { "7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"}, "7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"}, @@ -349,7 +348,7 @@ "7310 Fahrrad - Aufwand": {"account_type": "Expense Account"}, "7320 Kfz - Aufwand": {"account_type": "Expense Account"}, "7330 LKW - Aufwand": {"account_type": "Expense Account"}, - "7340 Lastenrad - Aufwand": {"account_type": "Expense Account"}, + "7340 Lastenrad - Aufwand": {"account_type": "Expense Account"}, "7350 Reise- und Fahraufwand": {"account_type": "Expense Account"}, "7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"}, "7380 Nachrichtenaufwand": {"account_type": "Expense Account"}, @@ -409,7 +408,7 @@ "8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"}, "8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"}, "root_type": "Income" - }, + }, "Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": { "9000 Gezeichnetes bzw. gewidmetes Kapital": { "account_type": "Equity" @@ -435,5 +434,5 @@ }, "root_type": "Equity" } - } + } } diff --git a/erpnext/accounts/doctype/journal_entry/test_records.json b/erpnext/accounts/doctype/journal_entry/test_records.json index dafcf56abd..717c579c7a 100644 --- a/erpnext/accounts/doctype/journal_entry/test_records.json +++ b/erpnext/accounts/doctype/journal_entry/test_records.json @@ -1,97 +1,94 @@ [ - { - "cheque_date": "2013-03-14", - "cheque_no": "33", - "company": "_Test Company", - "doctype": "Journal Entry", - "accounts": [ - { - "account": "Debtors - _TC", - "party_type": "Customer", - "party": "_Test Customer", - "credit_in_account_currency": 400.0, - "debit_in_account_currency": 0.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "_Test Cost Center - _TC" - }, - { - "account": "_Test Bank - _TC", - "credit_in_account_currency": 0.0, - "debit_in_account_currency": 400.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "_Test Cost Center - _TC" - } - ], - "naming_series": "_T-Journal Entry-", - "posting_date": "2013-02-14", - "user_remark": "test", - "voucher_type": "Bank Entry" - }, - - - { - "cheque_date": "2013-02-14", - "cheque_no": "33", - "company": "_Test Company", - "doctype": "Journal Entry", - "accounts": [ - { - "account": "_Test Payable - _TC", - "party_type": "Supplier", - "party": "_Test Supplier", - "credit_in_account_currency": 0.0, - "debit_in_account_currency": 400.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "_Test Cost Center - _TC" - }, - { - "account": "_Test Bank - _TC", - "credit_in_account_currency": 400.0, - "debit_in_account_currency": 0.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "_Test Cost Center - _TC" - } - ], - "naming_series": "_T-Journal Entry-", - "posting_date": "2013-02-14", - "user_remark": "test", - "voucher_type": "Bank Entry" - }, - - - { - "cheque_date": "2013-02-14", - "cheque_no": "33", - "company": "_Test Company", - "doctype": "Journal Entry", - "accounts": [ - { - "account": "Debtors - _TC", - "party_type": "Customer", - "party": "_Test Customer", - "credit_in_account_currency": 0.0, - "debit_in_account_currency": 400.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "_Test Cost Center - _TC" - }, - { - "account": "Sales - _TC", - "cost_center": "_Test Cost Center - _TC", - "credit_in_account_currency": 400.0, - "debit_in_account_currency": 0.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - "cost_center": "_Test Cost Center - _TC" - } - ], - "naming_series": "_T-Journal Entry-", - "posting_date": "2013-02-14", - "user_remark": "test", - "voucher_type": "Bank Entry" - } + { + "cheque_date": "2013-03-14", + "cheque_no": "33", + "company": "_Test Company", + "doctype": "Journal Entry", + "accounts": [ + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "credit_in_account_currency": 400.0, + "debit_in_account_currency": 0.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts", + "cost_center": "_Test Cost Center - _TC" + }, + { + "account": "_Test Bank - _TC", + "credit_in_account_currency": 0.0, + "debit_in_account_currency": 400.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts", + "cost_center": "_Test Cost Center - _TC" + } + ], + "naming_series": "_T-Journal Entry-", + "posting_date": "2013-02-14", + "user_remark": "test", + "voucher_type": "Bank Entry" + }, + + { + "cheque_date": "2013-02-14", + "cheque_no": "33", + "company": "_Test Company", + "doctype": "Journal Entry", + "accounts": [ + { + "account": "_Test Payable - _TC", + "party_type": "Supplier", + "party": "_Test Supplier", + "credit_in_account_currency": 0.0, + "debit_in_account_currency": 400.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts", + "cost_center": "_Test Cost Center - _TC" + }, + { + "account": "_Test Bank - _TC", + "credit_in_account_currency": 400.0, + "debit_in_account_currency": 0.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts", + "cost_center": "_Test Cost Center - _TC" + } + ], + "naming_series": "_T-Journal Entry-", + "posting_date": "2013-02-14", + "user_remark": "test", + "voucher_type": "Bank Entry" + }, + + { + "cheque_date": "2013-02-14", + "cheque_no": "33", + "company": "_Test Company", + "doctype": "Journal Entry", + "accounts": [ + { + "account": "Debtors - _TC", + "party_type": "Customer", + "party": "_Test Customer", + "credit_in_account_currency": 0.0, + "debit_in_account_currency": 400.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts", + "cost_center": "_Test Cost Center - _TC" + }, + { + "account": "Sales - _TC", + "credit_in_account_currency": 400.0, + "debit_in_account_currency": 0.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts", + "cost_center": "_Test Cost Center - _TC" + } + ], + "naming_series": "_T-Journal Entry-", + "posting_date": "2013-02-14", + "user_remark": "test", + "voucher_type": "Bank Entry" + } ] diff --git a/erpnext/setup/demo_data/journal_entry.json b/erpnext/setup/demo_data/journal_entry.json index b751c7cf24..a681be4f5b 100644 --- a/erpnext/setup/demo_data/journal_entry.json +++ b/erpnext/setup/demo_data/journal_entry.json @@ -4,22 +4,22 @@ "cheque_no": "33", "doctype": "Journal Entry", "accounts": [ - { - "party_type": "Customer", - "party": "ABC Enterprises", - "credit_in_account_currency": 40000.0, - "debit_in_account_currency": 0.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - }, - { - "credit_in_account_currency": 0.0, - "debit_in_account_currency": 40000.0, - "doctype": "Journal Entry Account", - "parentfield": "accounts", - } + { + "party_type": "Customer", + "party": "ABC Enterprises", + "credit_in_account_currency": 40000.0, + "debit_in_account_currency": 0.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts" + }, + { + "credit_in_account_currency": 0.0, + "debit_in_account_currency": 40000.0, + "doctype": "Journal Entry Account", + "parentfield": "accounts" + } ], "user_remark": "test", "voucher_type": "Bank Entry" } -] \ No newline at end of file +] From 8c2c90f77a42acbfa21fd5ca5391770d101ed6ff Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 15:50:07 +0530 Subject: [PATCH 16/24] feat: provision to add items in Stock Reservation dialog (backport #38558) (#38920) * feat: provision to update items in Stock Reservation dialog (cherry picked from commit 9471d8fff94f84a627957c5ff2c7f8f1ed53a098) * fix(ux): show row index and field label while selecting the Sales Order Item (cherry picked from commit 00261094c8ce0663f953b73ae17a5e7f3f494b59) * feat: provision to add items in Stock Reservation dialog (cherry picked from commit 8d5045ef4caf695dbfdc087b78c8f68a79976813) --------- Co-authored-by: s-aga-r --- erpnext/controllers/queries.py | 28 +++++++ .../doctype/sales_order/sales_order.js | 78 +++++++++++++++++-- 2 files changed, 101 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 199732b152..e858820965 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -891,3 +891,31 @@ def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, as_list=1, ) return terms + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters) -> list: + table = frappe.qb.DocType(doctype) + query = ( + frappe.qb.from_(table) + .select( + table.name, + Concat("#", table.idx, ", ", table.item_code), + ) + .orderby(table.idx) + .offset(start) + .limit(page_len) + ) + + if filters: + for field, value in filters.items(): + query = query.where(table[field] == value) + + if txt: + txt += "%" + query = query.where( + ((table.idx.like(txt.replace("#", ""))) | (table.item_code.like(txt))) | (table.name.like(txt)) + ) + + return query.run(as_dict=False) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 97b214e33e..b206e3fe33 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -182,7 +182,7 @@ frappe.ui.form.on("Sales Order", { create_stock_reservation_entries(frm) { const dialog = new frappe.ui.Dialog({ title: __("Stock Reservation"), - size: "large", + size: "extra-large", fields: [ { fieldname: "set_warehouse", @@ -207,6 +207,50 @@ frappe.ui.form.on("Sales Order", { }, }, {fieldtype: "Column Break"}, + { + fieldname: "add_item", + fieldtype: "Link", + label: __("Add Item"), + options: "Sales Order Item", + get_query: () => { + return { + query: "erpnext.controllers.queries.get_filtered_child_rows", + filters: { + "parenttype": frm.doc.doctype, + "parent": frm.doc.name, + "reserve_stock": 1, + } + } + }, + onchange: () => { + let sales_order_item = dialog.get_value("add_item"); + + if (sales_order_item) { + frm.doc.items.forEach(item => { + if (item.name === sales_order_item) { + let unreserved_qty = (flt(item.stock_qty) - (item.stock_reserved_qty ? flt(item.stock_reserved_qty) : (flt(item.delivered_qty) * flt(item.conversion_factor)))) / flt(item.conversion_factor); + + if (unreserved_qty > 0) { + dialog.fields_dict.items.df.data.forEach((row) => { + if (row.sales_order_item === sales_order_item) { + unreserved_qty -= row.qty_to_reserve; + } + }); + } + + dialog.fields_dict.items.df.data.push({ + 'sales_order_item': item.name, + 'item_code': item.item_code, + 'warehouse': dialog.get_value("set_warehouse") || item.warehouse, + 'qty_to_reserve': Math.max(unreserved_qty, 0) + }); + dialog.fields_dict.items.grid.refresh(); + dialog.set_value("add_item", undefined); + } + }); + } + }, + }, {fieldtype: "Section Break"}, { fieldname: "items", @@ -218,10 +262,34 @@ frappe.ui.form.on("Sales Order", { fields: [ { fieldname: "sales_order_item", - fieldtype: "Data", + fieldtype: "Link", label: __("Sales Order Item"), + options: "Sales Order Item", reqd: 1, - read_only: 1, + in_list_view: 1, + get_query: () => { + return { + query: "erpnext.controllers.queries.get_filtered_child_rows", + filters: { + "parenttype": frm.doc.doctype, + "parent": frm.doc.name, + "reserve_stock": 1, + } + } + }, + onchange: (event) => { + if (event) { + let name = $(event.currentTarget).closest(".grid-row").attr("data-name"); + let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc; + + frm.doc.items.forEach(item => { + if (item.name === item_row.sales_order_item) { + item_row.item_code = item.item_code; + } + }); + dialog.fields_dict.items.grid.refresh(); + } + } }, { fieldname: "item_code", @@ -284,14 +352,14 @@ frappe.ui.form.on("Sales Order", { frm.doc.items.forEach(item => { if (item.reserve_stock) { - let unreserved_qty = (flt(item.stock_qty) - (item.stock_reserved_qty ? flt(item.stock_reserved_qty) : (flt(item.delivered_qty) * flt(item.conversion_factor)))) + let unreserved_qty = (flt(item.stock_qty) - (item.stock_reserved_qty ? flt(item.stock_reserved_qty) : (flt(item.delivered_qty) * flt(item.conversion_factor)))) / flt(item.conversion_factor); if (unreserved_qty > 0) { dialog.fields_dict.items.df.data.push({ 'sales_order_item': item.name, 'item_code': item.item_code, 'warehouse': item.warehouse, - 'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor)) + 'qty_to_reserve': unreserved_qty }); } } From 8f643f0df85be91654a1cc27f4c81e0125c887f7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 16:45:40 +0530 Subject: [PATCH 17/24] fix: `Reserved Stock` report (backport #38922) (#38924) * chore: improve `Allowed Qty` error msg (cherry picked from commit 1a1629196d0daedef3db4f73191e21321689306d) * fix: `Reserved Stock` report (cherry picked from commit a5d5223c0e7f85a64f70288ec6e0048864b2ffd7) --------- Co-authored-by: s-aga-r --- .../stock_reservation_entry.py | 47 +++++++++++------- .../report/reserved_stock/reserved_stock.js | 48 ++++++++++--------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 85550c2b7d..fee0e0ce93 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -9,7 +9,7 @@ from frappe.model.document import Document from frappe.query_builder.functions import Sum from frappe.utils import cint, flt -from erpnext.stock.utils import get_or_make_bin +from erpnext.stock.utils import get_or_make_bin, get_stock_balance class StockReservationEntry(Document): @@ -151,7 +151,7 @@ class StockReservationEntry(Document): """Validates `Reserved Qty` when `Reservation Based On` is `Qty`.""" if self.reservation_based_on == "Qty": - self.validate_with_max_reserved_qty(self.reserved_qty) + self.validate_with_allowed_qty(self.reserved_qty) def auto_reserve_serial_and_batch(self, based_on: str = None) -> None: """Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`.""" @@ -324,7 +324,7 @@ class StockReservationEntry(Document): frappe.throw(msg) # Should be called after validating Serial and Batch Nos. - self.validate_with_max_reserved_qty(qty_to_be_reserved) + self.validate_with_allowed_qty(qty_to_be_reserved) self.db_set("reserved_qty", qty_to_be_reserved) def update_reserved_qty_in_voucher( @@ -429,7 +429,7 @@ class StockReservationEntry(Document): msg = _("Stock Reservation Entry cannot be updated as it has been delivered.") frappe.throw(msg) - def validate_with_max_reserved_qty(self, qty_to_be_reserved: float) -> None: + def validate_with_allowed_qty(self, qty_to_be_reserved: float) -> None: """Validates `Reserved Qty` with `Max Reserved Qty`.""" self.db_set( @@ -448,12 +448,12 @@ class StockReservationEntry(Document): ) voucher_delivered_qty = flt(delivered_qty) * flt(conversion_factor) - max_reserved_qty = min( + allowed_qty = min( self.available_qty, (self.voucher_qty - voucher_delivered_qty - total_reserved_qty) ) - if max_reserved_qty <= 0 and self.voucher_type == "Sales Order": - msg = _("Item {0} is already delivered for Sales Order {1}.").format( + if self.get("_action") != "submit" and self.voucher_type == "Sales Order" and allowed_qty <= 0: + msg = _("Item {0} is already reserved/delivered against Sales Order {1}.").format( frappe.bold(self.item_code), frappe.bold(self.voucher_no) ) @@ -463,19 +463,33 @@ class StockReservationEntry(Document): else: frappe.throw(msg) - if qty_to_be_reserved > max_reserved_qty: + if qty_to_be_reserved > allowed_qty: + actual_qty = get_stock_balance(self.item_code, self.warehouse) msg = """ - Cannot reserve more than Max Reserved Qty {0} {1}.

- The Max Reserved Qty is calculated as follows:
+ Cannot reserve more than Allowed Qty {0} {1} for Item {2} against {3} {4}.

+ The Allowed Qty is calculated as follows:
    -
  • Available Qty To Reserve = (Actual Stock Qty - Reserved Stock Qty)
  • -
  • Voucher Qty = Voucher Item Qty
  • -
  • Delivered Qty = Qty delivered against the Voucher Item
  • -
  • Total Reserved Qty = Qty reserved against the Voucher Item
  • -
  • Max Reserved Qty = Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))
  • +
  • Actual Qty [Available Qty at Warehouse] = {5}
  • +
  • Reserved Stock [Ignore current SRE] = {6}
  • +
  • Available Qty To Reserve [Actual Qty - Reserved Stock] = {7}
  • +
  • Voucher Qty [Voucher Item Qty] = {8}
  • +
  • Delivered Qty [Qty delivered against the Voucher Item] = {9}
  • +
  • Total Reserved Qty [Qty reserved against the Voucher Item] = {10}
  • +
  • Allowed Qty [Minimum of (Available Qty To Reserve, (Voucher Qty - Delivered Qty - Total Reserved Qty))] = {11}
""".format( - frappe.bold(max_reserved_qty), self.stock_uom + frappe.bold(allowed_qty), + self.stock_uom, + frappe.bold(self.item_code), + self.voucher_type, + frappe.bold(self.voucher_no), + actual_qty, + actual_qty - self.available_qty, + self.available_qty, + self.voucher_qty, + voucher_delivered_qty, + total_reserved_qty, + allowed_qty, ) frappe.throw(msg) @@ -509,7 +523,6 @@ def get_available_qty_to_reserve( """Returns `Available Qty to Reserve (Actual Qty - Reserved Qty)` for Item, Warehouse and Batch combination.""" from erpnext.stock.doctype.batch.batch import get_batch_qty - from erpnext.stock.utils import get_stock_balance if batch_no: return get_batch_qty( diff --git a/erpnext/stock/report/reserved_stock/reserved_stock.js b/erpnext/stock/report/reserved_stock/reserved_stock.js index 68727411d5..2b075e2276 100644 --- a/erpnext/stock/report/reserved_stock/reserved_stock.js +++ b/erpnext/stock/report/reserved_stock/reserved_stock.js @@ -149,34 +149,36 @@ frappe.query_reports["Reserved Stock"] = { formatter: (value, row, column, data, default_formatter) => { value = default_formatter(value, row, column, data); - if (column.fieldname == "status") { - switch (data.status) { - case "Partially Reserved": - value = "" + value + ""; - break; - case "Reserved": - value = "" + value + ""; - break; - case "Partially Delivered": - value = "" + value + ""; - break; - case "Delivered": - value = "" + value + ""; - break; + if (data) { + if (column.fieldname == "status") { + switch (data.status) { + case "Partially Reserved": + value = "" + value + ""; + break; + case "Reserved": + value = "" + value + ""; + break; + case "Partially Delivered": + value = "" + value + ""; + break; + case "Delivered": + value = "" + value + ""; + break; + } } - } - else if (column.fieldname == "delivered_qty") { - if (data.delivered_qty > 0) { - if (data.reserved_qty > data.delivered_qty) { - value = "" + value + ""; + else if (column.fieldname == "delivered_qty") { + if (data.delivered_qty > 0) { + if (data.reserved_qty > data.delivered_qty) { + value = "" + value + ""; + } + else { + value = "" + value + ""; + } } else { - value = "" + value + ""; + value = "" + value + ""; } } - else { - value = "" + value + ""; - } } return value; From 82960e33124b4264dbb8512c23484cc7f945d4e5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 17:55:58 +0530 Subject: [PATCH 18/24] fix: reset the incoming rate on changing of the warehouse (backport #38925) (#38926) fix: reset the incoming rate on changing of the warehouse (#38925) (cherry picked from commit 161ae1edd1ebcafd14d7a302ad1adde238e43426) Co-authored-by: rohitwaghchaure --- erpnext/public/js/utils/sales_common.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 084cca7db5..b92b02e826 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -184,6 +184,12 @@ erpnext.sales_common = { refresh_field("incentives",row.name,row.parentfield); } + warehouse(doc, cdt, cdn) { + if (doc.docstatus === 0 && doc.is_return && !doc.return_against) { + frappe.model.set_value(cdt, cdn, "incoming_rate", 0.0); + } + } + toggle_editable_price_list_rate() { var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name); var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate")); From e0dbb573b1d64f002aca81df2287717fad94657d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 20 Dec 2023 17:22:30 +0530 Subject: [PATCH 19/24] fix: incorrect price list in customer-wise item price report (cherry picked from commit 9a00edb03115b72afb4274e6fcf2dc7d5b431657) --- .../customer_wise_item_price.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py index a58f40362b..40aa9acc3c 100644 --- a/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py +++ b/erpnext/selling/report/customer_wise_item_price/customer_wise_item_price.py @@ -3,11 +3,11 @@ import frappe -from frappe import _ +from frappe import _, qb +from frappe.query_builder import Criterion from erpnext import get_default_company from erpnext.accounts.party import get_party_details -from erpnext.stock.get_item_details import get_price_list_rate_for def execute(filters=None): @@ -50,6 +50,42 @@ def get_columns(filters=None): ] +def fetch_item_prices( + customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None +): + price_list_map = frappe._dict() + ip = qb.DocType("Item Price") + and_conditions = [] + or_conditions = [] + if items: + and_conditions.append(ip.item_code.isin([x.item_code for x in items])) + and_conditions.append(ip.selling == True) + + or_conditions.append(ip.customer == None) + or_conditions.append(ip.price_list == None) + + if customer: + or_conditions.append(ip.customer == customer) + + if price_list: + or_conditions.append(ip.price_list == price_list) + + if selling_price_list: + or_conditions.append(ip.price_list == selling_price_list) + + res = ( + qb.from_(ip) + .select(ip.item_code, ip.price_list, ip.price_list_rate) + .where(Criterion.all(and_conditions)) + .where(Criterion.any(or_conditions)) + .run(as_dict=True) + ) + for x in res: + price_list_map.update({(x.item_code, x.price_list): x.price_list_rate}) + + return price_list_map + + def get_data(filters=None): data = [] customer_details = get_customer_details(filters) @@ -59,9 +95,17 @@ def get_data(filters=None): "Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code" ) item_stock_map = {item.item_code: item.available for item in item_stock_map} + price_list_map = fetch_item_prices( + customer_details.customer, + customer_details.price_list, + customer_details.selling_price_list, + items, + ) for item in items: - price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0 + price_list_rate = price_list_map.get( + (item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0 + ) available_stock = item_stock_map.get(item.item_code) data.append( From f704ccbb59d1dd00ef4c4f58282f0c6530b79fa3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 14:45:05 +0530 Subject: [PATCH 20/24] fix(ux): make PR and PI Item rate field readonly based on `Maintain Same Rate` (backport #38942) (#38944) * fix(ux): make PI Item rate field editable (cherry picked from commit eb5bb9f9a99e54be71c7ae5e24235b5b44692444) * fix(ux): make PI Item rate field readonly based on `Maintain Same Rate` (cherry picked from commit cb9114442b936f03c9c975900282b3b9db62e453) * fix(ux): make PR Item rate field readonly based on `Maintain Same Rate` (cherry picked from commit b1ba2103323c8bfe066c288b4c285cf1f3c5b20b) --------- Co-authored-by: s-aga-r --- .../doctype/purchase_invoice/purchase_invoice.js | 12 ++++++++++++ .../purchase_invoice_item.json | 3 +-- .../doctype/purchase_receipt/purchase_receipt.js | 14 ++++++++++++++ .../purchase_receipt_item.json | 3 +-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index cebd61a6f5..215d8ec215 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -163,6 +163,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } }) }, __("Get Items From")); + + if (!this.frm.doc.is_return) { + frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => { + if (value) { + this.frm.doc.items.forEach((item) => { + this.frm.fields_dict.items.grid.update_docfield_property( + "rate", "read_only", (item.purchase_receipt && item.pr_detail) + ); + }); + } + }); + } } this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 7cad3ae1c0..9cf4e4fd7c 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -288,7 +288,6 @@ "oldfieldname": "import_rate", "oldfieldtype": "Currency", "options": "currency", - "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)", "reqd": 1 }, { @@ -919,7 +918,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 16:26:05.629780", + "modified": "2023-12-25 22:00:28.043555", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 6c9d3392e3..2cbccb0774 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -88,6 +88,20 @@ frappe.ui.form.on("Purchase Receipt", { }, __('Create')); } + if (frm.doc.docstatus === 0) { + if (!frm.doc.is_return) { + frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => { + if (value) { + frm.doc.items.forEach((item) => { + frm.fields_dict.items.grid.update_docfield_property( + "rate", "read_only", (item.purchase_order && item.purchase_order_item) + ); + }); + } + }); + } + } + frm.events.add_custom_buttons(frm); }, diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 7344d2a599..9bd692ad61 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -359,7 +359,6 @@ "oldfieldtype": "Currency", "options": "currency", "print_width": "100px", - "read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)", "width": "100px" }, { @@ -1104,7 +1103,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-11-30 16:12:02.364608", + "modified": "2023-12-25 22:32:09.801965", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From a41cf6243701bd97033fbf2a5a921537cdcd157a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:39:00 +0530 Subject: [PATCH 21/24] fix: do not make serial batch bundle for zero qty (backport #38949) (#38954) fix: do not make serial batch bundle for zero qty (#38949) (cherry picked from commit 06d6220a2aa2f7855db2c2f985d046280e26f13a) Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle.py | 21 +++++++ .../stock_reconciliation.py | 5 +- .../test_stock_reconciliation.py | 60 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 7ddf1573de..08b36546fa 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -85,6 +85,7 @@ class SerialandBatchBundle(Document): # end: auto-generated types def validate(self): + self.set_batch_no() self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() self.validate_voucher_no() @@ -99,6 +100,26 @@ class SerialandBatchBundle(Document): self.set_incoming_rate() self.calculate_qty_and_amount() + def set_batch_no(self): + if self.has_serial_no and self.has_batch_no: + serial_nos = [d.serial_no for d in self.entries if d.serial_no] + has_no_batch = any(not d.batch_no for d in self.entries) + if not has_no_batch: + return + + serial_no_batch = frappe._dict( + frappe.get_all( + "Serial No", + filters={"name": ("in", serial_nos)}, + fields=["name", "batch_no"], + as_list=True, + ) + ) + + for row in self.entries: + if not row.batch_no: + row.batch_no = serial_no_batch.get(row.serial_no) + def validate_serial_nos_inventory(self): if not (self.has_serial_no and self.type_of_transaction == "Outward"): return diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index e8d652e2b2..6819968394 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -171,7 +171,7 @@ class StockReconciliation(StockController): }, ) - if item_details.has_batch_no: + elif item_details.has_batch_no: batch_nos_details = get_available_batches( frappe._dict( { @@ -228,6 +228,9 @@ class StockReconciliation(StockController): def set_new_serial_and_batch_bundle(self): for item in self.items: + if not item.qty: + continue + if item.current_serial_and_batch_bundle and not item.serial_and_batch_bundle: current_doc = frappe.get_doc("Serial and Batch Bundle", item.current_serial_and_batch_bundle) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 1ec99bf9a5..70e9fb2205 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -865,6 +865,66 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): sr1.load_from_db() self.assertEqual(sr1.difference_amount, 10000) + def test_make_stock_zero_for_serial_batch_item(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + serial_item = self.make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "DJJ.####"} + ).name + batch_item = self.make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BDJJ.####", + "create_new_batch": 1, + } + ).name + + serial_batch_item = self.make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "ADJJ.####", + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "SN-ADJJ.####", + } + ).name + + warehouse = "_Test Warehouse - _TC" + + for item_code in [serial_item, batch_item, serial_batch_item]: + make_stock_entry( + item_code=item_code, + target=warehouse, + qty=10, + basic_rate=100, + ) + + _reco = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=0.0, + ) + + serial_batch_bundle = frappe.get_all( + "Stock Ledger Entry", + {"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0, "voucher_no": _reco.name}, + "serial_and_batch_bundle", + ) + + self.assertEqual(len(serial_batch_bundle), 1) + + _reco.cancel() + + serial_batch_bundle = frappe.get_all( + "Stock Ledger Entry", + {"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0, "voucher_no": _reco.name}, + "serial_and_batch_bundle", + ) + + self.assertEqual(len(serial_batch_bundle), 0) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) From 0e48ef7ace45be181c7be62ead9887eb64c6990c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 16:29:34 +0530 Subject: [PATCH 22/24] fix: not able to import serial batch bundle using csv (backport #38950) (#38955) * fix: not able to import serial batch bundle using csv (#38950) (cherry picked from commit d00f6672a8d9db1e28f9a86ff8efbedcdc95c41a) # Conflicts: # erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py * chore: fixed conflicts --------- Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 08b36546fa..afb53fb112 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1185,7 +1185,7 @@ def create_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non doc.append( "entries", { - "qty": (row.qty or 1.0) * (1 if type_of_transaction == "Inward" else -1), + "qty": (flt(row.qty) or 1.0) * (1 if type_of_transaction == "Inward" else -1), "warehouse": warehouse, "batch_no": row.batch_no, "serial_no": row.serial_no, @@ -1213,7 +1213,7 @@ def update_serial_batch_no_ledgers(entries, child_row, parent_doc, warehouse=Non doc.append( "entries", { - "qty": d.get("qty") * (1 if doc.type_of_transaction == "Inward" else -1), + "qty": (flt(d.get("qty")) or 1.0) * (1 if doc.type_of_transaction == "Inward" else -1), "warehouse": warehouse or d.get("warehouse"), "batch_no": d.get("batch_no"), "serial_no": d.get("serial_no"), From ab9fce333d2867a2d50aef33d021e157dc54c316 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Dec 2023 21:55:23 +0530 Subject: [PATCH 23/24] fix: min order qty optional in production plan (backport #38956) (#38958) fix: min order qty optional in production plan (#38956) * fix: min order qty optional in production plan * fix: test cases (cherry picked from commit b09c9354fb621c4283d6ebde91f3d061ea88f7f6) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/bom/bom.json | 3 ++- .../production_plan/production_plan.js | 2 ++ .../production_plan/production_plan.json | 9 +++++++- .../production_plan/production_plan.py | 19 +++++++++++++-- .../production_plan/test_production_plan.py | 23 +++++++++++++++++++ 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index e8d3542835..5083873681 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -218,6 +218,7 @@ "options": "\nWork Order\nJob Card" }, { + "default": "1", "fieldname": "conversion_rate", "fieldtype": "Float", "label": "Conversion Rate", @@ -636,7 +637,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-07 11:38:08.152294", + "modified": "2023-12-26 19:34:08.159312", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index dd102b0fae..cd92263543 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -305,6 +305,8 @@ frappe.ui.form.on('Production Plan', { frappe.throw(__("Select the Warehouse")); } + frm.set_value("consider_minimum_order_qty", 0); + if (frm.doc.ignore_existing_ordered_qty) { frm.events.get_items_for_material_requests(frm); } else { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 49386c4ebc..257b60c486 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -48,6 +48,7 @@ "material_request_planning", "include_non_stock_items", "include_subcontracted_items", + "consider_minimum_order_qty", "include_safety_stock", "ignore_existing_ordered_qty", "column_break_25", @@ -423,13 +424,19 @@ "fieldtype": "Link", "label": "Sub Assembly Warehouse", "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "consider_minimum_order_qty", + "fieldtype": "Check", + "label": "Consider Minimum Order Qty" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-03 14:08:11.928027", + "modified": "2023-12-26 16:31:13.740777", "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 a00dd084ce..caa6e464d2 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -67,6 +67,7 @@ class ProductionPlan(Document): combine_items: DF.Check combine_sub_items: DF.Check company: DF.Link + consider_minimum_order_qty: DF.Check customer: DF.Link | None for_warehouse: DF.Link | None from_date: DF.Date | None @@ -1211,7 +1212,14 @@ def get_subitems( def get_material_request_items( - row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict + doc, + row, + sales_order, + company, + ignore_existing_ordered_qty, + include_safety_stock, + warehouse, + bin_dict, ): total_qty = row["qty"] @@ -1220,8 +1228,14 @@ def get_material_request_items( required_qty = total_qty elif total_qty > bin_dict.get("projected_qty", 0): required_qty = total_qty - bin_dict.get("projected_qty", 0) - if required_qty > 0 and required_qty < row["min_order_qty"]: + + if ( + doc.get("consider_minimum_order_qty") + and required_qty > 0 + and required_qty < row["min_order_qty"] + ): required_qty = row["min_order_qty"] + item_group_defaults = get_item_group_defaults(row.item_code, company) if not row["purchase_uom"]: @@ -1559,6 +1573,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d if details.qty > 0: items = get_material_request_items( + doc, details, sales_order, company, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f86725d601..cb99b8845a 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1499,6 +1499,29 @@ class TestProductionPlan(FrappeTestCase): after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) self.assertAlmostEqual(after_qty, before_qty) + def test_min_order_qty_in_pp(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.stock.utils import get_or_make_bin + + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name + + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1) + + pln.for_warehouse = rm_warehouse + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + self.assertEqual(d.get("quantity"), 10.0) + + pln.consider_minimum_order_qty = 1 + mr_items = get_items_for_material_requests(pln.as_dict()) + for d in mr_items: + self.assertEqual(d.get("quantity"), 1000.0) + def create_production_plan(**args): """ From 5874be0f79e7d1bb54c60b22048a2b66baf8078f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Dec 2023 14:06:55 +0530 Subject: [PATCH 24/24] fix: incorrect qty in serial batch bundle against pick list (backport #38964) (#38966) fix: incorrect qty in serial batch bundle against pick list (#38964) (cherry picked from commit 47ee801d373058a2739c2fd42d971d624c42d5a7) Co-authored-by: rohitwaghchaure --- .../js/utils/serial_no_batch_selector.js | 1 + .../doctype/delivery_note/delivery_note.py | 17 ++- erpnext/stock/doctype/pick_list/pick_list.js | 38 ++++++ erpnext/stock/doctype/pick_list/pick_list.py | 15 ++- .../stock/doctype/pick_list/test_pick_list.py | 118 +++++++++++++++++- .../serial_and_batch_bundle.py | 109 +++++++++++++++- 6 files changed, 289 insertions(+), 9 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 4abc8fa395..4cd1243413 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -502,6 +502,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { set_data(data) { data.forEach(d => { + d.qty = Math.abs(d.qty); this.dialog.fields_dict.entries.df.data.push(d); }); diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 675f8e9158..132f8f2e29 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -311,11 +311,13 @@ class DeliveryNote(SellingController): ) def set_serial_and_batch_bundle_from_pick_list(self): + from erpnext.stock.serial_batch_bundle import SerialBatchCreation + if not self.pick_list: return for item in self.items: - if item.pick_list_item: + if item.pick_list_item and not item.serial_and_batch_bundle: filters = { "item_code": item.item_code, "voucher_type": "Pick List", @@ -326,7 +328,17 @@ class DeliveryNote(SellingController): bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name") if bundle_id: - item.serial_and_batch_bundle = bundle_id + cls_obj = SerialBatchCreation( + { + "type_of_transaction": "Outward", + "serial_and_batch_bundle": bundle_id, + "item_code": item.get("item_code"), + } + ) + + cls_obj.duplicate_package() + + item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle def validate_proj_cust(self): """check for does customer belong to same project as entered..""" @@ -408,6 +420,7 @@ class DeliveryNote(SellingController): self.update_stock_ledger() self.cancel_packing_slips() + self.update_pick_list_status() self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 7cd171ea92..afd6ce8138 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -283,6 +283,7 @@ frappe.ui.form.on('Pick List Item', { }); } }, + uom: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); if (row.uom) { @@ -291,13 +292,50 @@ frappe.ui.form.on('Pick List Item', { }); } }, + qty: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor); }, + conversion_factor: (frm, cdt, cdn) => { let row = frappe.get_doc(cdt, cdn); frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor); + }, + + pick_serial_and_batch(frm, cdt, cdn) { + let item = locals[cdt][cdn]; + let path = "assets/erpnext/js/utils/serial_no_batch_selector.js"; + + frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) + .then((r) => { + if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) { + item.has_serial_no = r.message.has_serial_no; + item.has_batch_no = r.message.has_batch_no; + item.type_of_transaction = item.qty > 0 ? "Outward":"Inward"; + + item.title = item.has_serial_no ? + __("Select Serial No") : __("Select Batch No"); + + if (item.has_serial_no && item.has_batch_no) { + item.title = __("Select Serial and Batch"); + } + + frappe.require(path, function() { + new erpnext.SerialBatchPackageSelector( + frm, item, (r) => { + if (r) { + let qty = Math.abs(r.total_qty); + frappe.model.set_value(item.doctype, item.name, { + "serial_and_batch_bundle": r.name, + "qty": qty + }); + } + } + ); + }); + } + }); } }); diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 545e45f3d8..758448af79 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -21,6 +21,7 @@ from erpnext.selling.doctype.sales_order.sales_order import ( ) from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( get_auto_batch_nos, + get_picked_serial_nos, ) from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.serial_batch_bundle import SerialBatchCreation @@ -167,6 +168,9 @@ class PickList(Document): "Serial and Batch Bundle", row.serial_and_batch_bundle ).set_serial_and_batch_values(self, row) + def on_trash(self): + self.remove_serial_and_batch_bundle() + def remove_serial_and_batch_bundle(self): for row in self.locations: if row.serial_and_batch_bundle: @@ -723,13 +727,14 @@ def get_available_item_locations( def get_available_item_locations_for_serialized_item( item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): + picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses) + sn = frappe.qb.DocType("Serial No") query = ( frappe.qb.from_(sn) .select(sn.name, sn.warehouse) .where((sn.item_code == item_code) & (sn.company == company)) .orderby(sn.creation) - .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: @@ -742,6 +747,9 @@ def get_available_item_locations_for_serialized_item( warehouse_serial_nos_map = frappe._dict() picked_qty = required_qty for serial_no, warehouse in serial_nos: + if serial_no in picked_serial_nos: + continue + if picked_qty <= 0: break @@ -786,7 +794,8 @@ def get_available_item_locations_for_batched_item( { "item_code": item_code, "warehouse": from_warehouses, - "qty": required_qty + total_picked_qty, + "qty": required_qty, + "is_pick_list": True, } ) ) @@ -1050,7 +1059,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte @frappe.whitelist() def target_document_exists(pick_list_name, purpose): if purpose == "Delivery": - return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name}) + return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1}) return stock_entry_exists(pick_list_name) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 56c44bfd25..322b0b46ba 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -644,6 +644,122 @@ class TestPickList(FrappeTestCase): so.reload() self.assertEqual(so.per_picked, 50) + def test_picklist_for_batch_item(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + properties={"is_stock_item": 1, "has_batch_no": 1, "batch_no_series": "PICKLT-.######"} + ).name + + # create batch + for batch_id in ["PICKLT-000001", "PICKLT-000002"]: + if not frappe.db.exists("Batch", batch_id): + frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": item, + } + ).insert() + + make_stock_entry( + item=item, + to_warehouse=warehouse, + qty=50, + basic_rate=100, + batches=frappe._dict({"PICKLT-000001": 30, "PICKLT-000002": 20}), + ) + + so = make_sales_order(item_code=item, qty=25.0, rate=100) + pl = create_pick_list(so.name) + # pick half the qty + for loc in pl.locations: + self.assertEqual(loc.qty, 25.0) + self.assertTrue(loc.serial_and_batch_bundle) + + data = frappe.get_all( + "Serial and Batch Entry", + fields=["qty", "batch_no"], + filters={"parent": loc.serial_and_batch_bundle}, + ) + + for d in data: + self.assertEqual(d.batch_no, "PICKLT-000001") + self.assertEqual(d.qty, 25.0 * -1) + + pl.save() + pl.submit() + + so1 = make_sales_order(item_code=item, qty=10.0, rate=100) + pl = create_pick_list(so1.name) + # pick half the qty + for loc in pl.locations: + self.assertEqual(loc.qty, 10.0) + self.assertTrue(loc.serial_and_batch_bundle) + + data = frappe.get_all( + "Serial and Batch Entry", + fields=["qty", "batch_no"], + filters={"parent": loc.serial_and_batch_bundle}, + ) + + for d in data: + self.assertTrue(d.batch_no in ["PICKLT-000001", "PICKLT-000002"]) + if d.batch_no == "PICKLT-000001": + self.assertEqual(d.qty, 5.0 * -1) + elif d.batch_no == "PICKLT-000002": + self.assertEqual(d.qty, 5.0 * -1) + + pl.save() + pl.submit() + pl.cancel() + + def test_picklist_for_serial_item(self): + warehouse = "_Test Warehouse - _TC" + item = make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PICKLT-.######"} + ).name + + make_stock_entry(item=item, to_warehouse=warehouse, qty=50, basic_rate=100) + + so = make_sales_order(item_code=item, qty=25.0, rate=100) + pl = create_pick_list(so.name) + picked_serial_nos = [] + # pick half the qty + for loc in pl.locations: + self.assertEqual(loc.qty, 25.0) + self.assertTrue(loc.serial_and_batch_bundle) + + data = frappe.get_all( + "Serial and Batch Entry", fields=["serial_no"], filters={"parent": loc.serial_and_batch_bundle} + ) + + picked_serial_nos = [d.serial_no for d in data] + self.assertEqual(len(picked_serial_nos), 25) + + pl.save() + pl.submit() + + so1 = make_sales_order(item_code=item, qty=10.0, rate=100) + pl = create_pick_list(so1.name) + # pick half the qty + for loc in pl.locations: + self.assertEqual(loc.qty, 10.0) + self.assertTrue(loc.serial_and_batch_bundle) + + data = frappe.get_all( + "Serial and Batch Entry", + fields=["qty", "batch_no"], + filters={"parent": loc.serial_and_batch_bundle}, + ) + + self.assertEqual(len(data), 10) + for d in data: + self.assertTrue(d.serial_no not in picked_serial_nos) + + pl.save() + pl.submit() + pl.cancel() + def test_picklist_with_bundles(self): warehouse = "_Test Warehouse - _TC" @@ -732,7 +848,7 @@ class TestPickList(FrappeTestCase): dn.cancel() pl.reload() - self.assertEqual(pl.status, "Completed") + self.assertEqual(pl.status, "Open") pl.cancel() pl.reload() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index afb53fb112..dd38e1127f 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -936,7 +936,7 @@ def parse_csv_file_to_get_serial_batch(reader): if index == 0: has_serial_no = row[0] == "Serial No" has_batch_no = row[0] == "Batch No" - if not has_batch_no: + if not has_batch_no and len(row) > 1: has_batch_no = row[1] == "Batch No" continue @@ -1611,10 +1611,17 @@ def get_auto_batch_nos(kwargs): stock_ledgers_batches = get_stock_ledgers_batches(kwargs) pos_invoice_batches = get_reserved_batches_for_pos(kwargs) sre_reserved_batches = get_reserved_batches_for_sre(kwargs) + picked_batches = frappe._dict() + if kwargs.get("is_pick_list"): + picked_batches = get_picked_batches(kwargs) - if stock_ledgers_batches or pos_invoice_batches or sre_reserved_batches: + if stock_ledgers_batches or pos_invoice_batches or sre_reserved_batches or picked_batches: update_available_batches( - available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches + available_batches, + stock_ledgers_batches, + pos_invoice_batches, + sre_reserved_batches, + picked_batches, ) if not kwargs.consider_negative_batches: @@ -1771,6 +1778,102 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]: return group_by_voucher +def get_picked_batches(kwargs) -> dict[str, dict]: + picked_batches = frappe._dict() + + table = frappe.qb.DocType("Serial and Batch Bundle") + child_table = frappe.qb.DocType("Serial and Batch Entry") + pick_list_table = frappe.qb.DocType("Pick List") + + query = ( + frappe.qb.from_(table) + .inner_join(child_table) + .on(table.name == child_table.parent) + .inner_join(pick_list_table) + .on(table.voucher_no == pick_list_table.name) + .select( + child_table.batch_no, + child_table.warehouse, + Sum(child_table.qty).as_("qty"), + ) + .where( + (table.docstatus != 2) + & (pick_list_table.status != "Completed") + & (table.type_of_transaction == "Outward") + & (table.is_cancelled == 0) + & (table.voucher_type == "Pick List") + & (table.voucher_no.isnotnull()) + ) + ) + + if kwargs.get("item_code"): + query = query.where(table.item_code == kwargs.get("item_code")) + + if kwargs.get("warehouse"): + if isinstance(kwargs.warehouse, list): + query = query.where(table.warehouse.isin(kwargs.warehouse)) + else: + query = query.where(table.warehouse == kwargs.get("warehouse")) + + data = query.run(as_dict=True) + for row in data: + if not row.qty: + continue + + key = (row.batch_no, row.warehouse) + if key not in picked_batches: + picked_batches[key] = frappe._dict( + { + "qty": row.qty, + "warehouse": row.warehouse, + } + ) + else: + picked_batches[key].qty += row.qty + + return picked_batches + + +def get_picked_serial_nos(item_code, warehouse=None) -> list[str]: + table = frappe.qb.DocType("Serial and Batch Bundle") + child_table = frappe.qb.DocType("Serial and Batch Entry") + pick_list_table = frappe.qb.DocType("Pick List") + + query = ( + frappe.qb.from_(table) + .inner_join(child_table) + .on(table.name == child_table.parent) + .inner_join(pick_list_table) + .on(table.voucher_no == pick_list_table.name) + .select( + child_table.serial_no, + ) + .where( + (table.docstatus != 2) + & (pick_list_table.status != "Completed") + & (table.type_of_transaction == "Outward") + & (table.is_cancelled == 0) + & (table.voucher_type == "Pick List") + & (table.voucher_no.isnotnull()) + ) + ) + + if item_code: + query = query.where(table.item_code == item_code) + + if warehouse: + if isinstance(warehouse, list): + query = query.where(table.warehouse.isin(warehouse)) + else: + query = query.where(table.warehouse == warehouse) + + data = query.run(as_dict=True) + if not data: + return [] + + return [row.serial_no for row in data if row.serial_no] + + def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]: bundle_table = frappe.qb.DocType("Serial and Batch Bundle") serial_batch_table = frappe.qb.DocType("Serial and Batch Entry")