mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 23:39:28 +00:00
fix(stock): preserve serial selection and use internal IDs in tests
This commit is contained in:
@@ -818,7 +818,7 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
)
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
|
||||
batch_no = create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01")
|
||||
item = frappe.get_doc("Item", "_BATCH ITEM")
|
||||
|
||||
se = make_stock_entry(
|
||||
@@ -826,12 +826,10 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
item_code="_BATCH ITEM",
|
||||
qty=2,
|
||||
basic_rate=100,
|
||||
batch_no="TestBatch 01",
|
||||
batch_no=batch_no,
|
||||
)
|
||||
|
||||
pos_inv1 = create_pos_invoice(
|
||||
item=item.name, rate=300, qty=1, do_not_submit=1, batch_no="TestBatch 01"
|
||||
)
|
||||
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1, batch_no=batch_no)
|
||||
pos_inv1.append(
|
||||
"payments",
|
||||
{"mode_of_payment": "Cash", "amount": 300},
|
||||
@@ -849,7 +847,7 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
"voucher_no": pos_inv2.name,
|
||||
"qty": 2,
|
||||
"avg_rate": 300,
|
||||
"batches": frappe._dict({"TestBatch 01": 2}),
|
||||
"batches": frappe._dict({batch_no: 2}),
|
||||
"type_of_transaction": "Outward",
|
||||
"company": pos_inv2.company,
|
||||
}
|
||||
@@ -925,6 +923,7 @@ class TestPOSInvoice(POSInvoiceTestMixin):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos_inv.submit)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||
def test_bundle_stock_availability_validation(self):
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
@@ -36,6 +36,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
|
||||
make_serial_batch_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.stock.tests.test_utils import StockTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
@@ -2643,25 +2644,8 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
batch_no = "BATCH-PI-BNU-TPRBI-0001"
|
||||
serial_nos = ["SNU-PI-TPRSI-0001", "SNU-PI-TPRSI-0002", "SNU-PI-TPRSI-0003"]
|
||||
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": batch_item,
|
||||
}
|
||||
).insert()
|
||||
|
||||
for serial_no in serial_nos:
|
||||
if not frappe.db.exists("Serial No", serial_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": serial_item,
|
||||
"serial_no": serial_no,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
batch_no = SerialBatchIdentity("Batch").resolve(batch_item, [batch_no], create=True)[0]
|
||||
serial_nos = SerialBatchIdentity("Serial No").resolve(serial_item, serial_nos, create=True)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=batch_item,
|
||||
|
||||
@@ -21,6 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation, get_serial_nos_from_bundle
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
|
||||
@@ -433,9 +434,10 @@ class SubcontractingController(StockController):
|
||||
consumed_bundles = voucher_bundle_data.get(bundle_key, frappe._dict())
|
||||
|
||||
if consumed_bundles.serial_nos:
|
||||
self.available_materials[key]["serial_no"] = list(
|
||||
set(self.available_materials[key]["serial_no"]) - set(consumed_bundles.serial_nos)
|
||||
)
|
||||
consumed_serials = set(consumed_bundles.serial_nos)
|
||||
self.available_materials[key]["serial_no"] = [
|
||||
sn for sn in self.available_materials[key]["serial_no"] if sn not in consumed_serials
|
||||
]
|
||||
|
||||
if consumed_bundles.batch_nos:
|
||||
for batch_no, qty in consumed_bundles.batch_nos.items():
|
||||
@@ -449,9 +451,10 @@ class SubcontractingController(StockController):
|
||||
from erpnext.deprecation_dumpster import deprecation_warning
|
||||
|
||||
deprecation_warning("unknown", "v16", "No instructions.")
|
||||
self.available_materials[key]["serial_no"] = list(
|
||||
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
consumed_serials = set(get_serial_nos(row.serial_no))
|
||||
self.available_materials[key]["serial_no"] = [
|
||||
sn for sn in self.available_materials[key]["serial_no"] if sn not in consumed_serials
|
||||
]
|
||||
|
||||
# Will be deprecated in v16
|
||||
if row.batch_no and not consumed_bundles.batch_nos:
|
||||
@@ -531,6 +534,12 @@ class SubcontractingController(StockController):
|
||||
|
||||
self.__set_alternative_item_details(row)
|
||||
|
||||
serial_numbers = SerialBatchIdentity("Serial No").labels(
|
||||
[sn for details in self.available_materials.values() for sn in details.serial_no]
|
||||
)
|
||||
for details in self.available_materials.values():
|
||||
details.serial_no.sort(key=lambda sn: serial_numbers.get(sn) or sn)
|
||||
|
||||
self.__transferred_items = copy.deepcopy(self.available_materials)
|
||||
self.__update_consumed_materials("Subcontracting Receipt")
|
||||
|
||||
@@ -682,7 +691,7 @@ class SubcontractingController(StockController):
|
||||
return available_batches
|
||||
|
||||
def __get_serial_nos_for_bundle(self, qty, key):
|
||||
available_sns = sorted(self.available_materials[key]["serial_no"])[0 : cint(qty)]
|
||||
available_sns = self.available_materials[key]["serial_no"][0 : cint(qty)]
|
||||
serial_nos = []
|
||||
|
||||
for serial_no in available_sns:
|
||||
|
||||
@@ -995,9 +995,9 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
if value.get(field):
|
||||
data = value.get(field)
|
||||
if field == "serial_no":
|
||||
data = sorted(data)
|
||||
|
||||
self.assertEqual(data, transferred_detais.get(field))
|
||||
self.assertCountEqual(data, transferred_detais.get(field))
|
||||
else:
|
||||
self.assertEqual(data, transferred_detais.get(field))
|
||||
|
||||
scr2 = make_subcontracting_receipt(sco.name)
|
||||
scr2.save()
|
||||
@@ -1010,9 +1010,9 @@ class TestSubcontractingController(ERPNextTestSuite):
|
||||
if value.get(field):
|
||||
data = value.get(field)
|
||||
if field == "serial_no":
|
||||
data = sorted(data)
|
||||
|
||||
self.assertEqual(data, transferred_detais.get(field))
|
||||
self.assertCountEqual(data, transferred_detais.get(field))
|
||||
else:
|
||||
self.assertEqual(data, transferred_detais.get(field))
|
||||
|
||||
def test_subcontracting_with_same_components_different_fg_with_serial_batch_fields(self):
|
||||
"""
|
||||
@@ -1338,7 +1338,7 @@ def make_stock_transfer_entry(**args):
|
||||
batches = defaultdict(float)
|
||||
if item_details and item_details.serial_no:
|
||||
serial_nos = item_details.serial_no[0 : cint(row.qty)]
|
||||
item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
|
||||
item_details.serial_no = item_details.serial_no[cint(row.qty) :]
|
||||
|
||||
if item_details and item_details.batch_no:
|
||||
for batch_no, batch_qty in item_details.batch_no.items():
|
||||
|
||||
@@ -1860,7 +1860,7 @@ class TestJobCard(ERPNextTestSuite):
|
||||
self.assertEqual(len(entries), 5)
|
||||
for entry in entries:
|
||||
self.assertEqual(flt(entry.qty), 10.0)
|
||||
self.assertTrue(entry.batch_no.startswith("BS-ROD-PC-"))
|
||||
self.assertTrue(frappe.db.get_value("Batch", entry.batch_no, "batch_id").startswith("BS-ROD-PC-"))
|
||||
self.assertEqual(frappe.db.get_value("Batch", entry.batch_no, "parent_batch"), parent_batch)
|
||||
|
||||
manufacture_entry.reload()
|
||||
|
||||
@@ -36,6 +36,7 @@ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import OperationsNotCompleteError
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.stock.utils import get_bin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
@@ -1971,6 +1972,7 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
self.assertAlmostEqual(rows["Stores - _TC"], flt(first.required_qty) * 4 / 10, places=6)
|
||||
self.assertAlmostEqual(rows["_Test Warehouse 1 - _TC"], 5 * 4 / 10, places=6)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Buying Settings", {"allow_multiple_items": 0})
|
||||
def test_allocation_collapses_groups_when_multiple_items_disallowed(self):
|
||||
work_order = make_wo_order_test_record(
|
||||
planned_start_date=now(), qty=10, source_warehouse="Stores - _TC"
|
||||
@@ -2163,6 +2165,8 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
transferred_ste_doc.items[0].serial_no = "\n".join(serial_nos_list)
|
||||
transferred_ste_doc.items[0].serial_and_batch_bundle = None
|
||||
transferred_ste_doc.items[0].use_serial_batch_fields = 1
|
||||
transferred_ste_doc.submit()
|
||||
|
||||
# First Manufacture stock entry
|
||||
@@ -3770,8 +3774,12 @@ class TestWorkOrder(ERPNextTestSuite):
|
||||
|
||||
# Pre-generate two sets of FG serial numbers
|
||||
series = frappe.db.get_value("Item", fg_item, "serial_no_series")
|
||||
fg_serials_1 = [make_autoname(series) for _ in range(3)]
|
||||
fg_serials_2 = [make_autoname(series) for _ in range(3)]
|
||||
fg_serials_1 = SerialBatchIdentity("Serial No").resolve(
|
||||
fg_item, [make_autoname(series) for _ in range(3)], create=True
|
||||
)
|
||||
fg_serials_2 = SerialBatchIdentity("Serial No").resolve(
|
||||
fg_item, [make_autoname(series) for _ in range(3)], create=True
|
||||
)
|
||||
|
||||
# Manufacture entry 1 — consumes rm_serials_1, produces fg_serials_1
|
||||
se_manufacture_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 3))
|
||||
|
||||
@@ -3453,8 +3453,8 @@ class TestSalesOrder(ERPNextTestSuite):
|
||||
serial_nos_in_bundle = get_serial_nos(dn.packed_items[1].serial_and_batch_bundle)
|
||||
batches_in_bundle = list(get_batches_from_bundle(dn.packed_items[1].serial_and_batch_bundle).keys())
|
||||
|
||||
self.assertEqual(sre_serial_nos, serial_nos_in_bundle)
|
||||
self.assertEqual(sre_batch_nos, batches_in_bundle)
|
||||
self.assertCountEqual(sre_serial_nos, serial_nos_in_bundle)
|
||||
self.assertCountEqual(sre_batch_nos, batches_in_bundle)
|
||||
|
||||
dn.items[0].qty = 5
|
||||
dn.save()
|
||||
@@ -3495,8 +3495,8 @@ class TestSalesOrder(ERPNextTestSuite):
|
||||
serial_nos_in_bundle = get_serial_nos(si.packed_items[1].serial_and_batch_bundle)
|
||||
batches_in_bundle = list(get_batches_from_bundle(si.packed_items[1].serial_and_batch_bundle).keys())
|
||||
|
||||
self.assertEqual(sre_serial_nos, serial_nos_in_bundle)
|
||||
self.assertEqual(sre_batch_nos, batches_in_bundle)
|
||||
self.assertCountEqual(sre_serial_nos, serial_nos_in_bundle)
|
||||
self.assertCountEqual(sre_batch_nos, batches_in_bundle)
|
||||
|
||||
si.items[0].qty = 5
|
||||
si.save()
|
||||
|
||||
@@ -20,6 +20,7 @@ from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import (
|
||||
SerialNoInventoryDimensionError,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -503,6 +504,7 @@ class TestInventoryDimension(ERPNextTestSuite):
|
||||
{"has_serial_no": 1, "is_stock_item": 1},
|
||||
)
|
||||
serial_no = "Test Serialized Inventory Dimension Serial No"
|
||||
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
|
||||
warehouse = create_warehouse("Serialized Inventory Dimension Warehouse")
|
||||
|
||||
create_inventory_dimension(
|
||||
@@ -563,6 +565,7 @@ class TestInventoryDimension(ERPNextTestSuite):
|
||||
{"has_serial_no": 1, "is_stock_item": 1},
|
||||
)
|
||||
serial_no = "Test Serialized Empty Inventory Dimension Serial No"
|
||||
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
|
||||
warehouse = create_warehouse("Serialized Empty Inventory Dimension Warehouse")
|
||||
|
||||
create_inventory_dimension(
|
||||
@@ -599,6 +602,7 @@ class TestInventoryDimension(ERPNextTestSuite):
|
||||
{"has_serial_no": 1, "is_stock_item": 1},
|
||||
)
|
||||
serial_no = "Test Serialized Required Inventory Dimension Serial No"
|
||||
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
|
||||
warehouse = create_warehouse("Serialized Required Inventory Dimension Warehouse")
|
||||
|
||||
create_inventory_dimension(
|
||||
@@ -644,6 +648,7 @@ class TestInventoryDimension(ERPNextTestSuite):
|
||||
{"has_serial_no": 1, "is_stock_item": 1},
|
||||
)
|
||||
serial_no = "Test Serialized Legacy Inventory Dimension Serial No"
|
||||
serial_no = SerialBatchIdentity("Serial No").resolve(item.name, [serial_no], create=True)[0]
|
||||
warehouse = create_warehouse("Serialized Legacy Inventory Dimension Warehouse")
|
||||
|
||||
create_inventory_dimension(
|
||||
|
||||
@@ -21,6 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
|
||||
get_serial_nos_from_bundle,
|
||||
)
|
||||
from erpnext.stock.serial_batch_bundle import SerialNoValuation
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -433,15 +434,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
item_code = "_Test Serialized Item"
|
||||
warehouse = "Stores - TCP1"
|
||||
|
||||
if not frappe.db.exists("Serial No", serial_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": item_code,
|
||||
"serial_no": serial_no,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
serial_no = SerialBatchIdentity("Serial No").resolve(item_code, [serial_no], create=True)[0]
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory",
|
||||
@@ -751,27 +744,12 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
"SN-TLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
serial_nos = SerialBatchIdentity("Serial No").resolve(
|
||||
sn_item, serial_nos, create=True, defaults={"company": "_Test Company"}
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Batch", "BATCH-TLCVSNO-0001"):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": batch_item,
|
||||
"batch_id": "BATCH-TLCVSNO-0001",
|
||||
}
|
||||
)
|
||||
batch_doc.insert()
|
||||
batch_no = SerialBatchIdentity("Batch").resolve(batch_item, ["BATCH-TLCVSNO-0001"], create=True)[0]
|
||||
batch_doc = frappe.get_doc("Batch", batch_no)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
@@ -813,7 +791,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
row.db_set("batch_no", "BATCH-TLCVSNO-0001")
|
||||
row.db_set("batch_no", batch_no)
|
||||
|
||||
for sn in serial_nos:
|
||||
sn_doc = frappe.get_doc("Serial No", sn)
|
||||
@@ -902,27 +880,12 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
"SN-TDVLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
serial_nos = SerialBatchIdentity("Serial No").resolve(
|
||||
sn_item, serial_nos, create=True, defaults={"company": "_Test Company"}
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Batch", "BATCH-TDVLCVSNO-0001"):
|
||||
batch_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"item": batch_item,
|
||||
"batch_id": "BATCH-TDVLCVSNO-0001",
|
||||
}
|
||||
)
|
||||
batch_doc.insert()
|
||||
batch_no = SerialBatchIdentity("Batch").resolve(batch_item, ["BATCH-TDVLCVSNO-0001"], create=True)[0]
|
||||
batch_doc = frappe.get_doc("Batch", batch_no)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
@@ -974,7 +937,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
if row.item_code == sn_item:
|
||||
row.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
row.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
row.db_set("batch_no", batch_no)
|
||||
|
||||
stock_ledger_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": pr.name})
|
||||
for sle in stock_ledger_entries:
|
||||
@@ -982,7 +945,7 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
doc.db_set("batch_no", batch_no)
|
||||
|
||||
dn = create_delivery_note(
|
||||
company=company,
|
||||
@@ -1017,14 +980,14 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
if doc.item_code == sn_item:
|
||||
doc.db_set("serial_no", ", ".join(serial_nos))
|
||||
else:
|
||||
doc.db_set("batch_no", "BATCH-TDVLCVSNO-0001")
|
||||
doc.db_set("batch_no", batch_no)
|
||||
|
||||
available_batches = get_auto_batch_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": batch_item,
|
||||
"warehouse": warehouse,
|
||||
"batch_no": ["BATCH-TDVLCVSNO-0001"],
|
||||
"batch_no": [batch_no],
|
||||
"consider_negative_batches": True,
|
||||
}
|
||||
)
|
||||
@@ -1092,17 +1055,9 @@ class TestLandedCostVoucher(ERPNextTestSuite):
|
||||
"SN-ALCVTDVLCVSNO-0005",
|
||||
]
|
||||
|
||||
for sn in serial_nos:
|
||||
if not frappe.db.exists("Serial No", sn):
|
||||
sn_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Serial No",
|
||||
"item_code": sn_item,
|
||||
"serial_no": sn,
|
||||
"company": "_Test Company",
|
||||
}
|
||||
)
|
||||
sn_doc.insert()
|
||||
serial_nos = SerialBatchIdentity("Serial No").resolve(
|
||||
sn_item, serial_nos, create=True, defaults={"company": "_Test Company"}
|
||||
)
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDate
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
from erpnext.stock.tests.test_utils import StockTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
@@ -59,11 +60,9 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
|
||||
item = "_Test Serialized Item"
|
||||
serial = "_Test SN Tie 9"
|
||||
company_a, company_b = "_Test Company", "_Test Company 1"
|
||||
if frappe.db.exists("Serial No", serial):
|
||||
frappe.delete_doc("Serial No", serial, force=1)
|
||||
frappe.get_doc(
|
||||
{"doctype": "Serial No", "serial_no": serial, "item_code": item, "company": company_b}
|
||||
).insert(ignore_permissions=True)
|
||||
serial = SerialBatchIdentity("Serial No").resolve(
|
||||
item, [serial], create=True, defaults={"company": company_b}
|
||||
)[0]
|
||||
|
||||
def mk_sle(name, rate):
|
||||
if frappe.db.exists("Stock Ledger Entry", name):
|
||||
@@ -1691,7 +1690,8 @@ def setup_item_valuation_test(
|
||||
batches = [f"IV - Test Batch {i} {valuation_method} {suffix}" for i in batches_list]
|
||||
|
||||
for i, batch_id in enumerate(batches):
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
batches[i] = frappe.db.get_value("Batch", {"item": item.item_code, "batch_id": batch_id})
|
||||
if not batches[i]:
|
||||
ubw = use_batchwise_valuation
|
||||
if isinstance(use_batchwise_valuation, list | tuple):
|
||||
ubw = use_batchwise_valuation[i]
|
||||
@@ -1702,6 +1702,7 @@ def setup_item_valuation_test(
|
||||
).insert()
|
||||
batch.use_batchwise_valuation = ubw
|
||||
batch.db_update()
|
||||
batches[i] = batch.name
|
||||
|
||||
return item.item_code, warehouses, batches
|
||||
|
||||
|
||||
@@ -549,7 +549,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
def test_valid_batch(self):
|
||||
create_batch_item_with_batch("Testing Batch Item 1", "001")
|
||||
create_batch_item_with_batch("Testing Batch Item 2", "002")
|
||||
batch_no = create_batch_item_with_batch("Testing Batch Item 2", "002")
|
||||
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
@@ -559,7 +559,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
|
||||
"voucher_type": "Stock Reconciliation",
|
||||
"entries": [
|
||||
{
|
||||
"batch_no": "002",
|
||||
"batch_no": batch_no,
|
||||
"qty": 1,
|
||||
"incoming_rate": 100,
|
||||
}
|
||||
@@ -567,7 +567,7 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
self.assertRaisesRegex(frappe.ValidationError, "does not belong to Item", doc.save)
|
||||
|
||||
def test_serial_no_cancellation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
@@ -2219,17 +2219,15 @@ class TestStockReconciliation(ERPNextTestSuite, StockTestMixin):
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
if not batch_item_doc.has_batch_no:
|
||||
batch_item_doc.has_batch_no = 1
|
||||
batch_item_doc.create_new_batch = 1
|
||||
batch_item_doc.save(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
b = frappe.new_doc("Batch")
|
||||
b.item = item_name
|
||||
b.batch_id = batch_id
|
||||
b.save()
|
||||
return SerialBatchIdentity("Batch").resolve(item_name, [batch_id], create=True)[0]
|
||||
|
||||
|
||||
def insert_existing_sle(warehouse, item_code="_Test Item"):
|
||||
|
||||
@@ -487,7 +487,7 @@ class FIFOSlots:
|
||||
or []
|
||||
)
|
||||
|
||||
return self.uppercase_serial_nos(serial_nos), batch_nos
|
||||
return serial_nos, batch_nos
|
||||
|
||||
def _get_row_batch_nos(self, row: dict) -> list:
|
||||
if not row.batch_no:
|
||||
@@ -495,7 +495,7 @@ class FIFOSlots:
|
||||
|
||||
return [
|
||||
[
|
||||
row.batch_no.upper(),
|
||||
row.batch_no,
|
||||
self._get_batchwise_valuation(row.batch_no),
|
||||
abs(row.actual_qty),
|
||||
abs(row.stock_value_difference),
|
||||
@@ -512,10 +512,6 @@ class FIFOSlots:
|
||||
elif len(fifo_queue) > qty_after:
|
||||
fifo_queue[:] = fifo_queue[:qty_after]
|
||||
|
||||
def uppercase_serial_nos(self, serial_nos):
|
||||
"Convert serial nos to uppercase for uniformity."
|
||||
return [sn.upper() for sn in serial_nos]
|
||||
|
||||
def _get_batchwise_valuation(self, batch_no: str):
|
||||
if batch_no not in self.batchwise_valuation_by_batch:
|
||||
# only reachable when stock ledger entries are passed in directly;
|
||||
@@ -1152,7 +1148,7 @@ class FIFOSlots:
|
||||
bundle_wise_batch_nos = frappe._dict({})
|
||||
for bundle_name, batch_no, use_batchwise_valuation, qty, stock_value_difference in query.run():
|
||||
bundle_wise_batch_nos.setdefault(bundle_name, []).append(
|
||||
[batch_no.upper(), use_batchwise_valuation, qty, stock_value_difference]
|
||||
[batch_no, use_batchwise_valuation, qty, stock_value_difference]
|
||||
)
|
||||
|
||||
return bundle_wise_batch_nos
|
||||
|
||||
@@ -12,6 +12,7 @@ from erpnext.stock.report.stock_ageing.stock_ageing import (
|
||||
format_report_data,
|
||||
get_average_age,
|
||||
)
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -19,6 +20,27 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
def setUp(self) -> None:
|
||||
self.filters = frappe._dict(company="_Test Company", to_date="2021-12-10", ranges=["30", "60", "90"])
|
||||
|
||||
def test_serial_ids_keep_their_case_in_fifo_slots(self):
|
||||
rows = [
|
||||
frappe._dict(
|
||||
name="Serialized Item",
|
||||
actual_qty=qty,
|
||||
qty_after_transaction=balance,
|
||||
stock_value_difference=qty * 10,
|
||||
warehouse="WH 1",
|
||||
posting_date=date,
|
||||
voucher_type="Stock Entry",
|
||||
voucher_no=str(index),
|
||||
has_serial_no=True,
|
||||
serial_no=serials,
|
||||
)
|
||||
for index, (qty, balance, date, serials) in enumerate(
|
||||
[(2, 2, "2021-12-01", "id-aB\nid-Cd"), (-1, 1, "2021-12-02", "id-aB")]
|
||||
)
|
||||
]
|
||||
slots = FIFOSlots(self.filters, rows).generate()
|
||||
self.assertEqual(slots["Serialized Item"]["fifo_queue"], [["id-Cd", "2021-12-01", 10.0]])
|
||||
|
||||
def test_normal_inward_outward_queue(self):
|
||||
"Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)"
|
||||
sle = [
|
||||
@@ -530,12 +552,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
|
||||
).name
|
||||
|
||||
batch_no = "SA-RECO-REVALUE-BATCH"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-RECO-REVALUE-BATCH")
|
||||
|
||||
def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference):
|
||||
return frappe._dict(
|
||||
@@ -583,12 +600,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
|
||||
).name
|
||||
|
||||
batch_no = "SA-PARTIAL-RECO-BATCH"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-PARTIAL-RECO-BATCH")
|
||||
|
||||
def make_sle(posting_date, voucher_type, voucher_no, actual_qty, qty_after, stock_value_difference):
|
||||
return frappe._dict(
|
||||
@@ -636,12 +648,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
|
||||
).name
|
||||
|
||||
batch_no = "SA-POOL-SPLIT-BATCH"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-POOL-SPLIT-BATCH")
|
||||
|
||||
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
|
||||
return frappe._dict(
|
||||
@@ -687,12 +694,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
|
||||
).name
|
||||
|
||||
batch_no = "SA-POOL-RESIDUAL-BATCH"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-POOL-RESIDUAL-BATCH")
|
||||
|
||||
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
|
||||
return frappe._dict(
|
||||
@@ -734,12 +736,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
{"is_stock_item": 1, "has_batch_no": 1, "valuation_method": "FIFO"},
|
||||
).name
|
||||
|
||||
batch_no = "SA-POOL-REBALANCE-BATCH"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc({"doctype": "Batch", "batch_id": batch_no, "item": item_code}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-POOL-REBALANCE-BATCH")
|
||||
|
||||
def make_sle(posting_date, voucher_no, actual_qty, qty_after, stock_value_difference):
|
||||
return frappe._dict(
|
||||
@@ -1533,38 +1530,14 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
},
|
||||
).name
|
||||
|
||||
def make_batch(batch_id, use_batchwise_valuation):
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", use_batchwise_valuation)
|
||||
|
||||
batchwise_above_90 = "SA-BATCHWISE-ABOVE-90"
|
||||
non_batchwise_above_90 = "SA-NON-BATCHWISE-ABOVE-90"
|
||||
batchwise_61_90 = "SA-BATCHWISE-61-90"
|
||||
non_batchwise_61_90 = "SA-NON-BATCHWISE-61-90"
|
||||
batchwise_31_60 = "SA-BATCHWISE-31-60"
|
||||
non_batchwise_31_60 = "SA-NON-BATCHWISE-31-60"
|
||||
batchwise_0_30 = "SA-BATCHWISE-0-30"
|
||||
non_batchwise_0_30 = "SA-NON-BATCHWISE-0-30"
|
||||
|
||||
for batch_id, use_batchwise_valuation in {
|
||||
batchwise_above_90: 1,
|
||||
non_batchwise_above_90: 0,
|
||||
batchwise_61_90: 1,
|
||||
non_batchwise_61_90: 0,
|
||||
batchwise_31_60: 1,
|
||||
non_batchwise_31_60: 0,
|
||||
batchwise_0_30: 1,
|
||||
non_batchwise_0_30: 0,
|
||||
}.items():
|
||||
make_batch(batch_id, use_batchwise_valuation)
|
||||
batchwise_above_90 = make_batch(item_code, "SA-BATCHWISE-ABOVE-90", 1)
|
||||
non_batchwise_above_90 = make_batch(item_code, "SA-NON-BATCHWISE-ABOVE-90", 0)
|
||||
batchwise_61_90 = make_batch(item_code, "SA-BATCHWISE-61-90", 1)
|
||||
non_batchwise_61_90 = make_batch(item_code, "SA-NON-BATCHWISE-61-90", 0)
|
||||
batchwise_31_60 = make_batch(item_code, "SA-BATCHWISE-31-60", 1)
|
||||
non_batchwise_31_60 = make_batch(item_code, "SA-NON-BATCHWISE-31-60", 0)
|
||||
batchwise_0_30 = make_batch(item_code, "SA-BATCHWISE-0-30", 1)
|
||||
non_batchwise_0_30 = make_batch(item_code, "SA-NON-BATCHWISE-0-30", 0)
|
||||
|
||||
qty_after_transaction = 0
|
||||
|
||||
@@ -1641,22 +1614,8 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
},
|
||||
).name
|
||||
|
||||
def make_batch(batch_id):
|
||||
if not frappe.db.exists("Batch", batch_id):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_id,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_id, "use_batchwise_valuation", 1)
|
||||
|
||||
source_batch = "SA-BATCHWISE-TRANSFER-SOURCE"
|
||||
target_batch = "SA-BATCHWISE-TRANSFER-TARGET"
|
||||
make_batch(source_batch)
|
||||
make_batch(target_batch)
|
||||
source_batch = make_batch(item_code, "SA-BATCHWISE-TRANSFER-SOURCE")
|
||||
target_batch = make_batch(item_code, "SA-BATCHWISE-TRANSFER-TARGET")
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
@@ -1735,17 +1694,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
},
|
||||
).name
|
||||
|
||||
batch_no = "SA-BATCHWISE-NEGATIVE-STOCK"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-STOCK")
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
@@ -1814,19 +1763,8 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
},
|
||||
).name
|
||||
|
||||
buffer_batch = "SA-BATCHWISE-NEGATIVE-BUFFER"
|
||||
negative_batch = "SA-BATCHWISE-NEGATIVE-NON-HEAD"
|
||||
for batch_no in [buffer_batch, negative_batch]:
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
buffer_batch = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-BUFFER")
|
||||
negative_batch = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-NON-HEAD")
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
@@ -1905,17 +1843,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
},
|
||||
).name
|
||||
|
||||
batch_no = "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": item_code,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", 1)
|
||||
batch_no = make_batch(item_code, "SA-BATCHWISE-NEGATIVE-LATER-VOUCHER")
|
||||
|
||||
sle = [
|
||||
frappe._dict(
|
||||
@@ -2056,7 +1984,7 @@ class TestStockAgeing(ERPNextTestSuite):
|
||||
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
|
||||
self.assertEqual(item_result["total_qty"], 5.0)
|
||||
self.assertEqual(
|
||||
item_result["fifo_queue"], [[batch_no.upper(), 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
|
||||
item_result["fifo_queue"], [[batch_no, 1, 5.0, getdate(add_days(base_date, -2)), 50.0]]
|
||||
)
|
||||
|
||||
def test_legacy_batch_no_sle_with_streaming_cursor(self):
|
||||
@@ -2143,3 +2071,9 @@ def generate_item_and_item_wh_wise_slots(filters, sle):
|
||||
filters.show_warehouse_wise_stock = False
|
||||
|
||||
return item_wise_slots, item_wh_wise_slots
|
||||
|
||||
|
||||
def make_batch(item_code, batch_id, use_batchwise_valuation=1):
|
||||
batch_no = SerialBatchIdentity("Batch").resolve(item_code, [batch_id], create=True)[0]
|
||||
frappe.db.set_value("Batch", batch_no, "use_batchwise_valuation", use_batchwise_valuation)
|
||||
return batch_no
|
||||
|
||||
@@ -77,7 +77,7 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
).insert()
|
||||
|
||||
# create batch
|
||||
frappe.get_doc(
|
||||
batch = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": "BATCH01",
|
||||
@@ -92,7 +92,7 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
"price_list": "Standard Selling",
|
||||
"item_code": item.item_code,
|
||||
"price_list_rate": 50,
|
||||
"batch_no": "BATCH01",
|
||||
"batch_no": batch.name,
|
||||
}
|
||||
).insert()
|
||||
|
||||
@@ -104,7 +104,7 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
qty=100,
|
||||
rate=100,
|
||||
batch_no="BATCH01",
|
||||
batch_no=batch.name,
|
||||
)
|
||||
|
||||
# creating sales order just to create delivery note from it
|
||||
@@ -122,7 +122,7 @@ class TestGetItemDetail(ERPNextTestSuite):
|
||||
|
||||
# Test 2 : On saving the DN, item's batch will be fetched and rate will be updated from Item Price
|
||||
dn.save()
|
||||
self.assertEqual(dn.items[0].batch_no, "BATCH01")
|
||||
self.assertEqual(dn.items[0].batch_no, batch.name)
|
||||
self.assertEqual(dn.items[0].rate, 50)
|
||||
|
||||
def test_maintain_same_rate_keeps_source_rate_on_refetch(self):
|
||||
|
||||
@@ -37,6 +37,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.stock.serial_batch_identity import SerialBatchIdentity
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
@@ -1753,14 +1754,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
batch_no = "BATCH-BNGS-0001"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": fg_item,
|
||||
}
|
||||
).insert()
|
||||
batch_no = SerialBatchIdentity("Batch").resolve(fg_item, [batch_no], create=True)[0]
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
self.assertFalse(scr.items[0].serial_and_batch_bundle)
|
||||
@@ -1830,14 +1824,7 @@ class TestSubcontractingReceipt(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
batch_no = "BATCH-REJ-BNGS-0001"
|
||||
if not frappe.db.exists("Batch", batch_no):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Batch",
|
||||
"batch_id": batch_no,
|
||||
"item": fg_item,
|
||||
}
|
||||
).insert()
|
||||
batch_no = SerialBatchIdentity("Batch").resolve(fg_item, [batch_no], create=True)[0]
|
||||
|
||||
rej_warehouse = create_warehouse("_Test Subcontract Warehouse For Rejected Qty")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user