mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-22 18:59:58 +00:00
fix(manufacturing): attribute available materials by original item
add_materials_from_transfer() merged transfer rows of the same item and
warehouse into one bucket even when their original_item differed, keeping
only the first row's attribution. Return entries and Manufacture backflush
rows built from a merged bucket then charged the whole quantity to a single
requirement, so per-row returned_qty and net transfer coverage landed on
the wrong required item.
Buckets are now keyed by (item_code, warehouse, original_item). Manufacture
consumption rows carry no original_item, so consumption is distributed
across the item's buckets in transfer order; any residual lands on the last
bucket, preserving the previous net quantity. Serial and batch deductions
resolve the bucket that actually holds the serial no or batch.
Restore the pre-split-refactor guard that skipped zero-qty rows when
building entries from buckets (dropped in e4b5e6bd1e); without it a fully
consumed bucket would emit a zero-qty row and block submission.
This commit is contained in:
@@ -668,6 +668,8 @@ class ManufactureStockEntry(BaseManufactureStockEntry):
|
||||
qty = row.qty if is_return else (flt(row.qty) * flt(self.doc.fg_completed_qty)) / pending_qty_to_mfg
|
||||
item_args["qty"] = ceil_qty_if_uom_has_whole_number(qty, row.uom)
|
||||
item_args["transfer_qty"] = item_args["qty"]
|
||||
if not flt(item_args["qty"], frappe.get_precision("Stock Entry Detail", "qty")):
|
||||
return
|
||||
if is_return:
|
||||
if row.get("original_item"):
|
||||
item_args["original_item"] = row.original_item
|
||||
@@ -760,7 +762,7 @@ class ManufactureStockEntry(BaseManufactureStockEntry):
|
||||
def add_materials_from_transfer(self):
|
||||
for row in self._transfer_entries:
|
||||
row.warehouse = row.t_warehouse
|
||||
key = (row.item_code, row.warehouse)
|
||||
key = (row.item_code, row.warehouse, row.original_item or None)
|
||||
if key not in self.available_materials:
|
||||
self.available_materials[key] = frappe._dict(row)
|
||||
else:
|
||||
@@ -790,20 +792,49 @@ class ManufactureStockEntry(BaseManufactureStockEntry):
|
||||
def remove_consumed_materials_from_available(self):
|
||||
for row in self._consumption_entries:
|
||||
row.warehouse = row.s_warehouse
|
||||
key = (row.item_code, row.warehouse)
|
||||
self.available_materials[key].qty -= row.qty
|
||||
buckets = self._get_available_buckets(row.item_code, row.warehouse)
|
||||
self._deduct_consumed_qty(buckets, flt(row.qty))
|
||||
if row.serial_and_batch_bundle:
|
||||
self._deduct_consumed_serial_batch(key, row.serial_and_batch_bundle)
|
||||
self._deduct_consumed_serial_batch(buckets, row.serial_and_batch_bundle)
|
||||
|
||||
def _deduct_consumed_serial_batch(self, key, sabb_name):
|
||||
def _get_available_buckets(self, item_code, warehouse):
|
||||
return [
|
||||
bucket
|
||||
for key, bucket in self.available_materials.items()
|
||||
if key[0] == item_code and key[1] == warehouse
|
||||
]
|
||||
|
||||
def _deduct_consumed_qty(self, buckets, consumed_qty):
|
||||
for bucket in buckets[:-1]:
|
||||
deducted = min(max(flt(bucket.qty), 0.0), consumed_qty)
|
||||
bucket.qty -= deducted
|
||||
consumed_qty -= deducted
|
||||
buckets[-1].qty -= consumed_qty
|
||||
|
||||
def _deduct_consumed_serial_batch(self, buckets, sabb_name):
|
||||
_details = self.get_sabb_details(sabb_name)
|
||||
if _details.serial_nos:
|
||||
for sn in _details.serial_nos:
|
||||
self.available_materials[key].serial_nos.remove(sn)
|
||||
for serial_no in _details.serial_nos:
|
||||
self._get_serial_no_bucket(buckets, serial_no).serial_nos.remove(serial_no)
|
||||
elif _details.batches:
|
||||
for batch_no, qty in _details.batches.items():
|
||||
# qty is negative, so add instead of subtract
|
||||
self.available_materials[key].batches[batch_no] += qty
|
||||
self._deduct_consumed_batch_qty(buckets, batch_no, -qty)
|
||||
|
||||
def _get_serial_no_bucket(self, buckets, serial_no):
|
||||
for bucket in buckets:
|
||||
if bucket.serial_nos and serial_no in bucket.serial_nos:
|
||||
return bucket
|
||||
return buckets[-1]
|
||||
|
||||
def _deduct_consumed_batch_qty(self, buckets, batch_no, consumed_qty):
|
||||
holders = [bucket for bucket in buckets if bucket.batches and batch_no in bucket.batches]
|
||||
if not holders:
|
||||
holders = buckets[-1:]
|
||||
for bucket in holders[:-1]:
|
||||
deducted = min(max(flt(bucket.batches[batch_no]), 0.0), consumed_qty)
|
||||
bucket.batches[batch_no] -= deducted
|
||||
consumed_qty -= deducted
|
||||
holders[-1].batches[batch_no] -= consumed_qty
|
||||
|
||||
def add_additional_cost(self):
|
||||
if not self.wo_doc:
|
||||
|
||||
Reference in New Issue
Block a user