fix: stock reconciliation validation for serial and batch
This commit is contained in:
@@ -226,7 +226,14 @@ class SerialandBatchBundle(Document):
|
|||||||
if not (self.has_serial_no and self.type_of_transaction == "Outward"):
|
if not (self.has_serial_no and self.type_of_transaction == "Outward"):
|
||||||
return
|
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 = {
|
kwargs = {
|
||||||
"item_code": self.item_code,
|
"item_code": self.item_code,
|
||||||
"warehouse": self.warehouse,
|
"warehouse": self.warehouse,
|
||||||
@@ -239,6 +246,15 @@ class SerialandBatchBundle(Document):
|
|||||||
if self.voucher_type == "POS Invoice":
|
if self.voucher_type == "POS Invoice":
|
||||||
kwargs["ignore_voucher_nos"] = [self.voucher_no]
|
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))
|
available_serial_nos = get_available_serial_nos(frappe._dict(kwargs))
|
||||||
|
|
||||||
serial_no_warehouse = {}
|
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.")
|
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:
|
if self.flags and self.flags.via_landed_cost_voucher:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.has_serial_no:
|
if not self.has_serial_no:
|
||||||
return
|
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:
|
if not serial_nos:
|
||||||
return
|
return
|
||||||
@@ -724,6 +743,36 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransactionError)
|
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):
|
def reset_qty(self, row, qty_field=None):
|
||||||
qty_field = self.get_qty_field(row, qty_field=qty_field)
|
qty_field = self.get_qty_field(row, qty_field=qty_field)
|
||||||
qty = abs(flt(row.get(qty_field), self.precision("total_qty")))
|
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):
|
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:
|
if not self.returned_against:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -953,7 +1000,7 @@ class SerialandBatchBundle(Document):
|
|||||||
if d.serial_and_batch_bundle:
|
if d.serial_and_batch_bundle:
|
||||||
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
|
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
|
||||||
else:
|
else:
|
||||||
serial_nos = get_serial_nos(d.serial_no)
|
serial_nos = parse_serial_nos(d.serial_no)
|
||||||
|
|
||||||
elif self.has_batch_no:
|
elif self.has_batch_no:
|
||||||
if d.serial_and_batch_bundle:
|
if d.serial_and_batch_bundle:
|
||||||
@@ -1115,7 +1162,7 @@ class SerialandBatchBundle(Document):
|
|||||||
).run()
|
).run()
|
||||||
|
|
||||||
def validate_serial_and_batch_inventory(self):
|
def validate_serial_and_batch_inventory(self):
|
||||||
self.check_future_entries_exists()
|
self.check_future_entries_exists(is_cancelled=True)
|
||||||
self.validate_batch_inventory()
|
self.validate_batch_inventory()
|
||||||
|
|
||||||
def validate_batch_inventory(self):
|
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)
|
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.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
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):
|
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()
|
serial_nos = set()
|
||||||
data = get_stock_ledgers_for_serial_nos(kwargs)
|
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)
|
serial_nos.difference_update(sns)
|
||||||
|
|
||||||
elif d.serial_no:
|
elif d.serial_no:
|
||||||
sns = get_serial_nos(d.serial_no)
|
sns = parse_serial_nos(d.serial_no)
|
||||||
if d.actual_qty > 0:
|
if d.actual_qty > 0:
|
||||||
serial_nos.update(sns)
|
serial_nos.update(sns)
|
||||||
else:
|
else:
|
||||||
serial_nos.difference_update(sns)
|
serial_nos.difference_update(sns)
|
||||||
|
|
||||||
serial_nos = list(serial_nos)
|
serial_nos = list(serial_nos)
|
||||||
|
|
||||||
for serial_no in ignore_serial_nos:
|
for serial_no in ignore_serial_nos:
|
||||||
if serial_no in serial_nos:
|
if serial_no in serial_nos:
|
||||||
serial_nos.remove(serial_no)
|
serial_nos.remove(serial_no)
|
||||||
@@ -1942,7 +1981,6 @@ def get_reserved_voucher_details(kwargs):
|
|||||||
|
|
||||||
def get_reserved_serial_nos_for_pos(kwargs):
|
def get_reserved_serial_nos_for_pos(kwargs):
|
||||||
from erpnext.controllers.sales_and_purchase_return import get_returned_serial_nos
|
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 = []
|
ignore_serial_nos = []
|
||||||
pos_invoices = frappe.get_all(
|
pos_invoices = frappe.get_all(
|
||||||
@@ -1978,7 +2016,7 @@ def get_reserved_serial_nos_for_pos(kwargs):
|
|||||||
returned_serial_nos = []
|
returned_serial_nos = []
|
||||||
for pos_invoice in pos_invoices:
|
for pos_invoice in pos_invoices:
|
||||||
if pos_invoice.serial_no:
|
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:
|
if pos_invoice.is_return:
|
||||||
continue
|
continue
|
||||||
@@ -2515,12 +2553,14 @@ def get_stock_ledgers_for_serial_nos(kwargs):
|
|||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(stock_ledger_entry)
|
frappe.qb.from_(stock_ledger_entry)
|
||||||
.select(
|
.select(
|
||||||
|
stock_ledger_entry.posting_datetime,
|
||||||
stock_ledger_entry.actual_qty,
|
stock_ledger_entry.actual_qty,
|
||||||
stock_ledger_entry.serial_no,
|
stock_ledger_entry.serial_no,
|
||||||
stock_ledger_entry.serial_and_batch_bundle,
|
stock_ledger_entry.serial_and_batch_bundle,
|
||||||
)
|
)
|
||||||
.where(stock_ledger_entry.is_cancelled == 0)
|
.where(stock_ledger_entry.is_cancelled == 0)
|
||||||
.orderby(stock_ledger_entry.posting_datetime)
|
.orderby(stock_ledger_entry.posting_datetime)
|
||||||
|
.orderby(stock_ledger_entry.creation)
|
||||||
)
|
)
|
||||||
|
|
||||||
if kwargs.get("posting_date"):
|
if kwargs.get("posting_date"):
|
||||||
@@ -2649,3 +2689,20 @@ def make_batch_no(batch_no, item_code):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def is_duplicate_serial_no(bundle_id, serial_no):
|
def is_duplicate_serial_no(bundle_id, serial_no):
|
||||||
return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": 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:
|
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
|
||||||
serial_nos = get_parsed_serial_nos(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:
|
if not serial_nos:
|
||||||
return
|
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"
|
status = "Inactive"
|
||||||
if self.sle.actual_qty < 0:
|
if sle.actual_qty < 0:
|
||||||
status = "Delivered"
|
status = "Delivered"
|
||||||
if self.sle.voucher_type == "Stock Entry":
|
if sle.voucher_type == "Stock Entry":
|
||||||
purpose = frappe.get_cached_value("Stock Entry", self.sle.voucher_no, "purpose")
|
purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose")
|
||||||
if purpose in [
|
if purpose in [
|
||||||
"Manufacture",
|
"Manufacture",
|
||||||
"Material Issue",
|
"Material Issue",
|
||||||
@@ -401,17 +411,17 @@ class SerialBatchBundle:
|
|||||||
"Active"
|
"Active"
|
||||||
if warehouse
|
if warehouse
|
||||||
else status
|
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",
|
else "Inactive",
|
||||||
)
|
)
|
||||||
.set(sn_table.company, self.sle.company)
|
.set(sn_table.company, sle.company)
|
||||||
.where(sn_table.name.isin(serial_nos))
|
.where(sn_table.name.isin(serial_nos))
|
||||||
)
|
)
|
||||||
|
|
||||||
if status == "Delivered":
|
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:
|
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_expiry_date, warranty_expiry_date)
|
||||||
query = query.set(sn_table.warranty_period, warranty_period)
|
query = query.set(sn_table.warranty_period, warranty_period)
|
||||||
else:
|
else:
|
||||||
@@ -420,6 +430,39 @@ class SerialBatchBundle:
|
|||||||
|
|
||||||
query.run()
|
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):
|
def set_batch_no_in_serial_nos(self):
|
||||||
entries = frappe.get_all(
|
entries = frappe.get_all(
|
||||||
"Serial and Batch Entry",
|
"Serial and Batch Entry",
|
||||||
|
|||||||
@@ -1936,6 +1936,9 @@ def get_stock_reco_qty_shift(args):
|
|||||||
stock_reco_qty_shift = 0
|
stock_reco_qty_shift = 0
|
||||||
if args.get("is_cancelled"):
|
if args.get("is_cancelled"):
|
||||||
if args.get("previous_qty_after_transaction"):
|
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
|
# get qty (balance) that was set at submission
|
||||||
last_balance = args.get("previous_qty_after_transaction")
|
last_balance = args.get("previous_qty_after_transaction")
|
||||||
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
|
stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
|
||||||
|
|||||||
Reference in New Issue
Block a user