From 14fe0af88752ca34e5dda9d260b3dd8d9d47634f Mon Sep 17 00:00:00 2001 From: kunhi Date: Mon, 12 Feb 2024 12:18:37 +0400 Subject: [PATCH 01/19] fix: fetch company terms (cherry picked from commit d97b6d38ef732205f8de17dcb01d90ea1f5120fa) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7cda11addc..b86fb92a32 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.setup(doc); } company() { + super.company(); erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); let me = this; From bef38f74fee5650c5805a327d1bb8e59deaa48af Mon Sep 17 00:00:00 2001 From: kunhi Date: Tue, 13 Feb 2024 22:32:10 +0400 Subject: [PATCH 02/19] fix: no need call for company method in sales invoice js (cherry picked from commit e3bd8d10b0a99fca326c7252063ef11b3665d3cf) --- .../doctype/sales_invoice/sales_invoice.js | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b86fb92a32..fb356b6ad7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -13,26 +13,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.setup_posting_date_time_check(); super.setup(doc); } - company() { - super.company(); - erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - - let me = this; - if (this.frm.doc.company) { - frappe.call({ - method: - "erpnext.accounts.party.get_party_account", - args: { - party_type: 'Customer', - party: this.frm.doc.customer, - company: this.frm.doc.company - }, - callback: (response) => { - if (response) me.frm.set_value("debit_to", response.message); - }, - }); - } - } onload() { var me = this; super.onload(); From a56d5b805c086b562f47f1ad4ec8c83153564b7e Mon Sep 17 00:00:00 2001 From: kunhi Date: Tue, 13 Feb 2024 22:49:39 +0400 Subject: [PATCH 03/19] fix: update_dimension is required and not need party account method (cherry picked from commit e6949d71f628a15ea87ee143d09dce47b66f0e02) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index fb356b6ad7..da3c2a4825 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -13,6 +13,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.setup_posting_date_time_check(); super.setup(doc); } + company() { + super.company(); + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + } onload() { var me = this; super.onload(); From 518b22bffcfdd803ae5c3dd5f91af90b5bbb3a8a Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 14 Feb 2024 18:45:11 +0530 Subject: [PATCH 04/19] fix: party item code in Blanket Order (cherry picked from commit 1a8f7f94036e1703ab51f8f591d88688ad2c9f52) --- .../doctype/blanket_order/blanket_order.py | 39 +++++++++++++++++++ .../blanket_order/test_blanket_order.py | 25 ++++++++++++ .../blanket_order_item.json | 12 +++++- .../blanket_order_item/blanket_order_item.py | 1 + 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index f8f336a6e8..08f05459c7 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -41,11 +41,50 @@ class BlanketOrder(Document): def validate(self): self.validate_dates() self.validate_duplicate_items() + self.set_party_item_code() def validate_dates(self): if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From date cannot be greater than To date")) + def set_party_item_code(self): + item_ref = {} + if self.blanket_order_type == "Selling": + item_ref = self.get_customer_items_ref() + print(item_ref) + else: + item_ref = self.get_supplier_items_ref() + + if not item_ref: + return + + for row in self.items: + row.party_item_code = item_ref.get(row.item_code) + + def get_customer_items_ref(self): + items = [d.item_code for d in self.items] + + return frappe._dict( + frappe.get_all( + "Item Customer Detail", + filters={"parent": ("in", items), "customer_name": self.customer}, + fields=["parent", "ref_code"], + as_list=True, + ) + ) + + def get_supplier_items_ref(self): + items = [d.item_code for d in self.items] + + return frappe._dict( + frappe.get_all( + "Item Supplier", + filters={"parent": ("in", items), "supplier": self.supplier}, + fields=["parent", "supplier_part_no"], + as_list=True, + ) + ) + def validate_duplicate_items(self): item_list = [] for item in self.items: diff --git a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py index e9fc25b5bc..3f3b6f092c 100644 --- a/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/test_blanket_order.py @@ -5,6 +5,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, today from erpnext import get_company_currency +from erpnext.stock.doctype.item.test_item import make_item from .blanket_order import make_order @@ -90,6 +91,30 @@ class TestBlanketOrder(FrappeTestCase): frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) po.submit() + def test_party_item_code(self): + item_doc = make_item("_Test Item 1 for Blanket Order") + item_code = item_doc.name + + customer = "_Test Customer" + supplier = "_Test Supplier" + + if not frappe.db.exists( + "Item Customer Detail", {"customer_name": customer, "parent": item_code} + ): + item_doc.append("customer_items", {"customer_name": customer, "ref_code": "CUST-REF-1"}) + item_doc.save() + + if not frappe.db.exists("Item Supplier", {"supplier": supplier, "parent": item_code}): + item_doc.append("supplier_items", {"supplier": supplier, "supplier_part_no": "SUPP-PART-1"}) + item_doc.save() + + # Blanket Order for Selling + bo = make_blanket_order(blanket_order_type="Selling", customer=customer, item_code=item_code) + self.assertEqual(bo.items[0].party_item_code, "CUST-REF-1") + + bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code) + self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1") + def make_blanket_order(**args): args = frappe._dict(args) diff --git a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json index 977ad547f5..aa7831fd6b 100644 --- a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json +++ b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2018-05-24 07:20:04.255236", "doctype": "DocType", "editable_grid": 1, @@ -6,6 +7,7 @@ "field_order": [ "item_code", "item_name", + "party_item_code", "column_break_3", "qty", "rate", @@ -62,10 +64,17 @@ "fieldname": "terms_and_conditions", "fieldtype": "Text", "label": "Terms and Conditions" + }, + { + "fieldname": "party_item_code", + "fieldtype": "Data", + "label": "Party Item Code", + "read_only": 1 } ], "istable": 1, - "modified": "2019-11-18 19:37:46.245878", + "links": [], + "modified": "2024-02-14 18:25:26.479672", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order Item", @@ -74,5 +83,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py index 068c2e9118..316d294eaf 100644 --- a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py +++ b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.py @@ -20,6 +20,7 @@ class BlanketOrderItem(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data + party_item_code: DF.Data | None qty: DF.Float rate: DF.Currency terms_and_conditions: DF.Text | None From c50639334f13438ab8eb7e2d36b204e1a0be11d6 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 15 Feb 2024 14:28:06 +0530 Subject: [PATCH 05/19] chore: fix linter issue (cherry picked from commit 230a7d8d537500c0f401f21c742f2a2ad653062a) --- erpnext/manufacturing/doctype/blanket_order/blanket_order.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py index 08f05459c7..e406b357b7 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.py +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.py @@ -51,7 +51,6 @@ class BlanketOrder(Document): item_ref = {} if self.blanket_order_type == "Selling": item_ref = self.get_customer_items_ref() - print(item_ref) else: item_ref = self.get_supplier_items_ref() From 37329469c3432acc5277171deddd5d40d9c72b96 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 15 Feb 2024 14:58:34 +0530 Subject: [PATCH 06/19] fix: not able to make purchase receipt (cherry picked from commit 2fb0499923d36d5d1e0efe520ab432edaf1e6bb6) --- erpnext/controllers/subcontracting_controller.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 17a2b07daa..eac35b0d39 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -539,6 +539,10 @@ class SubcontractingController(StockController): def __add_supplied_item(self, item_row, bom_item, qty): bom_item.conversion_factor = item_row.conversion_factor rm_obj = self.append(self.raw_material_table, bom_item) + if rm_obj.get("qty"): + # Qty field not exists + rm_obj.qty = 0.0 + rm_obj.reference_name = item_row.name if self.doctype == self.subcontract_data.order_doctype: From bf5d2f5fe4779d382dd343dbb250e8353d228285 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 15 Feb 2024 19:48:54 +0100 Subject: [PATCH 07/19] fix(Bank Transaction): precision for `(un)allocated_amount` (cherry picked from commit 8a702a633883408cd41572c19dd157f8211ea905) --- .../accounts/doctype/bank_transaction/bank_transaction.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index fef3b569ed..5e17881b6c 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -94,10 +94,13 @@ class BankTransaction(Document): pe.append(reference) def update_allocated_amount(self): - self.allocated_amount = ( + allocated_amount = ( sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0 ) - self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount + unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - allocated_amount + + self.allocated_amount = flt(allocated_amount, self.precision("allocated_amount")) + self.unallocated_amount = flt(unallocated_amount, self.precision("unallocated_amount")) def before_submit(self): self.allocate_payment_entries() From 40d4e3261efd3eb8f98b05b2b7d35b08e1a50c18 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 16 Feb 2024 15:41:30 +0530 Subject: [PATCH 08/19] fix: use serial batch fields not enabled for new stock entry (cherry picked from commit dc9115a5865c86721863cfbda55bb00ac00cb756) --- erpnext/public/js/controllers/transaction.js | 2 +- .../stock/doctype/stock_entry/stock_entry.js | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0241afcf03..d08c36c1d0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -234,7 +234,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } set_fields_onload_for_line_item() { - if (this.frm.is_new && this.frm.doc?.items) { + if (this.frm.is_new() && this.frm.doc?.items) { this.frm.doc.items.forEach(item => { if (item.docstatus === 0 && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8da3e8fdd0..6753a3a216 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -936,6 +936,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle this.toggle_related_fields(this.frm.doc); this.toggle_enable_bom(); this.show_stock_ledger(); + this.set_fields_onload_for_line_item(); erpnext.utils.view_serial_batch_nos(this.frm); if (this.frm.doc.docstatus===1 && erpnext.is_perpetual_inventory_enabled(this.frm.doc.company)) { this.show_general_ledger(); @@ -944,6 +945,35 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle erpnext.utils.add_item(this.frm); } + serial_no(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + + if (item?.serial_no) { + // Replace all occurences of comma with line feed + item.serial_no = item.serial_no.replace(/,/g, '\n'); + item.conversion_factor = item.conversion_factor || 1; + + let valid_serial_nos = []; + let serialnos = item.serial_no.split("\n"); + for (var i = 0; i < serialnos.length; i++) { + if (serialnos[i] != "") { + valid_serial_nos.push(serialnos[i]); + } + } + frappe.model.set_value(item.doctype, item.name, + "qty", valid_serial_nos.length / item.conversion_factor); + } + } + + set_fields_onload_for_line_item() { + if (this.frm.is_new() && this.frm.doc?.items + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1) { + this.frm.doc.items.forEach(item => { + frappe.model.set_value(item.doctype, item.name, "use_serial_batch_fields", 1); + }) + } + } + scan_barcode() { frappe.flags.dialog_set = false; const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); @@ -1074,6 +1104,10 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle if(!row.s_warehouse) row.s_warehouse = this.frm.doc.from_warehouse; if(!row.t_warehouse) row.t_warehouse = this.frm.doc.to_warehouse; + + if (cint(frappe.user_defaults?.use_serial_batch_fields)) { + frappe.model.set_value(row.doctype, row.name, "use_serial_batch_fields", 1); + } } from_warehouse(doc) { From 15135952fcc259469b2c90cbc63aad20444a993b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:47:20 +0530 Subject: [PATCH 09/19] fix: batch filter not working in stock ledger report (backport #39934) (#39935) fix: batch filter not working in stock ledger report (cherry picked from commit a995e87567609ba4808e7bf2632b76004750aff0) Co-authored-by: Rohit Waghchaure --- .../stock/report/stock_ledger/stock_ledger.py | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index e59f2fe644..86af9e9a06 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -320,15 +320,45 @@ def get_stock_ledger_entries(filters, items): if items: query = query.where(sle.item_code.isin(items)) - for field in ["voucher_no", "batch_no", "project", "company"]: + for field in ["voucher_no", "project", "company"]: if filters.get(field) and field not in inventory_dimension_fields: query = query.where(sle[field] == filters.get(field)) + if filters.get("batch_no"): + bundles = get_serial_and_batch_bundles(filters) + + if bundles: + query = query.where( + (sle.serial_and_batch_bundle.isin(bundles)) | (sle.batch_no == filters.batch_no) + ) + else: + query = query.where(sle.batch_no == filters.batch_no) + query = apply_warehouse_filter(query, sle, filters) return query.run(as_dict=True) +def get_serial_and_batch_bundles(filters): + SBB = frappe.qb.DocType("Serial and Batch Bundle") + SBE = frappe.qb.DocType("Serial and Batch Entry") + + query = ( + frappe.qb.from_(SBE) + .inner_join(SBB) + .on(SBE.parent == SBB.name) + .select(SBE.parent) + .where( + (SBB.docstatus == 1) + & (SBB.has_batch_no == 1) + & (SBB.voucher_no.notnull()) + & (SBE.batch_no == filters.batch_no) + ) + ) + + return query.run(pluck=SBE.parent) + + def get_inventory_dimension_fields(): return [dimension.fieldname for dimension in get_inventory_dimensions()] From f5d7fbdaf8cadbd886368853526cd04c4f1425b2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:58:58 +0100 Subject: [PATCH 10/19] fix(Issue): create communication Ignore permisions and mandatory. Required, for example, when Issue is created by Customer via portal. (cherry picked from commit 3f1d008741e1255b2b9c0d31aa489d9b427ab5c7) --- erpnext/support/doctype/issue/issue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index c03fb3ea1f..cb18579c04 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -113,8 +113,8 @@ class Issue(Document): "reference_name": self.name, } ) - communication.ignore_permissions = True - communication.ignore_mandatory = True + communication.flags.ignore_permissions = True + communication.flags.ignore_mandatory = True communication.save() @frappe.whitelist() From ce111807933609304980ceadcb5f9c7cbdf9fe40 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 17 Feb 2024 07:19:32 +0530 Subject: [PATCH 11/19] refactor: use popup to inform on additional reconciliation step (cherry picked from commit 0d260faa0070f767220ba724dd42b0f2d2cc922d) --- erpnext/controllers/accounts_controller.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0f087d4854..28cca976f8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -216,6 +216,18 @@ class AccountsController(TransactionBase): ) ) + if self.get("is_return") and self.get("return_against"): + document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note" + frappe.msgprint( + _( + "{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}." + ).format( + document_type, + get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), + get_link_to_form(self.doctype, self.get("return_against")), + ) + ) + pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() From 74819b8e70bd712d04f960aee71e1cfee15d76af Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 17 Feb 2024 06:41:00 +0530 Subject: [PATCH 12/19] fix: group node in warehouse filter in Item-wise Sales Register (cherry picked from commit 44538bd02aa150844135d21934bf403ff5244e68) --- .../item_wise_sales_register/item_wise_sales_register.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index ce22d7566c..94457d5998 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -350,7 +350,13 @@ def get_conditions(filters, additional_conditions=None): and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" if filters.get("warehouse"): - conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" + if frappe.db.get_value("Warehouse", filters.get("warehouse"), "is_group"): + lft, rgt = frappe.db.get_all( + "Warehouse", filters={"name": filters.get("warehouse")}, fields=["lft", "rgt"], as_list=True + )[0] + conditions += f"and ifnull(`tabSales Invoice Item`.warehouse, '') in (select name from `tabWarehouse` where lft > {lft} and rgt < {rgt}) " + else: + conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" if filters.get("brand"): conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" From baa83ece2176c2b72c119b7828e8c561f0d71d09 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 15 Feb 2024 12:35:14 +0530 Subject: [PATCH 13/19] refactor: add total row if only one party is being filtered (cherry picked from commit b1dfa2537bee220e8bb915b8152e4aa3014befa9) --- .../report/accounts_receivable/accounts_receivable.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index eafce6d326..92d9755897 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -83,7 +83,10 @@ class ReceivablePayableReport(object): self.skip_total_row = 1 if self.filters.get("in_party_currency"): - self.skip_total_row = 1 + if self.filters.get("party") and len(self.filters.get("party")) == 1: + self.skip_total_row = 0 + else: + self.skip_total_row = 1 def get_data(self): self.get_ple_entries() From acd2e93f8cd4873f6e6f88b620f3308801e8f31a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:45:28 +0530 Subject: [PATCH 14/19] fix: do not empty serial batch fields (backport #39948) (#39956) fix: do not empty serial batch fields (#39948) (cherry picked from commit a4cbfabe0ee4c8fca3ac654e6b3fe3db8b551334) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 6 +++--- erpnext/public/js/controllers/buying.js | 4 ++-- erpnext/public/js/utils/sales_common.js | 2 +- erpnext/stock/doctype/pick_list/pick_list.js | 2 +- .../stock/doctype/purchase_receipt/test_purchase_receipt.py | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.js | 2 +- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index f920706ba6..0e039b9d1b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -162,6 +162,9 @@ class StockController(AccountsController): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.serial_batch_bundle import SerialBatchCreation + if self.get("_action") == "update_after_submit": + return + # To handle test cases if frappe.flags.in_test and frappe.flags.use_serial_and_batch_fields: return @@ -219,7 +222,6 @@ class StockController(AccountsController): row.db_set( { "rejected_serial_and_batch_bundle": sn_doc.name, - "rejected_serial_no": "", } ) else: @@ -227,8 +229,6 @@ class StockController(AccountsController): row.db_set( { "serial_and_batch_bundle": sn_doc.name, - "serial_no": "", - "batch_no": "", } ) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 77ecf75e0c..b3d301d988 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -368,7 +368,7 @@ erpnext.buying = { let update_values = { "serial_and_batch_bundle": r.name, - "qty": qty + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } if (r.warehouse) { @@ -408,7 +408,7 @@ erpnext.buying = { let update_values = { "serial_and_batch_bundle": r.name, - "rejected_qty": qty + "rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } if (r.warehouse) { diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index b8ec77f8e5..4bb78433ae 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -339,7 +339,7 @@ erpnext.sales_common = { frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": qty + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } } diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 3cc2956e96..056cd5cc99 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -330,7 +330,7 @@ frappe.ui.form.on('Pick List Item', { let qty = Math.abs(r.total_qty); frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": qty + "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ff0300f9e9..2b18507a5f 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2222,7 +2222,7 @@ class TestPurchaseReceipt(FrappeTestCase): ) self.assertEqual(pr.items[0].use_serial_batch_fields, 1) - self.assertFalse(pr.items[0].serial_no) + self.assertTrue(pr.items[0].serial_no) self.assertTrue(pr.items[0].serial_and_batch_bundle) sbb_doc = frappe.get_doc("Serial and Batch Bundle", pr.items[0].serial_and_batch_bundle) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6753a3a216..8c762915d4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1178,7 +1178,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { if (r) { frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) + "qty": Math.abs(r.total_qty) / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } } diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 571bef50f3..ebc20e1f6c 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -1766,7 +1766,7 @@ class TestStockEntry(FrappeTestCase): ) self.assertTrue(se.items[0].use_serial_batch_fields) - self.assertFalse(se.items[0].serial_no) + self.assertTrue(se.items[0].serial_no) self.assertTrue(se.items[0].serial_and_batch_bundle) for serial_no in serial_nos: @@ -1784,7 +1784,7 @@ class TestStockEntry(FrappeTestCase): se1.reload() self.assertTrue(se1.items[0].use_serial_batch_fields) - self.assertFalse(se1.items[0].serial_no) + self.assertTrue(se1.items[0].serial_no) self.assertTrue(se1.items[0].serial_and_batch_bundle) for serial_no in serial_nos: From b954bdfdf92c43c2f71bb4ca2f0efd0e377a7b9f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 19 Feb 2024 22:18:57 +0530 Subject: [PATCH 15/19] fix: float division by zero (cherry picked from commit e8ae4ed61d3a5bc6913825f0da15dd630331a5af) --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 379320237e..215b853e6a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -899,7 +899,7 @@ class update_entries_after(object): precision = doc.precision("total_qty") self.wh_data.qty_after_transaction += flt(doc.total_qty, precision) - if self.wh_data.qty_after_transaction: + if flt(self.wh_data.qty_after_transaction, precision): self.wh_data.valuation_rate = flt(self.wh_data.stock_value, precision) / flt( self.wh_data.qty_after_transaction, precision ) From 46f7569a5417966a368ab59d1f6b4df9fb9c6136 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 12:35:46 +0530 Subject: [PATCH 16/19] fix: reposting failed status not updated (backport #39970) (#39972) fix: reposting failed status not updated (#39970) (cherry picked from commit d4264f7ba12322f05f7f8128fd5a1cd8bc6eb231) Co-authored-by: rohitwaghchaure --- .../repost_item_valuation.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 2715324ad0..4660fb0f16 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -299,9 +299,20 @@ def repost(doc): doc.log_error("Unable to repost item valuation") message = frappe.message_log.pop() if frappe.message_log else "" + if isinstance(message, dict): + message = message.get("message") + if traceback: - message += "
" + "Traceback:
" + traceback - frappe.db.set_value(doc.doctype, doc.name, "error_log", message) + message += "

" + "Traceback:
" + traceback + + frappe.db.set_value( + doc.doctype, + doc.name, + { + "error_log": message, + "status": "Failed", + }, + ) outgoing_email_account = frappe.get_cached_value( "Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name" From 720144898f6d5a80458e7aa695beda33384782ac Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:13:00 +0530 Subject: [PATCH 17/19] fix: show active bom in the dropdown while making stock entry and MR (backport #39974) (#39976) fix: show active bom in the dropdown while making stock entry and MR (#39974) (cherry picked from commit 133f8bd92a33fb15d1ababf8b5f1d4f061426538) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.js | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 77a3d6d97f..1b40f2b489 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -250,7 +250,7 @@ frappe.ui.form.on('Material Request', { fields: [ {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), options:"BOM", reqd: 1, get_query: function() { - return {filters: { docstatus:1 }}; + return {filters: { docstatus:1, "is_active": 1 }}; }}, {"fieldname":"warehouse", "fieldtype":"Link", "label":__("For Warehouse"), options:"Warehouse", reqd: 1}, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8c762915d4..427147c4e4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -543,7 +543,9 @@ frappe.ui.form.on('Stock Entry', { let fields = [ {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), - options:"BOM", reqd: 1, get_query: filters()}, + options:"BOM", reqd: 1, get_query: () => { + return {filters: { docstatus:1, "is_active": 1 }}; + }}, {"fieldname":"source_warehouse", "fieldtype":"Link", "label":__("Source Warehouse"), options:"Warehouse"}, {"fieldname":"target_warehouse", "fieldtype":"Link", "label":__("Target Warehouse"), From 0f87ec15adb5ce76ada846c4746b4fa3f6451b48 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 15:53:12 +0530 Subject: [PATCH 18/19] fix: 'NoneType' object is not iterable (backport #39977) (#39981) fix: 'NoneType' object is not iterable (#39977) (cherry picked from commit 8e7d47b3a7453ce7ded83c4205c556716f2afa19) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/pick_list/pick_list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 0e1f8d78b8..8a1f79d4a2 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -73,6 +73,10 @@ class PickList(Document): self.update_status() self.set_item_locations() + if self.get("locations"): + self.validate_sales_order_percentage() + + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): if ( From 2ee51d36ffd8b77e9fe460bb758ef302f1132c2c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 00:04:01 +0530 Subject: [PATCH 19/19] fix: set batch created from bundle to batch field in stock transaction (backport #39966) (#39987) fix: set batch created from bundle to batch field in stock transaction (#39966) * fix: set batch created from bundle to batch field in stock transaction * fix: validation for serial and batch no (cherry picked from commit 4b24fcd2216caadbec402fc7d1e412bdcc662dad) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 42 +++++++++++- .../tests/test_subcontracting_controller.py | 14 ++-- erpnext/public/js/controllers/buying.js | 2 + erpnext/public/js/controllers/transaction.js | 18 ++---- erpnext/public/js/utils/sales_common.js | 1 + .../js/utils/serial_no_batch_selector.js | 22 +++---- erpnext/stock/doctype/pick_list/pick_list.js | 1 + .../purchase_receipt/test_purchase_receipt.py | 24 +++++++ .../serial_and_batch_bundle.js | 23 +++++-- .../stock/doctype/stock_entry/stock_entry.js | 1 + .../stock/doctype/stock_entry/stock_entry.py | 5 +- .../stock_reconciliation.py | 4 +- .../stock/report/stock_ledger/stock_ledger.js | 6 ++ .../stock/report/stock_ledger/stock_ledger.py | 64 ++++++++++++++++++- erpnext/stock/serial_batch_bundle.py | 18 ++++-- erpnext/stock/utils.py | 26 +++++--- 16 files changed, 213 insertions(+), 58 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 0e039b9d1b..c337f0dcf6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -7,7 +7,7 @@ from typing import List, Tuple import frappe from frappe import _, bold -from frappe.utils import cint, flt, get_link_to_form, getdate +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate import erpnext from erpnext.accounts.general_ledger import ( @@ -174,13 +174,16 @@ class StockController(AccountsController): table_name = "stock_items" for row in self.get(table_name): + if row.serial_and_batch_bundle and (row.serial_no or row.batch_no): + self.validate_serial_nos_and_batches_with_bundle(row) + if not row.serial_no and not row.batch_no and not row.get("rejected_serial_no"): continue if not row.use_serial_batch_fields and ( row.serial_no or row.batch_no or row.get("rejected_serial_no") ): - frappe.throw(_("Please enable Use Old Serial / Batch Fields to make_bundle")) + row.use_serial_batch_fields = 1 if row.use_serial_batch_fields and ( not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle") @@ -232,6 +235,41 @@ class StockController(AccountsController): } ) + def validate_serial_nos_and_batches_with_bundle(self, row): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + throw_error = False + if row.serial_no: + serial_nos = frappe.get_all( + "Serial and Batch Entry", fields=["serial_no"], filters={"parent": row.serial_and_batch_bundle} + ) + serial_nos = sorted([cstr(d.serial_no) for d in serial_nos]) + parsed_serial_nos = get_serial_nos(row.serial_no) + + if len(serial_nos) != len(parsed_serial_nos): + throw_error = True + elif serial_nos != parsed_serial_nos: + for serial_no in serial_nos: + if serial_no not in parsed_serial_nos: + throw_error = True + break + + elif row.batch_no: + batches = frappe.get_all( + "Serial and Batch Entry", fields=["batch_no"], filters={"parent": row.serial_and_batch_bundle} + ) + batches = sorted([d.batch_no for d in batches]) + + if batches != [row.batch_no]: + throw_error = True + + if throw_error: + frappe.throw( + _( + "At row {0}: Serial and Batch Bundle {1} has already created. Please remove the values from the serial no or batch no fields." + ).format(row.idx, row.serial_and_batch_bundle) + ) + def set_use_serial_batch_fields(self): if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"): for row in self.items: diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 47762ac4cf..95a7bcb398 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -401,7 +401,7 @@ class TestSubcontractingController(FrappeTestCase): { "main_item_code": "Subcontracted Item SA4", "item_code": "Subcontracted SRM Item 3", - "qty": 1.0, + "qty": 3.0, "rate": 100.0, "stock_uom": "Nos", "warehouse": "_Test Warehouse - _TC", @@ -914,12 +914,6 @@ def update_item_details(child_row, details): else child_row.get("consumed_qty") ) - if child_row.serial_no: - details.serial_no.extend(get_serial_nos(child_row.serial_no)) - - if child_row.batch_no: - details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") - if child_row.serial_and_batch_bundle: doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle) for row in doc.get("entries"): @@ -928,6 +922,12 @@ def update_item_details(child_row, details): if row.batch_no: details.batch_no[row.batch_no] += row.qty * (-1 if doc.type_of_transaction == "Outward" else 1) + else: + if child_row.serial_no: + details.serial_no.extend(get_serial_nos(child_row.serial_no)) + + if child_row.batch_no: + details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") def make_stock_transfer_entry(**args): diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index b3d301d988..1d0d47ec3d 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -368,6 +368,7 @@ erpnext.buying = { let update_values = { "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } @@ -408,6 +409,7 @@ erpnext.buying = { let update_values = { "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, "rejected_qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d08c36c1d0..c0298a4f14 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -145,6 +145,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + this.frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { + return me.set_query_for_batch(doc, cdt, cdn); + }); + } + if( this.frm.docstatus < 2 && this.frm.fields_dict["payment_terms_template"] @@ -1633,18 +1639,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe return item_list; } - items_delete() { - this.update_localstorage_scanned_data(); - } - - update_localstorage_scanned_data() { - let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"]; - if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) { - const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm}); - barcode_scanner.update_localstorage_scanned_data(); - } - } - _set_values_for_item_list(children) { const items_rule_dict = {}; diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 4bb78433ae..a957530ec8 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -339,6 +339,7 @@ erpnext.sales_common = { frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 80ade7086c..fccaf88c71 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { let warehouse = this.item?.type_of_transaction === "Outward" ? (this.item.warehouse || this.item.s_warehouse) : ""; + if (this.frm.doc.doctype === 'Stock Entry') { + warehouse = this.item.s_warehouse || this.item.t_warehouse; + } + if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') { warehouse = this.get_warehouse(); } @@ -367,19 +371,11 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label: __('Batch No'), in_list_view: 1, get_query: () => { - if (this.item.type_of_transaction !== "Outward") { - return { - filters: { - 'item': this.item.item_code, - } - } - } else { - return { - query : "erpnext.controllers.queries.get_batch_no", - filters: { - 'item_code': this.item.item_code, - 'warehouse': this.get_warehouse() - } + return { + query : "erpnext.controllers.queries.get_batch_no", + filters: { + 'item_code': this.item.item_code, + 'warehouse': this.item.s_warehouse || this.item.t_warehouse, } } }, diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 056cd5cc99..3a5daa1bb7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -330,6 +330,7 @@ frappe.ui.form.on('Pick List Item', { let qty = Math.abs(r.total_qty); frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, "qty": qty / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2b18507a5f..e906d4986b 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2280,6 +2280,30 @@ class TestPurchaseReceipt(FrappeTestCase): serial_no_status = frappe.db.get_value("Serial No", sn, "status") self.assertTrue(serial_no_status != "Active") + def test_auto_set_batch_based_on_bundle(self): + item_code = make_item( + "_Test Auto Set Batch Based on Bundle", + properties={ + "has_batch_no": 1, + "batch_number_series": "BATCH-BNU-TASBBB-.#####", + "create_new_batch": 1, + }, + ).name + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1) + + pr = make_purchase_receipt( + item_code=item_code, + qty=5, + rate=100, + ) + + self.assertTrue(pr.items[0].batch_no) + batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) + self.assertEqual(pr.items[0].batch_no, batch_no) + + frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 91b743016b..1f7bb4d245 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -207,13 +207,24 @@ frappe.ui.form.on('Serial and Batch Bundle', { }; }); - frm.set_query('batch_no', 'entries', () => { - return { - filters: { - item: frm.doc.item_code, - disabled: 0, + frm.set_query('batch_no', 'entries', (doc) => { + + if (doc.type_of_transaction ==="Outward") { + return { + query : "erpnext.controllers.queries.get_batch_no", + filters: { + item_code: doc.item_code, + warehouse: doc.warehouse, + } } - }; + } else { + return { + filters: { + item: doc.item_code, + disabled: 0, + } + }; + } }); frm.set_query('warehouse', 'entries', () => { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 427147c4e4..7f79f04aae 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1180,6 +1180,7 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => { if (r) { frappe.model.set_value(item.doctype, item.name, { "serial_and_batch_bundle": r.name, + "use_serial_batch_fields": 0, "qty": Math.abs(r.total_qty) / flt(item.conversion_factor || 1, precision("conversion_factor", item)) }); } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 81be3d1fc1..cbad472377 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -834,6 +834,7 @@ class StockEntry(StockController): currency=erpnext.get_company_currency(self.company), company=self.company, raise_error_if_no_rate=raise_error_if_no_rate, + batch_no=d.batch_no, serial_and_batch_bundle=d.serial_and_batch_bundle, ) @@ -862,7 +863,7 @@ class StockEntry(StockController): if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) rate = get_incoming_rate(args, raise_error_if_no_rate) - if rate > 0: + if rate >= 0: d.basic_rate = rate d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) @@ -885,6 +886,8 @@ class StockEntry(StockController): "allow_zero_valuation": item.allow_zero_valuation_rate, "serial_and_batch_bundle": item.serial_and_batch_bundle, "voucher_detail_no": item.name, + "batch_no": item.batch_no, + "serial_no": item.serial_no, } ) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index ce08615ed5..c69b20b7d9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -834,6 +834,7 @@ class StockReconciliation(StockController): if voucher_detail_no != row.name: continue + val_rate = 0.0 current_qty = 0.0 if row.current_serial_and_batch_bundle: current_qty = self.get_current_qty_for_serial_or_batch(row) @@ -843,7 +844,6 @@ class StockReconciliation(StockController): row.warehouse, self.posting_date, self.posting_time, - voucher_no=self.name, ) current_qty = item_dict.get("qty") @@ -885,7 +885,7 @@ class StockReconciliation(StockController): {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, "name", ) - and (not row.current_serial_and_batch_bundle and not row.batch_no) + and (not row.current_serial_and_batch_bundle) ): self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True) row.reload() diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index b00b422a67..2ec757b205 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -91,6 +91,12 @@ frappe.query_reports["Stock Ledger"] = { "options": "Currency\nFloat", "default": "Currency" }, + { + "fieldname": "segregate_serial_batch_bundle", + "label": __("Segregate Serial / Batch Bundle"), + "fieldtype": "Check", + "default": 0 + } ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 86af9e9a06..50764357fe 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -2,6 +2,8 @@ # License: GNU General Public License v3. See license.txt +import copy + import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime @@ -26,6 +28,10 @@ def execute(filters=None): item_details = get_item_details(items, sl_entries, include_uom) opening_row = get_opening_balance(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + bundle_details = {} + + if filters.get("segregate_serial_batch_bundle"): + bundle_details = get_serial_batch_bundle_details(sl_entries) data = [] conversion_factors = [] @@ -45,6 +51,9 @@ def execute(filters=None): item_detail = item_details[sle.item_code] sle.update(item_detail) + if bundle_info := bundle_details.get(sle.serial_and_batch_bundle): + data.extend(get_segregated_bundle_entries(sle, bundle_info)) + continue if filters.get("batch_no") or inventory_dimension_filters_applied: actual_qty += flt(sle.actual_qty, precision) @@ -76,6 +85,60 @@ def execute(filters=None): return columns, data +def get_segregated_bundle_entries(sle, bundle_details): + segregated_entries = [] + qty_before_transaction = sle.qty_after_transaction - sle.actual_qty + stock_value_before_transaction = sle.stock_value - sle.stock_value_difference + + for row in bundle_details: + new_sle = copy.deepcopy(sle) + new_sle.update(row) + + new_sle.update( + { + "in_out_rate": flt(new_sle.stock_value_difference / row.qty) if row.qty else 0, + "in_qty": row.qty if row.qty > 0 else 0, + "out_qty": row.qty if row.qty < 0 else 0, + "qty_after_transaction": qty_before_transaction + row.qty, + "stock_value": stock_value_before_transaction + new_sle.stock_value_difference, + "incoming_rate": row.incoming_rate if row.qty > 0 else 0, + } + ) + + qty_before_transaction += row.qty + stock_value_before_transaction += new_sle.stock_value_difference + + new_sle.valuation_rate = ( + stock_value_before_transaction / qty_before_transaction if qty_before_transaction else 0 + ) + + segregated_entries.append(new_sle) + + return segregated_entries + + +def get_serial_batch_bundle_details(sl_entries): + bundle_details = [] + for sle in sl_entries: + if sle.serial_and_batch_bundle: + bundle_details.append(sle.serial_and_batch_bundle) + + if not bundle_details: + return frappe._dict({}) + + _bundle_details = frappe._dict({}) + batch_entries = frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", bundle_details)}, + fields=["parent", "qty", "incoming_rate", "stock_value_difference", "batch_no", "serial_no"], + order_by="parent, idx", + ) + for entry in batch_entries: + _bundle_details.setdefault(entry.parent, []).append(entry) + + return _bundle_details + + def update_available_serial_nos(available_serial_nos, sle): serial_nos = get_serial_nos(sle.serial_no) key = (sle.item_code, sle.warehouse) @@ -256,7 +319,6 @@ def get_columns(filters): "options": "Serial and Batch Bundle", "width": 100, }, - {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, { "label": _("Project"), "fieldname": "project", diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index d8b5b34d44..fc1cca46f6 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -5,7 +5,7 @@ import frappe from frappe import _, bold from frappe.model.naming import make_autoname from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, flt, get_link_to_form, now, nowtime, today +from frappe.utils import cint, cstr, flt, get_link_to_form, now, nowtime, today from erpnext.stock.deprecated_serial_batch import ( DeprecatedBatchNoValuation, @@ -138,9 +138,17 @@ class SerialBatchBundle: self.child_doctype, self.sle.voucher_detail_no, "rejected_serial_and_batch_bundle", sn_doc.name ) else: - frappe.db.set_value( - self.child_doctype, self.sle.voucher_detail_no, "serial_and_batch_bundle", sn_doc.name - ) + values_to_update = { + "serial_and_batch_bundle": sn_doc.name, + } + + if frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"): + if sn_doc.has_serial_no: + values_to_update["serial_no"] = "\n".join(cstr(d.serial_no) for d in sn_doc.entries) + elif sn_doc.has_batch_no and len(sn_doc.entries) == 1: + values_to_update["batch_no"] = sn_doc.entries[0].batch_no + + frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update) @property def child_doctype(self): @@ -905,8 +913,6 @@ class SerialBatchCreation: self.batches = get_available_batches(kwargs) def set_auto_serial_batch_entries_for_inward(self): - print(self.get("serial_nos")) - if (self.get("batches") and self.has_batch_no) or ( self.get("serial_nos") and self.has_serial_no ): diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 54e0ab5acf..5410e692c4 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -262,7 +262,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): item_code=args.get("item_code"), ) - in_rate = sn_obj.get_incoming_rate() + return sn_obj.get_incoming_rate() elif item_details and item_details.has_batch_no and args.get("serial_and_batch_bundle"): args.actual_qty = args.qty @@ -272,23 +272,33 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): item_code=args.get("item_code"), ) - in_rate = batch_obj.get_incoming_rate() + return batch_obj.get_incoming_rate() elif (args.get("serial_no") or "").strip() and not args.get("serial_and_batch_bundle"): - in_rate = get_avg_purchase_rate(args.get("serial_no")) + args.actual_qty = args.qty + args.serial_nos = get_serial_nos_data(args.get("serial_no")) + + sn_obj = SerialNoValuation( + sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code") + ) + + return sn_obj.get_incoming_rate() elif ( args.get("batch_no") and frappe.db.get_value("Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True) and not args.get("serial_and_batch_bundle") ): - in_rate = get_batch_incoming_rate( - item_code=args.get("item_code"), + + args.actual_qty = args.qty + args.batch_nos = frappe._dict({args.batch_no: args}) + + batch_obj = BatchNoValuation( + sle=args, warehouse=args.get("warehouse"), - batch_no=args.get("batch_no"), - posting_date=args.get("posting_date"), - posting_time=args.get("posting_time"), + item_code=args.get("item_code"), ) + return batch_obj.get_incoming_rate() else: valuation_method = get_valuation_method(args.get("item_code")) previous_sle = get_previous_sle(args)