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 <rohitw1991@gmail.com>
This commit is contained in:
parent
0f87ec15ad
commit
2ee51d36ff
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
@ -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 = {};
|
||||
|
||||
|
@ -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))
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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))
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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', () => {
|
||||
|
@ -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))
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
):
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user