fix: serial and batch bundle for POS Invoice (backport #41491) (#42396)

* fix: serial and batch bundle for POS Invoice (#41491)

(cherry picked from commit e5dfc5e545)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
This commit is contained in:
mergify[bot]
2024-07-21 00:00:14 +05:30
committed by GitHub
parent 8da28dcfb2
commit 555be2be11
9 changed files with 149 additions and 15 deletions

View File

@@ -17,6 +17,10 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry 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.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.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 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -179,6 +183,94 @@ class TestPOSClosingEntry(unittest.TestCase):
accounting_dimension_department.save() accounting_dimension_department.save()
disable_dimension() 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): def init_user_and_profile(**args):
user = "test@example.com" user = "test@example.com"

View File

@@ -229,7 +229,9 @@ class POSInvoice(SalesInvoice):
self.check_phone_payments() self.check_phone_payments()
self.set_status(update=True) self.set_status(update=True)
self.make_bundle_for_sales_purchase_return() 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: if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count
@@ -283,10 +285,11 @@ class POSInvoice(SalesInvoice):
{"is_cancelled": 1, "voucher_no": ""}, {"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) row.db_set("serial_and_batch_bundle", None)
def submit_serial_batch_bundle(self): def submit_serial_batch_bundle(self, table_name):
for item in self.items: for item in self.get(table_name):
if item.serial_and_batch_bundle: if item.serial_and_batch_bundle:
doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
@@ -355,10 +358,16 @@ class POSInvoice(SalesInvoice):
error_msg = [] error_msg = []
for d in self.get("items"): for d in self.get("items"):
error_msg = "" 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)}" 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)}" error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}"
if error_msg: if error_msg:

View File

@@ -780,8 +780,6 @@ class TestPOSInvoice(unittest.TestCase):
pos_inv1.submit() pos_inv1.submit()
pos_inv1.reload() pos_inv1.reload()
self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle)
batches = get_auto_batch_nos( batches = get_auto_batch_nos(
frappe._dict({"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"}) 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() pos_inv.set_missing_values()
bundle_id = None 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" type_of_transaction = args.type_of_transaction or "Outward"
if pos_inv.is_return: 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", "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_and_batch_bundle": bundle_id, "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 # append in pos invoice items without item_code by checking flag without_item_code
if args.without_item_code: if args.without_item_code:
@@ -1023,6 +1024,8 @@ def create_pos_invoice(**args):
pos_inv.insert() pos_inv.insert()
if not args.do_not_submit: if not args.do_not_submit:
pos_inv.submit() pos_inv.submit()
if args.use_serial_batch_fields:
pos_inv.reload()
else: else:
pos_inv.payment_schedule = [] pos_inv.payment_schedule = []
else: else:

View File

@@ -634,7 +634,6 @@
"depends_on": "eval:doc.use_serial_batch_fields === 1", "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1,
"label": "Batch No", "label": "Batch No",
"options": "Batch", "options": "Batch",
"print_hide": 1 "print_hide": 1
@@ -655,7 +654,6 @@
"depends_on": "eval:doc.use_serial_batch_fields === 1", "depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Serial No", "label": "Serial No",
"oldfieldname": "serial_no", "oldfieldname": "serial_no",
@@ -827,7 +825,7 @@
"read_only": 1 "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", "fieldname": "serial_and_batch_bundle",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Serial and Batch Bundle", "label": "Serial and Batch Bundle",
@@ -853,7 +851,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-02-25 15:50:17.140269", "modified": "2024-05-07 15:56:53.343317",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",
@@ -863,4 +861,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -131,6 +131,7 @@ class POSInvoiceMergeLog(Document):
pos_invoice_docs = [frappe.get_cached_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] 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.update_pos_invoices(pos_invoice_docs)
self.serial_and_batch_bundle_reference_for_pos_invoice()
self.cancel_linked_invoices() self.cancel_linked_invoices()
def process_merging_into_sales_invoice(self, data): def process_merging_into_sales_invoice(self, data):
@@ -191,6 +192,7 @@ class POSInvoiceMergeLog(Document):
for i in items: for i in items:
if ( if (
i.item_code == item.item_code i.item_code == item.item_code
and not i.serial_and_batch_bundle
and not i.serial_no and not i.serial_no
and not i.batch_no and not i.batch_no
and i.uom == item.uom and i.uom == item.uom
@@ -312,6 +314,12 @@ class POSInvoiceMergeLog(Document):
doc.set_status(update=True) doc.set_status(update=True)
doc.save() 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): def cancel_linked_invoices(self):
for si_name in [self.consolidated_invoice, self.consolidated_credit_note]: for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
if not si_name: if not si_name:

View File

@@ -583,6 +583,7 @@ erpnext.PointOfSale.Controller = class {
new_item["serial_no"] = serial_no; 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; if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0;
item_row = this.frm.add_child("items", new_item); item_row = this.frm.add_child("items", new_item);

View File

@@ -101,7 +101,8 @@ erpnext.PointOfSale.ItemDetails = class {
const serialized = item_row.has_serial_no; const serialized = item_row.has_serial_no;
const batched = item_row.has_batch_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)) { if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) {
frappe.show_alert({ frappe.show_alert({
@@ -403,6 +404,7 @@ erpnext.PointOfSale.ItemDetails = class {
frappe.model.set_value(item_row.doctype, item_row.name, { frappe.model.set_value(item_row.doctype, item_row.name, {
serial_and_batch_bundle: r.name, serial_and_batch_bundle: r.name,
qty: Math.abs(r.total_qty), qty: Math.abs(r.total_qty),
use_serial_batch_fields: 0,
}); });
} }
}); });

View File

@@ -935,6 +935,9 @@ class SerialandBatchBundle(Document):
self.validate_voucher_no_docstatus() self.validate_voucher_no_docstatus()
def validate_voucher_no_docstatus(self): 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: if frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1:
msg = f"""The {self.voucher_type} {bold(self.voucher_no)} msg = f"""The {self.voucher_type} {bold(self.voucher_no)}
is in submitted state, please cancel it first""" is in submitted state, please cancel it first"""
@@ -1722,6 +1725,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
"`tabPOS Invoice Item`.warehouse", "`tabPOS Invoice Item`.warehouse",
"`tabPOS Invoice Item`.name as child_docname", "`tabPOS Invoice Item`.name as child_docname",
"`tabPOS Invoice`.name as parent_docname", "`tabPOS Invoice`.name as parent_docname",
"`tabPOS Invoice Item`.use_serial_batch_fields",
"`tabPOS Invoice Item`.serial_and_batch_bundle", "`tabPOS Invoice Item`.serial_and_batch_bundle",
], ],
filters=[ filters=[
@@ -1735,7 +1739,7 @@ def get_reserved_batches_for_pos(kwargs) -> dict:
ids = [ ids = [
pos_invoice.serial_and_batch_bundle pos_invoice.serial_and_batch_bundle
for pos_invoice in pos_invoices 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: if ids:

View File

@@ -246,6 +246,9 @@ class SerialBatchBundle:
frappe.throw(_(msg)) frappe.throw(_(msg))
def delink_serial_and_batch_bundle(self): def delink_serial_and_batch_bundle(self):
if self.is_pos_transaction():
return
update_values = { update_values = {
"serial_and_batch_bundle": "", "serial_and_batch_bundle": "",
} }
@@ -295,8 +298,22 @@ class SerialBatchBundle:
self.cancel_serial_and_batch_bundle() self.cancel_serial_and_batch_bundle()
def cancel_serial_and_batch_bundle(self): 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() 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): def submit_serial_and_batch_bundle(self):
doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle) doc = frappe.get_doc("Serial and Batch Bundle", self.sle.serial_and_batch_bundle)
self.validate_actual_qty(doc) self.validate_actual_qty(doc)