diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 6da36cf6014..f1e3f59ee97 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -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", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3f17822ebe5..1d9544d2651 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -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)), ) ) diff --git a/erpnext/manufacturing/page/shop_floor/shop_floor.py b/erpnext/manufacturing/page/shop_floor/shop_floor.py index dd4bce405c0..0ef5b99613e 100644 --- a/erpnext/manufacturing/page/shop_floor/shop_floor.py +++ b/erpnext/manufacturing/page/shop_floor/shop_floor.py @@ -22,6 +22,7 @@ JOB_CARD_FIELDS = [ "total_completed_qty", "for_quantity", "process_loss_qty", + "stock_uom", "finished_good", "transferred_qty", "status", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 921296c3297..f2c2a447817 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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 diff --git a/erpnext/patches/v16_0/set_stock_uom_in_job_card.py b/erpnext/patches/v16_0/set_stock_uom_in_job_card.py new file mode 100644 index 00000000000..7289ec6552e --- /dev/null +++ b/erpnext/patches/v16_0/set_stock_uom_in_job_card.py @@ -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