Merge pull request #47988 from rohitwaghchaure/fixed-support-40403
fix: stock reconciliation validation for serial nos
This commit is contained in:
@@ -226,7 +226,14 @@ class SerialandBatchBundle(Document):
|
||||
if not (self.has_serial_no and self.type_of_transaction == "Outward"):
|
||||
return
|
||||
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
if self.voucher_type == "Stock Reconciliation":
|
||||
serial_nos = self.get_serial_nos_for_validate()
|
||||
else:
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
kwargs = {
|
||||
"item_code": self.item_code,
|
||||
"warehouse": self.warehouse,
|
||||
@@ -239,6 +246,15 @@ class SerialandBatchBundle(Document):
|
||||
if self.voucher_type == "POS Invoice":
|
||||
kwargs["ignore_voucher_nos"] = [self.voucher_no]
|
||||
|
||||
if self.voucher_type == "Stock Reconciliation":
|
||||
kwargs.update(
|
||||
{
|
||||
"voucher_no": self.voucher_no,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
}
|
||||
)
|
||||
|
||||
available_serial_nos = get_available_serial_nos(frappe._dict(kwargs))
|
||||
|
||||
serial_no_warehouse = {}
|
||||
@@ -669,14 +685,17 @@ class SerialandBatchBundle(Document):
|
||||
):
|
||||
self.throw_error_message(f"The {self.voucher_type} # {self.voucher_no} should be submit first.")
|
||||
|
||||
def check_future_entries_exists(self):
|
||||
def check_future_entries_exists(self, is_cancelled=False):
|
||||
if self.flags and self.flags.via_landed_cost_voucher:
|
||||
return
|
||||
|
||||
if not self.has_serial_no:
|
||||
return
|
||||
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
if self.voucher_type == "Stock Reconciliation":
|
||||
serial_nos = self.get_serial_nos_for_validate(is_cancelled=is_cancelled)
|
||||
else:
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
|
||||
if not serial_nos:
|
||||
return
|
||||
@@ -724,6 +743,36 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError)
|
||||
|
||||
def get_serial_nos_for_validate(self, is_cancelled=False):
|
||||
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
|
||||
skip_serial_nos = self.get_skip_serial_nos_for_stock_reconciliation(is_cancelled=is_cancelled)
|
||||
serial_nos = list(set(sorted(serial_nos)) - set(sorted(skip_serial_nos)))
|
||||
|
||||
return serial_nos
|
||||
|
||||
def get_skip_serial_nos_for_stock_reconciliation(self, is_cancelled=False):
|
||||
data = get_stock_reco_details(self.voucher_detail_no)
|
||||
if not data:
|
||||
return []
|
||||
|
||||
if data.current_serial_no:
|
||||
current_serial_nos = set(parse_serial_nos(data.current_serial_no))
|
||||
serial_nos = set(parse_serial_nos(data.serial_no)) if data.serial_no else set([])
|
||||
return list(serial_nos.intersection(current_serial_nos))
|
||||
elif data.current_serial_and_batch_bundle:
|
||||
current_serial_nos = set(get_serial_nos_from_bundle(data.current_serial_and_batch_bundle))
|
||||
if is_cancelled:
|
||||
return current_serial_nos
|
||||
|
||||
serial_nos = (
|
||||
set(get_serial_nos_from_bundle(data.serial_and_batch_bundle))
|
||||
if data.serial_and_batch_bundle
|
||||
else set([])
|
||||
)
|
||||
return list(serial_nos.intersection(current_serial_nos))
|
||||
|
||||
return []
|
||||
|
||||
def reset_qty(self, row, qty_field=None):
|
||||
qty_field = self.get_qty_field(row, qty_field=qty_field)
|
||||
qty = abs(flt(row.get(qty_field), self.precision("total_qty")))
|
||||
@@ -927,8 +976,6 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
def validate_serial_and_batch_no_for_returned(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
if not self.returned_against:
|
||||
return
|
||||
|
||||
@@ -953,7 +1000,7 @@ class SerialandBatchBundle(Document):
|
||||
if d.serial_and_batch_bundle:
|
||||
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
|
||||
else:
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
serial_nos = parse_serial_nos(d.serial_no)
|
||||
|
||||
elif self.has_batch_no:
|
||||
if d.serial_and_batch_bundle:
|
||||
@@ -1115,7 +1162,7 @@ class SerialandBatchBundle(Document):
|
||||
).run()
|
||||
|
||||
def validate_serial_and_batch_inventory(self):
|
||||
self.check_future_entries_exists()
|
||||
self.check_future_entries_exists(is_cancelled=True)
|
||||
self.validate_batch_inventory()
|
||||
|
||||
def validate_batch_inventory(self):
|
||||
@@ -1422,13 +1469,6 @@ def make_batch_nos(item_code, batch_nos):
|
||||
frappe.msgprint(_("Batch Nos are created successfully"), alert=True)
|
||||
|
||||
|
||||
def parse_serial_nos(data):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
|
||||
return [s.strip() for s in cstr(data).strip().replace(",", "\n").split("\n") if s.strip()]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
@@ -1815,8 +1855,6 @@ def get_non_expired_batches(batches):
|
||||
|
||||
|
||||
def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
serial_nos = set()
|
||||
data = get_stock_ledgers_for_serial_nos(kwargs)
|
||||
|
||||
@@ -1830,13 +1868,14 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
|
||||
serial_nos.difference_update(sns)
|
||||
|
||||
elif d.serial_no:
|
||||
sns = get_serial_nos(d.serial_no)
|
||||
sns = parse_serial_nos(d.serial_no)
|
||||
if d.actual_qty > 0:
|
||||
serial_nos.update(sns)
|
||||
else:
|
||||
serial_nos.difference_update(sns)
|
||||
|
||||
serial_nos = list(serial_nos)
|
||||
|
||||
for serial_no in ignore_serial_nos:
|
||||
if serial_no in serial_nos:
|
||||
serial_nos.remove(serial_no)
|
||||
@@ -1942,7 +1981,6 @@ def get_reserved_voucher_details(kwargs):
|
||||
|
||||
def get_reserved_serial_nos_for_pos(kwargs):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_returned_serial_nos
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
ignore_serial_nos = []
|
||||
pos_invoices = frappe.get_all(
|
||||
@@ -1978,7 +2016,7 @@ def get_reserved_serial_nos_for_pos(kwargs):
|
||||
returned_serial_nos = []
|
||||
for pos_invoice in pos_invoices:
|
||||
if pos_invoice.serial_no:
|
||||
ignore_serial_nos.extend(get_serial_nos(pos_invoice.serial_no))
|
||||
ignore_serial_nos.extend(parse_serial_nos(pos_invoice.serial_no))
|
||||
|
||||
if pos_invoice.is_return:
|
||||
continue
|
||||
@@ -2515,12 +2553,14 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
||||
query = (
|
||||
frappe.qb.from_(stock_ledger_entry)
|
||||
.select(
|
||||
stock_ledger_entry.posting_datetime,
|
||||
stock_ledger_entry.actual_qty,
|
||||
stock_ledger_entry.serial_no,
|
||||
stock_ledger_entry.serial_and_batch_bundle,
|
||||
)
|
||||
.where(stock_ledger_entry.is_cancelled == 0)
|
||||
.orderby(stock_ledger_entry.posting_datetime)
|
||||
.orderby(stock_ledger_entry.creation)
|
||||
)
|
||||
|
||||
if kwargs.get("posting_date"):
|
||||
@@ -2649,3 +2689,20 @@ def make_batch_no(batch_no, item_code):
|
||||
@frappe.whitelist()
|
||||
def is_duplicate_serial_no(bundle_id, serial_no):
|
||||
return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})
|
||||
|
||||
|
||||
def parse_serial_nos(serial_no):
|
||||
if isinstance(serial_no, list):
|
||||
return serial_no
|
||||
|
||||
return [s.strip() for s in cstr(serial_no).strip().replace(",", "\n").split("\n") if s.strip()]
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_stock_reco_details(voucher_detail_no):
|
||||
return frappe.db.get_value(
|
||||
"Stock Reconciliation Item",
|
||||
voucher_detail_no,
|
||||
["current_serial_no", "serial_no", "serial_and_batch_bundle", "current_serial_and_batch_bundle"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
@@ -373,16 +373,26 @@ class SerialBatchBundle:
|
||||
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
|
||||
serial_nos = get_parsed_serial_nos(self.sle.serial_no)
|
||||
|
||||
warehouse = self.warehouse if self.sle.actual_qty > 0 else None
|
||||
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
if self.sle.voucher_type == "Stock Reconciliation" and self.sle.actual_qty > 0:
|
||||
self.update_serial_no_status_for_stock_reco(serial_nos)
|
||||
return
|
||||
|
||||
self.update_serial_no_status_warehouse(self.sle, serial_nos)
|
||||
|
||||
def update_serial_no_status_warehouse(self, sle, serial_nos):
|
||||
warehouse = self.warehouse if sle.actual_qty > 0 else None
|
||||
|
||||
if isinstance(serial_nos, str):
|
||||
serial_nos = [serial_nos]
|
||||
|
||||
status = "Inactive"
|
||||
if self.sle.actual_qty < 0:
|
||||
if sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
if self.sle.voucher_type == "Stock Entry":
|
||||
purpose = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose")
|
||||
if sle.voucher_type == "Stock Entry":
|
||||
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if purpose in [
|
||||
"Manufacture",
|
||||
"Material Issue",
|
||||
@@ -401,17 +411,17 @@ class SerialBatchBundle:
|
||||
"Active"
|
||||
if warehouse
|
||||
else status
|
||||
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
|
||||
if (sn_table.purchase_document_no != sle.voucher_no and sle.is_cancelled != 1)
|
||||
else "Inactive",
|
||||
)
|
||||
.set(sn_table.company, self.sle.company)
|
||||
.set(sn_table.company, sle.company)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
)
|
||||
|
||||
if status == "Delivered":
|
||||
warranty_period = frappe.get_cached_value("Item", self.sle.item_code, "warranty_period")
|
||||
warranty_period = frappe.get_cached_value("Item", sle.item_code, "warranty_period")
|
||||
if warranty_period:
|
||||
warranty_expiry_date = add_days(self.sle.posting_date, cint(warranty_period))
|
||||
warranty_expiry_date = add_days(sle.posting_date, cint(warranty_period))
|
||||
query = query.set(sn_table.warranty_expiry_date, warranty_expiry_date)
|
||||
query = query.set(sn_table.warranty_period, warranty_period)
|
||||
else:
|
||||
@@ -420,6 +430,39 @@ class SerialBatchBundle:
|
||||
|
||||
query.run()
|
||||
|
||||
def update_serial_no_status_for_stock_reco(self, serial_nos):
|
||||
for serial_no in serial_nos:
|
||||
sle_doctype = frappe.qb.DocType("Stock Ledger Entry")
|
||||
sn_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sle_doctype)
|
||||
.inner_join(sn_table)
|
||||
.on(sle_doctype.serial_and_batch_bundle == sn_table.parent)
|
||||
.select(
|
||||
sle_doctype.warehouse,
|
||||
sle_doctype.actual_qty,
|
||||
sle_doctype.voucher_type,
|
||||
sle_doctype.voucher_no,
|
||||
sle_doctype.is_cancelled,
|
||||
sle_doctype.item_code,
|
||||
sle_doctype.posting_date,
|
||||
sle_doctype.company,
|
||||
)
|
||||
.where(
|
||||
(sn_table.serial_no == serial_no)
|
||||
& (sle_doctype.is_cancelled == 0)
|
||||
& (sn_table.docstatus == 1)
|
||||
)
|
||||
.orderby(sle_doctype.posting_datetime, order=Order.desc)
|
||||
.orderby(sle_doctype.creation, order=Order.desc)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
sle = query.run(as_dict=1)
|
||||
if sle:
|
||||
self.update_serial_no_status_warehouse(sle[0], serial_no)
|
||||
|
||||
def set_batch_no_in_serial_nos(self):
|
||||
entries = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
|
||||
@@ -1939,6 +1939,9 @@ def get_stock_reco_qty_shift(args):
|
||||
stock_reco_qty_shift = 0
|
||||
if args.get("is_cancelled"):
|
||||
if args.get("previous_qty_after_transaction"):
|
||||
if args.get("serial_and_batch_bundle"):
|
||||
return args.get("previous_qty_after_transaction")
|
||||
|
||||
# get qty (balance) that was set at submission
|
||||
last_balance = args.get("previous_qty_after_transaction")
|
||||
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
|
||||
|
||||
Reference in New Issue
Block a user