mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-25 09:38:31 +00:00
fix: stock reservation created against job card
(cherry picked from commit dca2cfd009)
This commit is contained in:
committed by
Mergify
parent
86dd2e786c
commit
305483e074
@@ -297,7 +297,9 @@ class BOM(WebsiteGenerator):
|
|||||||
self.validate_scrap_items()
|
self.validate_scrap_items()
|
||||||
self.set_default_uom()
|
self.set_default_uom()
|
||||||
self.validate_semi_finished_goods()
|
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):
|
def validate_semi_finished_goods(self):
|
||||||
if not self.track_semi_finished_goods or not self.operations:
|
if not self.track_semi_finished_goods or not self.operations:
|
||||||
@@ -333,7 +335,7 @@ class BOM(WebsiteGenerator):
|
|||||||
if row.bom_no:
|
if row.bom_no:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
operation_idx_with_no_rm[row.idx] = row.operation
|
operation_idx_with_no_rm[row.idx] = row
|
||||||
|
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
if row.operation_row_id and row.operation_row_id in operation_idx_with_no_rm:
|
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.validate_work_order()
|
||||||
self.set_employees()
|
self.set_employees()
|
||||||
self.validate_semi_finished_goods()
|
|
||||||
|
if self.docstatus == 1:
|
||||||
|
self.validate_semi_finished_goods()
|
||||||
|
|
||||||
def validate_semi_finished_goods(self):
|
def validate_semi_finished_goods(self):
|
||||||
if not self.track_semi_finished_goods:
|
if not self.track_semi_finished_goods:
|
||||||
|
|||||||
@@ -1609,6 +1609,7 @@ class WorkOrder(Document):
|
|||||||
"item_code": row.item_code,
|
"item_code": row.item_code,
|
||||||
"voucher_detail_no": row.name,
|
"voucher_detail_no": row.name,
|
||||||
"warehouse": row.source_warehouse,
|
"warehouse": row.source_warehouse,
|
||||||
|
"status": ("not in", ["Closed", "Cancelled", "Completed"]),
|
||||||
},
|
},
|
||||||
pluck="name",
|
pluck="name",
|
||||||
):
|
):
|
||||||
@@ -1817,24 +1818,10 @@ class WorkOrder(Document):
|
|||||||
elif stock_entry.job_card:
|
elif stock_entry.job_card:
|
||||||
# Reserve the final product for the job card.
|
# Reserve the final product for the job card.
|
||||||
finished_good = frappe.db.get_value("Job Card", stock_entry.job_card, "finished_good")
|
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:
|
item_details = self.get_items_to_reserve_for_job_card(stock_entry, finished_good)
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
# Reserve the final product for the sales order.
|
# Reserve the final product for the sales order.
|
||||||
item_details = self.get_so_details()
|
item_details = self.get_so_details()
|
||||||
@@ -1888,6 +1875,53 @@ class WorkOrder(Document):
|
|||||||
|
|
||||||
return items
|
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):
|
def get_wo_details(self):
|
||||||
doctype = frappe.qb.DocType("Work Order")
|
doctype = frappe.qb.DocType("Work Order")
|
||||||
child_doctype = frappe.qb.DocType("Work Order Item")
|
child_doctype = frappe.qb.DocType("Work Order Item")
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
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
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||||
|
|
||||||
@@ -106,6 +111,10 @@ class ManufactureEntry:
|
|||||||
"Manufacturing Settings", "backflush_raw_materials_based_on"
|
"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():
|
for item_code, _dict in item_dict.items():
|
||||||
_dict.from_warehouse = self.source_wh.get(item_code) or self.wip_warehouse
|
_dict.from_warehouse = self.source_wh.get(item_code) or self.wip_warehouse
|
||||||
_dict.to_warehouse = ""
|
_dict.to_warehouse = ""
|
||||||
@@ -120,9 +129,131 @@ class ManufactureEntry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
_dict.qty = calculated_qty
|
_dict.qty = calculated_qty
|
||||||
|
self.update_available_serial_batches(_dict, available_serial_batches)
|
||||||
|
|
||||||
self.stock_entry.add_to_stock_entry_detail(item_dict)
|
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):
|
def get_items_from_job_card(self):
|
||||||
item_dict = {}
|
item_dict = {}
|
||||||
items = frappe.get_all(
|
items = frappe.get_all(
|
||||||
|
|||||||
Reference in New Issue
Block a user