Skip to content

Commit

Permalink
feat: Track Semi-finished goods (including subcontracted items) again…
Browse files Browse the repository at this point in the history
…st Job Cards
  • Loading branch information
rohitwaghchaure committed May 31, 2024
1 parent 07c4212 commit 152a03a
Show file tree
Hide file tree
Showing 31 changed files with 1,322 additions and 303 deletions.
10 changes: 9 additions & 1 deletion erpnext/buying/doctype/purchase_order/purchase_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def validate_fg_item_for_subcontracting(self):
item.idx, item.fg_item
)
)
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
elif not item.bom and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(
item.idx, item.fg_item
Expand Down Expand Up @@ -919,6 +919,14 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
for idx, item in enumerate(target_doc.items):
item.warehouse = source_doc.items[idx].warehouse

for idx, item in enumerate(target_doc.items):
item.job_card = source_doc.items[idx].job_card
if not target_doc.supplier_warehouse:
# WIP warehouse is set as Supplier Warehouse in Job Card
target_doc.supplier_warehouse = frappe.get_cached_value(
"Job Card", item.job_card, "wip_warehouse"
)

return target_doc


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"page_break"
"page_break",
"column_break_pjyo",
"job_card"
],
"fields": [
{
Expand Down Expand Up @@ -909,13 +911,24 @@
{
"fieldname": "column_break_fyqr",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_pjyo",
"fieldtype": "Column Break"
},
{
"fieldname": "job_card",
"fieldtype": "Link",
"label": "Job Card",
"options": "Job Card",
"search_index": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:24.979325",
"modified": "2024-03-27 13:11:24.979325",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
Expand Down
54 changes: 54 additions & 0 deletions erpnext/manufacturing/doctype/bom/bom.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ frappe.ui.form.on("BOM", {
};
});

frm.set_query("bom_no", "operations", function(doc, cdt, cdn) {
let row = locals[cdt][cdn];
return {
query: "erpnext.controllers.queries.bom",
filters: {
'currency': frm.doc.currency,
'company': frm.doc.company,
'item': row.finished_good,
'is_active': 1,
'docstatus': 1,
'make_finished_good_against_job_card': 0
}
};
});

frm.set_query("source_warehouse", "items", function () {
return {
filters: {
Expand Down Expand Up @@ -85,6 +100,22 @@ frappe.ui.form.on("BOM", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},

default_source_warehouse(frm) {
if (frm.doc.default_source_warehouse) {
frm.doc.operations.forEach((d) => {
frappe.model.set_value(d.doctype, d.name, "source_warehouse", frm.doc.default_source_warehouse);
});
}
},

default_target_warehouse(frm) {
if (frm.doc.default_source_warehouse) {
frm.doc.operations.forEach((d) => {
frappe.model.set_value(d.doctype, d.name, "fg_warehouse", frm.doc.default_target_warehouse);
});
}
},

refresh(frm) {
frm.toggle_enable("item", frm.doc.__islocal);

Expand Down Expand Up @@ -432,6 +463,29 @@ frappe.ui.form.on("BOM", {
},
});


frappe.ui.form.on('BOM Operation', {
bom_no(frm, cdt, cdn) {
let row = locals[cdt][cdn];

if (row.bom_no && row.finished_good) {
frappe.call({
method: "add_materials_from_bom",
doc: frm.doc,
args: {
finished_good: row.finished_good,
bom_no: row.bom_no,
operation_row_id: row.idx,
qty: row.finished_good_qty,
},
callback(r) {
refresh_field("items");
}
})
}
}
})

erpnext.bom.BomController = class BomController extends erpnext.TransactionController {
conversion_rate(doc) {
if (this.frm.doc.currency === this.get_company_currency()) {
Expand Down
44 changes: 34 additions & 10 deletions erpnext/manufacturing/doctype/bom/bom.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,22 @@
"column_break_ivyw",
"currency",
"conversion_rate",
"materials_section",
"items",
"section_break_21",
"operations_section_section",
"with_operations",
"make_finished_good_against_job_card",
"column_break_23",
"transfer_material_against",
"routing",
"fg_based_operating_cost",
"column_break_joxb",
"default_source_warehouse",
"default_target_warehouse",
"fg_based_section_section",
"operating_cost_per_bom_quantity",
"operations_section",
"operations",
"materials_section",
"items",
"scrap_section",
"scrap_items_section",
"scrap_items",
Expand Down Expand Up @@ -211,7 +214,7 @@
},
{
"default": "Work Order",
"depends_on": "with_operations",
"depends_on": "eval: doc.with_operations === 1 && doc.make_finished_good_against_job_card === 0",
"fieldname": "transfer_material_against",
"fieldtype": "Select",
"label": "Transfer Material Against",
Expand Down Expand Up @@ -485,11 +488,6 @@
"fieldtype": "Check",
"label": "Show Operations"
},
{
"fieldname": "section_break_21",
"fieldtype": "Tab Break",
"label": "Operations"
},
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
Expand Down Expand Up @@ -534,6 +532,8 @@
"show_dashboard": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.with_operations",
"fieldname": "operations_section_section",
"fieldtype": "Section Break",
"label": "Operations"
Expand Down Expand Up @@ -630,14 +630,38 @@
{
"fieldname": "column_break_oxbz",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "with_operations",
"description": "User can consume the raw materials, Add Semi Finished Goods / Final Finished Good against the Operation using Job Cards",
"fieldname": "make_finished_good_against_job_card",
"fieldtype": "Check",
"label": "Make Finished Good Against Job Card"
},
{
"fieldname": "column_break_joxb",
"fieldtype": "Column Break"
},
{
"fieldname": "default_source_warehouse",
"fieldtype": "Link",
"label": "Default Source Warehouse",
"options": "Warehouse"
},
{
"fieldname": "default_target_warehouse",
"fieldtype": "Link",
"label": "Default Target Warehouse",
"options": "Warehouse"
}
],
"icon": "fa fa-sitemap",
"idx": 1,
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2024-04-02 16:22:47.518411",
"modified": "2024-04-02 16:23:47.518411",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
Expand Down
54 changes: 49 additions & 5 deletions erpnext/manufacturing/doctype/bom/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def validate(self):
self.clear_inspection()
self.validate_main_item()
self.validate_currency()
self.set_materials_based_on_operation_bom()
self.set_conversion_rate()
self.set_plc_conversion_rate()
self.validate_uom_is_interger()
Expand Down Expand Up @@ -544,6 +545,9 @@ def clear_operations(self):
if not self.with_operations:
self.set("operations", [])

if not self.with_operations and self.make_finished_good_against_job_card:
self.make_finished_good_against_job_card = 0

def clear_inspection(self):
if not self.inspection_required:
self.quality_inspection_template = None
Expand Down Expand Up @@ -645,6 +649,33 @@ def _throw_error(bom_name):
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)

def set_materials_based_on_operation_bom(self):
if not self.make_finished_good_against_job_card:
return

for row in self.get("operations"):
if row.bom_no and row.finished_good:
self.add_materials_from_bom(row.finished_good, row.bom_no, row.idx, qty=row.finished_good_qty)

@frappe.whitelist()
def add_materials_from_bom(self, finished_good, bom_no, operation_row_id, qty=None):
if not frappe.db.exists("BOM", {"item": finished_good, "name": bom_no, "docstatus": 1}):
frappe.throw(_("BOM {0} not found for the item {1}").format(bom_no, finished_good))

if not qty:
qty = 1

for row in self.items:
if row.operation_row_id == operation_row_id:
return

bom_items = get_bom_items(bom_no, self.company, qty=qty, fetch_exploded=0)
for row in bom_items:
row.uom = row.stock_uom
row.operation_row_id = operation_row_id
row.idx = None
self.append("items", row)

def traverse_tree(self, bom_list=None):
def _get_children(bom_no):
children = frappe.cache().hget("bom_children", bom_no)
Expand Down Expand Up @@ -1094,6 +1125,11 @@ def get_bom_items_as_dict(
):
item_dict = {}

group_by_cond = "group by item_code, stock_uom"
if frappe.get_cached_value("BOM", bom, "make_finished_good_against_job_card"):
fetch_exploded = 0
group_by_cond = "group by item_code, operation_row_id, stock_uom"

# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
query = """select
bom_item.item_code,
Expand Down Expand Up @@ -1122,7 +1158,7 @@ def get_bom_items_as_dict(
and bom.name = %(bom)s
and item.is_stock_item in (1, {is_stock_item})
{where_conditions}
group by item_code, stock_uom
{group_by_cond}
order by idx"""

is_stock_item = 0 if include_non_stock_items else 1
Expand All @@ -1132,6 +1168,7 @@ def get_bom_items_as_dict(
where_conditions="",
is_stock_item=is_stock_item,
qty_field="stock_qty",
group_by_cond=group_by_cond,
select_columns=""", bom_item.source_warehouse, bom_item.operation,
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""",
Expand All @@ -1147,6 +1184,7 @@ def get_bom_items_as_dict(
select_columns=", item.description",
is_stock_item=is_stock_item,
qty_field="stock_qty",
group_by_cond=group_by_cond,
)

items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)
Expand All @@ -1158,15 +1196,20 @@ def get_bom_items_as_dict(
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
select_columns=""", bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
bom_item.description, bom_item.base_rate as rate """,
bom_item.description, bom_item.base_rate as rate, bom_item.operation_row_id """,
group_by_cond=group_by_cond,
)
items = frappe.db.sql(query, {"qty": qty, "bom": bom, "company": company}, as_dict=True)

for item in items:
if item.item_code in item_dict:
item_dict[item.item_code]["qty"] += flt(item.qty)
key = item.item_code
if item.operation_row_id:
key = (item.item_code, item.operation_row_id)

if key in item_dict:
item_dict[key]["qty"] += flt(item.qty)
else:
item_dict[item.item_code] = item
item_dict[key] = item

for item, item_details in item_dict.items():
for d in [
Expand All @@ -1178,6 +1221,7 @@ def get_bom_items_as_dict(
if not item_details.get(d[1]) or (company_in_record and company != company_in_record):
item_dict[item][d[1]] = frappe.get_cached_value("Company", company, d[2]) if d[2] else None

print(item_dict)
return item_dict


Expand Down
28 changes: 28 additions & 0 deletions erpnext/manufacturing/doctype/bom_creator/bom_creator.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ frappe.ui.form.on("BOM Creator", {
reqd: 1,
default: 1.0,
},
{ fieldtype: "Section Break" },
{
label: __("Track Operations"),
fieldtype: "Check",
fieldname: "track_operations",
onchange: (r) => {
let track_operations = dialog.get_value("track_operations");
if (r.type === "input" && !track_operations) {
dialog.set_value("make_finished_good_against_job_card", 0);
}
}
},
{
label: __("Make Finished Good Against Job Card"),
fieldtype: "Check",
fieldname: "make_finished_good_against_job_card",
depends_on: "eval:doc.track_operations"
},
{ fieldtype: "Column Break" },
{
label: __("Final Operation"),
fieldtype: "Link",
fieldname: "final_operation",
options: "Operation",
default: "Assembly",
mandatory_depends_on: "eval:doc.make_finished_good_against_job_card",
depends_on: "eval:doc.make_finished_good_against_job_card"
},
],
primary_action_label: __("Create"),
primary_action: (values) => {
Expand Down
Loading

0 comments on commit 152a03a

Please sign in to comment.