mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 05:31:48 +00:00
feat(job_card): carry the stock uom on the job card
Every quantity the job card reports belongs to the item it produces, but the document had no unit of its own, so messages could only print bare numbers. Add the Stock UOM field, set from the finished good or the final product, and backfill the job cards that already exist.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"production_item",
|
||||
"column_break_qrpg",
|
||||
"for_quantity",
|
||||
"stock_uom",
|
||||
"column_break_yecz",
|
||||
"bom_no",
|
||||
"section_break_oisd",
|
||||
@@ -163,6 +164,13 @@
|
||||
"in_preview": 1,
|
||||
"label": "Qty To Manufacture"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
@@ -689,7 +697,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-23 12:00:00.000000",
|
||||
"modified": "2026-08-01 14:30:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -130,6 +130,7 @@ class JobCard(Document):
|
||||
"Cancelled",
|
||||
"Completed",
|
||||
]
|
||||
stock_uom: DF.Link | None
|
||||
sub_operations: DF.Table[JobCardOperation]
|
||||
target_warehouse: DF.Link | None
|
||||
time_logs: DF.Table[JobCardTimeLog]
|
||||
@@ -158,6 +159,7 @@ class JobCard(Document):
|
||||
|
||||
def before_validate(self):
|
||||
self.set_wip_warehouse()
|
||||
self.set_stock_uom()
|
||||
|
||||
def validate(self):
|
||||
self.validate_time_logs()
|
||||
@@ -955,10 +957,10 @@ class JobCard(Document):
|
||||
_(
|
||||
"Total Completed Qty ({0}), Process Loss Qty ({1}) and Pending Qty ({2}) must add up to the Qty to Manufacture ({3})."
|
||||
).format(
|
||||
bold(flt(self.total_completed_qty, precision)),
|
||||
bold(flt(self.process_loss_qty, precision)),
|
||||
bold(flt(self.pending_qty, precision)),
|
||||
bold(flt(self.for_quantity, precision)),
|
||||
bold(self.get_qty_with_uom(self.total_completed_qty)),
|
||||
bold(self.get_qty_with_uom(self.process_loss_qty)),
|
||||
bold(self.get_qty_with_uom(self.pending_qty)),
|
||||
bold(self.get_qty_with_uom(self.for_quantity)),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1241,7 +1243,12 @@ class JobCard(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}"
|
||||
).format(row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card),
|
||||
).format(
|
||||
row.idx,
|
||||
frappe.bold(self.get_qty_with_uom(required_qty, row.item_code)),
|
||||
frappe.bold(row.item_code),
|
||||
ste_doc.job_card,
|
||||
),
|
||||
title=_("Excess Transfer"),
|
||||
exc=JobCardOverTransferError,
|
||||
)
|
||||
@@ -1316,6 +1323,14 @@ class JobCard(Document):
|
||||
"""Qty this job card is expected to produce, the pending qty is left to another job card."""
|
||||
return flt(self.for_quantity) - flt(self.pending_qty)
|
||||
|
||||
def get_qty_with_uom(self, qty, item_code=None):
|
||||
"""A quantity in a message reads as a count of nothing without the unit it is measured in."""
|
||||
uom = self.stock_uom
|
||||
if item_code:
|
||||
uom = frappe.get_cached_value("Item", item_code, "stock_uom")
|
||||
|
||||
return f"{flt(qty, self.precision('total_completed_qty'))} {uom or ''}".strip()
|
||||
|
||||
def set_finished_good_status(self):
|
||||
# Only reached for a submitted job card (docstatus == 1) with a finished good, see set_status().
|
||||
qty_to_produce = self.get_qty_to_produce()
|
||||
@@ -1360,6 +1375,11 @@ class JobCard(Document):
|
||||
if not self.wip_warehouse:
|
||||
self.wip_warehouse = frappe.get_cached_value("Company", self.company, "default_wip_warehouse")
|
||||
|
||||
def set_stock_uom(self):
|
||||
item_code = self.finished_good or self.production_item
|
||||
if item_code:
|
||||
self.stock_uom = frappe.get_cached_value("Item", item_code, "stock_uom")
|
||||
|
||||
def set_operation_id(self):
|
||||
if not (self.work_order and self.operation):
|
||||
return
|
||||
@@ -1435,7 +1455,7 @@ class JobCard(Document):
|
||||
def get_previous_operations(self):
|
||||
previous_operations = frappe.get_all(
|
||||
"Work Order Operation",
|
||||
fields=["name", "operation", "status", "completed_qty", "sequence_id"],
|
||||
fields=["name", "operation", "status", "completed_qty", "sequence_id", "finished_good"],
|
||||
filters={"docstatus": 1, "parent": self.work_order, "sequence_id": ("<", self.sequence_id)},
|
||||
order_by="sequence_id, idx",
|
||||
)
|
||||
@@ -1494,9 +1514,9 @@ class JobCard(Document):
|
||||
_(
|
||||
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
|
||||
).format(
|
||||
bold(current_operation_qty),
|
||||
bold(self.get_qty_with_uom(current_operation_qty)),
|
||||
bold(self.operation),
|
||||
bold(row.completed_qty),
|
||||
bold(self.get_qty_with_uom(row.completed_qty, row.finished_good)),
|
||||
bold(row.operation),
|
||||
)
|
||||
)
|
||||
@@ -1522,9 +1542,9 @@ class JobCard(Document):
|
||||
_(
|
||||
"The completed quantity {0} of an operation {1} cannot be greater than the manufactured quantity {2} of a previous operation {3}. Submit the manufacturing entry for the operation {3} first."
|
||||
).format(
|
||||
bold(current_operation_qty),
|
||||
bold(self.get_qty_with_uom(current_operation_qty)),
|
||||
bold(self.operation),
|
||||
bold(manufactured_qty),
|
||||
bold(self.get_qty_with_uom(manufactured_qty, row.finished_good)),
|
||||
bold(row.operation),
|
||||
),
|
||||
OperationSequenceError,
|
||||
@@ -1717,10 +1737,10 @@ class JobCard(Document):
|
||||
_(
|
||||
"Completed Quantity ({0}), Pending Quantity ({1}) and Process Loss Quantity ({2}) must add up to the Qty to Manufacture ({3})."
|
||||
).format(
|
||||
bold(flt(kwargs.qty, precision)),
|
||||
bold(flt(kwargs.pending_qty, precision)),
|
||||
bold(flt(kwargs.process_loss_qty, precision)),
|
||||
bold(flt(kwargs.for_quantity, precision)),
|
||||
bold(self.get_qty_with_uom(kwargs.qty)),
|
||||
bold(self.get_qty_with_uom(kwargs.pending_qty)),
|
||||
bold(self.get_qty_with_uom(kwargs.process_loss_qty)),
|
||||
bold(self.get_qty_with_uom(kwargs.for_quantity)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ JOB_CARD_FIELDS = [
|
||||
"total_completed_qty",
|
||||
"for_quantity",
|
||||
"process_loss_qty",
|
||||
"stock_uom",
|
||||
"finished_good",
|
||||
"transferred_qty",
|
||||
"status",
|
||||
|
||||
@@ -507,3 +507,4 @@ erpnext.patches.v16_0.fix_subcontracting_titles
|
||||
erpnext.patches.v16_0.move_warehouse_defaults_to_company
|
||||
erpnext.patches.v16_0.backfill_repost_accounting_ledger_status
|
||||
erpnext.patches.v16_0.merge_seeded_item_group_root
|
||||
erpnext.patches.v16_0.set_stock_uom_in_job_card
|
||||
|
||||
39
erpnext/patches/v16_0/set_stock_uom_in_job_card.py
Normal file
39
erpnext/patches/v16_0/set_stock_uom_in_job_card.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"stock_uom": ("in", ["", None])},
|
||||
fields=["name", "finished_good", "production_item"],
|
||||
)
|
||||
|
||||
if not job_cards:
|
||||
return
|
||||
|
||||
item_codes = {row.finished_good or row.production_item for row in job_cards}
|
||||
item_codes.discard(None)
|
||||
if not item_codes:
|
||||
return
|
||||
|
||||
stock_uoms = dict(
|
||||
frappe.get_all(
|
||||
"Item",
|
||||
filters={"name": ("in", list(item_codes))},
|
||||
fields=["name", "stock_uom"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
updates = {}
|
||||
for row in job_cards:
|
||||
stock_uom = stock_uoms.get(row.finished_good or row.production_item)
|
||||
if stock_uom:
|
||||
updates[row.name] = {"stock_uom": stock_uom}
|
||||
|
||||
if not updates:
|
||||
return
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = True
|
||||
frappe.db.bulk_update("Job Card", updates)
|
||||
frappe.db.auto_commit_on_many_writes = False
|
||||
Reference in New Issue
Block a user