diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index 6319d5dfb026..e2084e89c435 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -17,6 +17,10 @@ from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.selling.page.point_of_sale.point_of_sale import get_items +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( + get_batch_from_bundle, +) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -179,6 +183,94 @@ def test_pos_closing_for_required_accounting_dimension_in_pos_profile(self): accounting_dimension_department.save() disable_dimension() + def test_merging_into_sales_invoice_for_batched_item(self): + frappe.flags.print_message = False + from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import ( + init_user_and_profile, + ) + from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( + consolidate_pos_invoices, + ) + from erpnext.stock.doctype.batch.batch import get_batch_qty + + frappe.db.sql("delete from `tabPOS Invoice`") + item_doc = make_item( + "_Test Item With Batch FOR POS Merge Test", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BATCH-PM-POS-MERGE-.####", + "create_new_batch": 1, + }, + ) + + item_code = item_doc.name + se = make_stock_entry( + target="_Test Warehouse - _TC", + item_code=item_code, + qty=10, + basic_rate=100, + use_serial_batch_fields=0, + ) + batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + test_user, pos_profile = init_user_and_profile() + opening_entry = create_opening_entry(pos_profile, test_user.name) + + pos_inv = create_pos_invoice( + item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no + ) + pos_inv2 = create_pos_invoice( + item_code=item_code, qty=5, rate=300, use_serial_batch_fields=1, batch_no=batch_no + ) + + batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty") + self.assertEqual(batch_qty, 10) + + batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) + self.assertEqual(batch_qty_with_pos, 0.0) + + pcv_doc = make_closing_entry_from_opening(opening_entry) + pcv_doc.submit() + + piv_merge = frappe.db.get_value("POS Invoice Merge Log", {"pos_closing_entry": pcv_doc.name}, "name") + + self.assertTrue(piv_merge) + piv_merge_doc = frappe.get_doc("POS Invoice Merge Log", piv_merge) + self.assertTrue(piv_merge_doc.pos_invoices[0].pos_invoice) + self.assertTrue(piv_merge_doc.pos_invoices[1].pos_invoice) + + pos_inv.load_from_db() + self.assertTrue(pos_inv.consolidated_invoice) + pos_inv2.load_from_db() + self.assertTrue(pos_inv2.consolidated_invoice) + + batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty") + self.assertEqual(batch_qty, 0.0) + + batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) + self.assertEqual(batch_qty_with_pos, 0.0) + + frappe.flags.print_message = True + + pcv_doc.reload() + pcv_doc.cancel() + + batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty") + self.assertEqual(batch_qty, 10) + + batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) + self.assertEqual(batch_qty_with_pos, 0.0) + + pos_inv.reload() + pos_inv2.reload() + + pos_inv.cancel() + pos_inv2.cancel() + + batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) + self.assertEqual(batch_qty_with_pos, 10.0) + def init_user_and_profile(**args): user = "test@example.com" diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 34a31d52dd00..ef4db1dac980 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -229,7 +229,9 @@ def on_submit(self): self.check_phone_payments() self.set_status(update=True) self.make_bundle_for_sales_purchase_return() - self.submit_serial_batch_bundle() + for table_name in ["items", "packed_items"]: + self.make_bundle_using_old_serial_batch_fields(table_name) + self.submit_serial_batch_bundle(table_name) if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count @@ -283,10 +285,11 @@ def delink_serial_and_batch_bundle(self): {"is_cancelled": 1, "voucher_no": ""}, ) + frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle).cancel() row.db_set("serial_and_batch_bundle", None) - def submit_serial_batch_bundle(self): - for item in self.items: + def submit_serial_batch_bundle(self, table_name): + for item in self.get(table_name): if item.serial_and_batch_bundle: doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) @@ -355,10 +358,16 @@ def validate_serialised_or_batched_item(self): error_msg = [] for d in self.get("items"): error_msg = "" - if d.get("has_serial_no") and not d.serial_and_batch_bundle: + if d.get("has_serial_no") and ( + (not d.use_serial_batch_fields and not d.serial_and_batch_bundle) + or (d.use_serial_batch_fields and not d.serial_no) + ): error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}" - elif d.get("has_batch_no") and not d.serial_and_batch_bundle: + elif d.get("has_batch_no") and ( + (not d.use_serial_batch_fields and not d.serial_and_batch_bundle) + or (d.use_serial_batch_fields and not d.batch_no) + ): error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}" if error_msg: diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 1fca04950983..f210a6434cf7 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -780,8 +780,6 @@ def test_pos_batch_reservation(self): pos_inv1.submit() pos_inv1.reload() - self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle) - batches = get_auto_batch_nos( frappe._dict({"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"}) ) @@ -957,7 +955,7 @@ def create_pos_invoice(**args): pos_inv.set_missing_values() bundle_id = None - if args.get("batch_no") or args.get("serial_no"): + if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")): type_of_transaction = args.type_of_transaction or "Outward" if pos_inv.is_return: @@ -998,6 +996,9 @@ def create_pos_invoice(**args): "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_and_batch_bundle": bundle_id, + "use_serial_batch_fields": args.use_serial_batch_fields, + "serial_no": args.serial_no if args.use_serial_batch_fields else None, + "batch_no": args.batch_no if args.use_serial_batch_fields else None, } # append in pos invoice items without item_code by checking flag without_item_code if args.without_item_code: @@ -1023,6 +1024,8 @@ def create_pos_invoice(**args): pos_inv.insert() if not args.do_not_submit: pos_inv.submit() + if args.use_serial_batch_fields: + pos_inv.reload() else: pos_inv.payment_schedule = [] else: diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json index b92579eb7997..828fc30db6ee 100644 --- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json +++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json @@ -634,7 +634,6 @@ "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "batch_no", "fieldtype": "Link", - "hidden": 1, "label": "Batch No", "options": "Batch", "print_hide": 1 @@ -655,7 +654,6 @@ "depends_on": "eval:doc.use_serial_batch_fields === 1", "fieldname": "serial_no", "fieldtype": "Text", - "hidden": 1, "in_list_view": 1, "label": "Serial No", "oldfieldname": "serial_no", @@ -827,7 +825,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.use_serial_batch_fields === 1", + "depends_on": "eval:doc.use_serial_batch_fields === 0 || doc.serial_and_batch_bundle", "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", @@ -853,7 +851,7 @@ ], "istable": 1, "links": [], - "modified": "2024-02-25 15:50:17.140269", + "modified": "2024-05-07 15:56:53.343317", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice Item", @@ -863,4 +861,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index e8f94b880e2b..2cf204dd3473 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -131,6 +131,7 @@ def on_cancel(self): pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] self.update_pos_invoices(pos_invoice_docs) + self.serial_and_batch_bundle_reference_for_pos_invoice() self.cancel_linked_invoices() def process_merging_into_sales_invoice(self, data): @@ -191,6 +192,7 @@ def merge_pos_invoice_into(self, invoice, data): for i in items: if ( i.item_code == item.item_code + and not i.serial_and_batch_bundle and not i.serial_no and not i.batch_no and i.uom == item.uom @@ -312,6 +314,12 @@ def update_pos_invoices(self, invoice_docs, sales_invoice="", credit_note=""): doc.set_status(update=True) doc.save() + def serial_and_batch_bundle_reference_for_pos_invoice(self): + for d in self.pos_invoices: + pos_invoice = frappe.get_doc("POS Invoice", d.pos_invoice) + for table_name in ["items", "packed_items"]: + pos_invoice.set_serial_and_batch_bundle(table_name) + def cancel_linked_invoices(self): for si_name in [self.consolidated_invoice, self.consolidated_credit_note]: if not si_name: diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 864ceffa8b13..000a79b70a02 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -583,6 +583,7 @@ erpnext.PointOfSale.Controller = class { new_item["serial_no"] = serial_no; } + new_item["use_serial_batch_fields"] = 1; if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0; item_row = this.frm.add_child("items", new_item); diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index f89b70ef45be..4673eaa98584 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -101,7 +101,8 @@ erpnext.PointOfSale.ItemDetails = class { const serialized = item_row.has_serial_no; const batched = item_row.has_batch_no; - const no_bundle_selected = !item_row.serial_and_batch_bundle; + const no_bundle_selected = + !item_row.serial_and_batch_bundle && !item_row.serial_no && !item_row.batch_no; if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) { frappe.show_alert({ @@ -403,6 +404,7 @@ erpnext.PointOfSale.ItemDetails = class { frappe.model.set_value(item_row.doctype, item_row.name, { serial_and_batch_bundle: r.name, qty: Math.abs(r.total_qty), + use_serial_batch_fields: 0, }); } }); 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 741d1fdfae9e..06dacb1cec16 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 @@ -935,6 +935,9 @@ def on_cancel(self): self.validate_voucher_no_docstatus() def validate_voucher_no_docstatus(self): + if self.voucher_type == "POS Invoice": + return + if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1: msg = f"""The {self.voucher_type} {bold(self.voucher_no)} is in submitted state, please cancel it first""" @@ -1718,6 +1721,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict: "`tabPOS Invoice Item`.warehouse", "`tabPOS Invoice Item`.name as child_docname", "`tabPOS Invoice`.name as parent_docname", + "`tabPOS Invoice Item`.use_serial_batch_fields", "`tabPOS Invoice Item`.serial_and_batch_bundle", ], filters=[ @@ -1731,7 +1735,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict: ids = [ pos_invoice.serial_and_batch_bundle for pos_invoice in pos_invoices - if pos_invoice.serial_and_batch_bundle + if pos_invoice.serial_and_batch_bundle and not pos_invoice.use_serial_batch_fields ] if ids: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 2207b2e3c74f..4880adab9213 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -246,6 +246,9 @@ def validate_item_and_warehouse(self): frappe.throw(_(msg)) def delink_serial_and_batch_bundle(self): + if self.is_pos_transaction(): + return + update_values = { "serial_and_batch_bundle": "", } @@ -295,8 +298,22 @@ def post_process(self): self.cancel_serial_and_batch_bundle() def cancel_serial_and_batch_bundle(self): + if self.is_pos_transaction(): + return + frappe.get_cached_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle).cancel() + def is_pos_transaction(self): + if ( + self.sle.voucher_type == "Sales Invoice" + and self.sle.serial_and_batch_bundle + and frappe.get_cached_value( + "Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "voucher_type" + ) + == "POS Invoice" + ): + return True + def submit_serial_and_batch_bundle(self): doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle) self.validate_actual_qty(doc)