mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-15 15:45:01 +00:00
fix: stock reservation created against job card
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user