fix: stock reservation created against job card

This commit is contained in:
Rohit Waghchaure
2026-02-05 21:06:47 +05:30
parent a632d5f313
commit dca2cfd009
4 changed files with 190 additions and 21 deletions

View File

@@ -297,7 +297,9 @@ class BOM(WebsiteGenerator):
self.validate_scrap_items()
self.set_default_uom()
self.validate_semi_finished_goods()
self.validate_raw_materials_of_operation()
if self.docstatus == 1:
self.validate_raw_materials_of_operation()
def validate_semi_finished_goods(self):
if not self.track_semi_finished_goods or not self.operations:
@@ -333,7 +335,7 @@ class BOM(WebsiteGenerator):
if row.bom_no:
continue
operation_idx_with_no_rm[row.idx] = row.operation
operation_idx_with_no_rm[row.idx] = row
for row in self.items:
if row.operation_row_id and row.operation_row_id in operation_idx_with_no_rm:

View File

@@ -166,7 +166,9 @@ class JobCard(Document):
self.validate_work_order()
self.set_employees()
self.validate_semi_finished_goods()
if self.docstatus == 1:
self.validate_semi_finished_goods()
def validate_semi_finished_goods(self):
if not self.track_semi_finished_goods:

View File

@@ -1609,6 +1609,7 @@ class WorkOrder(Document):
"item_code": row.item_code,
"voucher_detail_no": row.name,
"warehouse": row.source_warehouse,
"status": ("not in", ["Closed", "Cancelled", "Completed"]),
},
pluck="name",
):
@@ -1817,24 +1818,10 @@ class WorkOrder(Document):
elif stock_entry.job_card:
# Reserve the final product for the job card.
finished_good = frappe.db.get_value("Job Card", stock_entry.job_card, "finished_good")
if finished_good == self.production_item:
return
for row in stock_entry.items:
if row.item_code == finished_good:
item_details = [
frappe._dict(
{
"item_code": row.item_code,
"stock_qty": row.qty,
"stock_reserved_qty": 0,
"warehouse": row.t_warehouse,
"voucher_no": stock_entry.work_order,
"voucher_type": "Work Order",
"name": row.name,
"delivered_qty": 0,
}
)
]
break
item_details = self.get_items_to_reserve_for_job_card(stock_entry, finished_good)
else:
# Reserve the final product for the sales order.
item_details = self.get_so_details()
@@ -1888,6 +1875,53 @@ class WorkOrder(Document):
return items
def get_items_to_reserve_for_job_card(self, stock_entry, finished_good):
item_details = []
for row in stock_entry.items:
if row.item_code == finished_good:
name = frappe.db.get_value(
"Work Order Item",
{"item_code": finished_good, "parent": self.name},
"name",
)
sres = frappe.get_all(
"Stock Reservation Entry",
fields=["reserved_qty"],
filters={
"voucher_no": self.name,
"item_code": finished_good,
"voucher_detail_no": name,
"warehouse": row.t_warehouse,
"docstatus": 1,
"status": "Reserved",
},
)
pending_qty = row.qty
for d in sres:
pending_qty -= d.reserved_qty
if pending_qty > 0:
item_details = [
frappe._dict(
{
"item_code": row.item_code,
"stock_qty": pending_qty,
"stock_reserved_qty": 0,
"warehouse": row.t_warehouse,
"voucher_no": stock_entry.work_order,
"voucher_type": "Work Order",
"name": name,
"delivered_qty": 0,
}
)
]
break
return item_details
def get_wo_details(self):
doctype = frappe.qb.DocType("Work Order")
child_doctype = frappe.qb.DocType("Work Order Item")

View File

@@ -2,10 +2,15 @@
# For license information, please see license.txt
from collections import defaultdict
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt
from frappe.utils import cint, flt
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
from erpnext.stock.utils import get_combine_datetime
class StockEntryType(Document):
@@ -104,6 +109,10 @@ class ManufactureEntry:
"Manufacturing Settings", "backflush_raw_materials_based_on"
)
available_serial_batches = frappe._dict({})
if backflush_based_on != "BOM":
available_serial_batches = self.get_transferred_serial_batches()
for item_code, _dict in item_dict.items():
_dict.from_warehouse = self.source_wh.get(item_code) or self.wip_warehouse
_dict.to_warehouse = ""
@@ -118,9 +127,131 @@ class ManufactureEntry:
)
_dict.qty = calculated_qty
self.update_available_serial_batches(_dict, available_serial_batches)
self.stock_entry.add_to_stock_entry_detail(item_dict)
def parse_available_serial_batches(self, item_dict, available_serial_batches):
key = (item_dict.item_code, item_dict.from_warehouse)
if key not in available_serial_batches:
return [], {}
_avl_dict = available_serial_batches[key]
qty = item_dict.qty
serial_nos = []
batches = frappe._dict()
if _avl_dict.serial_nos:
serial_nos = _avl_dict.serial_nos[: cint(qty)]
qty -= len(serial_nos)
for sn in serial_nos:
_avl_dict.serial_nos.remove(sn)
elif _avl_dict.batches:
batches = frappe._dict()
for batch_no, batch_qty in _avl_dict.batches.items():
if qty <= 0:
break
if batch_qty <= qty:
batches[batch_no] = batch_qty
qty -= batch_qty
else:
batches[batch_no] = qty
qty = 0
for _used_batch_no in batches:
_avl_dict.batches[_used_batch_no] -= batches[_used_batch_no]
if _avl_dict.batches[_used_batch_no] <= 0:
del _avl_dict.batches[_used_batch_no]
return serial_nos, batches
def update_available_serial_batches(self, item_dict, available_serial_batches):
serial_nos, batches = self.parse_available_serial_batches(item_dict, available_serial_batches)
if serial_nos or batches:
sabb = SerialBatchCreation(
{
"item_code": item_dict.item_code,
"warehouse": item_dict.from_warehouse,
"posting_datetime": get_combine_datetime(
self.stock_entry.posting_date, self.stock_entry.posting_time
),
"voucher_type": self.stock_entry.doctype,
"company": self.stock_entry.company,
"type_of_transaction": "Outward",
"qty": item_dict.qty,
"serial_nos": serial_nos,
"batches": batches,
"do_not_submit": True,
}
).make_serial_and_batch_bundle()
item_dict.serial_and_batch_bundle = sabb.name
def get_stock_entry_data(self):
stock_entry = frappe.qb.DocType("Stock Entry")
stock_entry_detail = frappe.qb.DocType("Stock Entry Detail")
return (
frappe.qb.from_(stock_entry)
.inner_join(stock_entry_detail)
.on(stock_entry.name == stock_entry_detail.parent)
.select(
stock_entry_detail.item_code,
stock_entry_detail.qty,
stock_entry_detail.serial_and_batch_bundle,
stock_entry_detail.s_warehouse,
stock_entry_detail.t_warehouse,
stock_entry.purpose,
)
.where(
(stock_entry.job_card == self.job_card)
& (stock_entry_detail.serial_and_batch_bundle.isnotnull())
& (stock_entry.docstatus == 1)
& (stock_entry.purpose.isin(["Material Transfer for Manufacture", "Manufacture"]))
)
.orderby(stock_entry.posting_date, stock_entry.posting_time)
).run(as_dict=True)
def get_transferred_serial_batches(self):
available_serial_batches = frappe._dict({})
stock_entry_data = self.get_stock_entry_data()
for row in stock_entry_data:
warehouse = (
row.t_warehouse if row.purpose == "Material Transfer for Manufacture" else row.s_warehouse
)
key = (row.item_code, warehouse)
if key not in available_serial_batches:
available_serial_batches[key] = frappe._dict(
{
"batches": defaultdict(float),
"serial_nos": [],
}
)
_avl_dict = available_serial_batches[key]
sabb_data = frappe.get_all(
"Serial and Batch Entry",
filters={"parent": row.serial_and_batch_bundle},
fields=["serial_no", "batch_no", "qty"],
)
for entry in sabb_data:
if entry.serial_no:
if entry.qty > 0:
_avl_dict.serial_nos.append(entry.serial_no)
else:
_avl_dict.serial_nos.remove(entry.serial_no)
if entry.batch_no:
_avl_dict.batches[entry.batch_no] += flt(entry.qty) * (
-1 if row.purpose == "Material Transfer for Manufacture" else 1
)
return available_serial_batches
def get_items_from_job_card(self):
item_dict = {}
items = frappe.get_all(