diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2005dac37d7..1f6de1aa7b1 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -28,6 +28,11 @@ frappe.ui.form.on("Purchase Order", { } }); + frm.set_query("fg_item", "items", function() { + return { + filters: {'is_sub_contracted_item': 1} + } + }); }, company: function(frm) { @@ -109,6 +114,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e 'Purchase Invoice': 'Purchase Invoice', 'Stock Entry': 'Material to Supplier', 'Payment Entry': 'Payment', + 'Subcontracting Order': 'Subcontracting Order' } super.setup(); @@ -183,6 +189,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer")); } + if (doc.is_subcontracted) { + cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create')); + } } if(flt(doc.per_billed) < 100) cur_frm.add_custom_button(__('Purchase Invoice'), @@ -407,6 +416,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e }) } + make_subcontracting_order() { + frappe.model.open_mapped_doc({ + method: "erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order", + frm: cur_frm, + freeze_message: __("Creating Subcontracting Order ...") + }) + } + add_from_mappers() { var me = this; this.frm.add_custom_button(__('Material Request'), diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 896208f25e1..307b57607e0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -16,6 +16,8 @@ "supplier_name", "apply_tds", "tax_withholding_category", + "is_subcontracted", + "supplier_warehouse", "column_break1", "company", "transaction_date", @@ -51,10 +53,7 @@ "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", - "sec_warehouse", - "is_subcontracted", - "col_break_warehouse", - "supplier_warehouse", + "section_break_45", "before_items_section", "scan_barcode", "items_col_break", @@ -154,7 +153,8 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "reqd": 1 }, { "fieldname": "naming_series", @@ -439,11 +439,6 @@ "permlevel": 1, "print_hide": 1 }, - { - "fieldname": "sec_warehouse", - "fieldtype": "Section Break", - "label": "Subcontracting" - }, { "description": "Sets 'Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", @@ -452,15 +447,10 @@ "options": "Warehouse", "print_hide": 1 }, - { - "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" - }, { "default": "No", "fieldname": "is_subcontracted", "fieldtype": "Select", - "in_standard_filter": 1, "label": "Supply Raw Materials", "options": "No\nYes", "print_hide": 1 @@ -1138,16 +1128,21 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "fieldname": "section_break_45", + "fieldtype": "Section Break" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-09-28 13:10:47.955401", + "modified": "2022-04-26 18:46:58.863174", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -1194,6 +1189,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "supplier_name", "track_changes": 1 diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 582bd8d1db8..e8b8b87b98d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -69,7 +69,7 @@ class PurchaseOrder(BuyingController): self.validate_with_previous_doc() self.validate_for_subcontracting() self.validate_minimum_order_qty() - self.validate_bom_for_subcontracting_items() + self.validate_fg_item_for_subcontracting() self.create_raw_materials_supplied("supplied_items") self.set_received_qty_for_drop_ship_items() validate_inter_company_party( @@ -193,12 +193,25 @@ class PurchaseOrder(BuyingController): ).format(item_code, qty, itemwise_min_order_qty.get(item_code)) ) - def validate_bom_for_subcontracting_items(self): - if self.is_subcontracted == "Yes": + def validate_fg_item_for_subcontracting(self): + if self.is_subcontracted: for item in self.items: - if not item.bom: + if not item.fg_item: frappe.throw( - _("BOM is not specified for subcontracting item {0} at row {1}").format( + _("Finished Good Item is not specified for service item {0} at row {1}").format( + item.item_code, item.idx + ) + ) + else: + if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): + frappe.throw( + _( + "Finished Good Item {0} must be a sub-contracted item for service item {1} at row {2}" + ).format(item.fg_item, item.item_code, item.idx) + ) + if not item.fg_item_qty: + frappe.throw( + _("Finished Good Item Qty is not specified for service item {0} at row {1}").format( item.item_code, item.idx ) ) @@ -746,3 +759,43 @@ def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): "serial_no": "\n".join(row.serial_no) if row.serial_no else "", } ) + + +@frappe.whitelist() +def make_subcontracting_order(source_name, target_doc=None): + return get_mapped_subcontracting_order(source_name, target_doc) + + +def get_mapped_subcontracting_order(source_name, target_doc=None): + + if target_doc and isinstance(target_doc, str): + target_doc = json.loads(target_doc) + for key in ["service_items", "items", "supplied_items"]: + if key in target_doc: + del target_doc[key] + target_doc = json.dumps(target_doc) + + target_doc = get_mapped_doc( + "Purchase Order", + source_name, + { + "Purchase Order": { + "doctype": "Subcontracting Order", + "field_map": {}, + "field_no_map": ["total_qty", "total", "net_total"], + "validation": { + "docstatus": ["=", 1], + }, + }, + "Purchase Order Item": { + "doctype": "Subcontracting Order Service Item", + "field_map": {}, + "field_no_map": [], + }, + }, + target_doc, + ) + + target_doc.populate_items_table() + + return target_doc diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 81f20100c39..0c38c3e8da8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -22,6 +22,6 @@ def get_data(): "label": _("Reference"), "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, - {"label": _("Sub-contracting"), "items": ["Stock Entry"]}, + {"label": _("Sub-contracting"), "items": ["Subcontracting Order"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index a18c527644e..b4cdb182119 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -11,6 +11,8 @@ "supplier_part_no", "item_name", "product_bundle", + "fg_item", + "fg_item_qty", "column_break_4", "schedule_date", "expected_delivery_date", @@ -572,18 +574,18 @@ "read_only": 1 }, { - "depends_on": "eval:parent.is_subcontracted == 'Yes'", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", "options": "BOM", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "default": "0", - "depends_on": "eval:parent.is_subcontracted == 'Yes'", "fieldname": "include_exploded_items", "fieldtype": "Check", + "hidden": 1, "label": "Include Exploded Items", "print_hide": 1 }, @@ -845,13 +847,29 @@ "label": "Sales Order Packed Item", "no_copy": 1, "print_hide": 1 + }, + { + "depends_on": "eval:parent.is_subcontracted == 'Yes'", + "fieldname": "fg_item", + "fieldtype": "Link", + "label": "Finished Good Item", + "mandatory_depends_on": "eval:parent.is_subcontracted == 'Yes'", + "options": "Item" + }, + { + "default": "1", + "depends_on": "eval:parent.is_subcontracted == 'Yes'", + "fieldname": "fg_item_qty", + "fieldtype": "Float", + "label": "Finished Good Item Qty", + "mandatory_depends_on": "eval:parent.is_subcontracted == 'Yes'" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-02 13:10:18.398976", + "modified": "2022-04-07 14:53:16.684010", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 47892073f3b..4823e8b05cb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -803,10 +803,7 @@ class BuyingController(StockController, Subcontracting): if self.doctype == "Material Request": return - if hasattr(self, "is_subcontracted") and self.is_subcontracted == "Yes": - validate_item_type(self, "is_sub_contracted_item", "subcontracted") - else: - validate_item_type(self, "is_purchase_item", "purchase") + validate_item_type(self, "is_purchase_item", "purchase") def get_asset_item_details(asset_items): diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 54e5daa6bd4..a925470d607 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -84,7 +84,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac if (me.frm.doc.is_subcontracted == "Yes") { return{ query: "erpnext.controllers.queries.item_query", - filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 } + filters:{ 'supplier': me.frm.doc.supplier, 'is_stock_item': 0 } } } else { diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 9339c5d998f..82604267045 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -484,7 +484,7 @@ erpnext.utils.update_child_items = function(opts) { filters = {"is_sales_item": 1}; } else if (frm.doc.doctype == 'Purchase Order') { if (frm.doc.is_subcontracted == "Yes") { - filters = {"is_sub_contracted_item": 1}; + filters = {"is_stock_item": 0}; } else { filters = {"is_purchase_item": 1}; } diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 6cb9f7e479a..a66e6f85f68 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -43,20 +43,19 @@ class Bin(Document): def update_reserved_qty_for_sub_contracting(self): # reserved qty - po = frappe.qb.DocType("Purchase Order") - supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") + sco = frappe.qb.DocType("Subcontracting Order") + supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") reserved_qty_for_sub_contract = ( - frappe.qb.from_(po) + frappe.qb.from_(sco) .from_(supplied_item) .select(Sum(Coalesce(supplied_item.required_qty, 0))) .where( (supplied_item.rm_item_code == self.item_code) - & (po.name == supplied_item.parent) - & (po.docstatus == 1) - & (po.is_subcontracted == "Yes") - & (po.status != "Closed") - & (po.per_received < 100) + & (sco.name == supplied_item.parent) + & (sco.docstatus == 1) + & (sco.status != "Closed") + & (sco.per_received < 100) & (supplied_item.reserve_warehouse == self.warehouse) ) ).run()[0][0] or 0.0 @@ -67,21 +66,20 @@ class Bin(Document): materials_transferred = ( frappe.qb.from_(se) .from_(se_item) - .from_(po) + .from_(sco) .select( Sum(Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty)) ) .where( (se.docstatus == 1) & (se.purpose == "Send to Subcontractor") - & (Coalesce(se.purchase_order, "") != "") + & (Coalesce(se.subcontracting_order, "") != "") & ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code)) & (se.name == se_item.parent) - & (po.name == se.purchase_order) - & (po.docstatus == 1) - & (po.is_subcontracted == "Yes") - & (po.status != "Closed") - & (po.per_received < 100) + & (sco.name == se.subcontracting_order) + & (sco.docstatus == 1) + & (sco.status != "Closed") + & (sco.per_received < 100) ) ).run()[0][0] or 0.0 diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1e624714d05..b851795389b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -134,8 +134,8 @@ class StockEntry(StockController): update_serial_nos_after_submit(self, "items") self.update_work_order() - self.validate_purchase_order() - self.update_purchase_order_supplied_items() + self.validate_subcontracting_order() + self.update_subcontracting_order_supplied_items() self.make_gl_entries() @@ -154,7 +154,7 @@ class StockEntry(StockController): self.set_material_request_transfer_status("Completed") def on_cancel(self): - self.update_purchase_order_supplied_items() + self.update_subcontracting_order_supplied_items() if self.work_order and self.purpose == "Material Consumption for Manufacture": self.validate_work_order_status() @@ -810,8 +810,8 @@ class StockEntry(StockController): serial_nos.append(sn) - def validate_purchase_order(self): - """Throw exception if more raw material is transferred against Purchase Order than in + def validate_subcontracting_order(self): + """Throw exception if more raw material is transferred against Subcontracting Order than in the raw materials supplied table""" backflush_raw_materials_based_on = frappe.db.get_single_value( "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" @@ -819,24 +819,28 @@ class StockEntry(StockController): qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) - if not (self.purpose == "Send to Subcontractor" and self.purchase_order): + if not (self.purpose == "Send to Subcontractor" and self.subcontracting_order): return if backflush_raw_materials_based_on == "BOM": - purchase_order = frappe.get_doc("Purchase Order", self.purchase_order) + subcontracting_order = frappe.get_doc("Subcontracting Order", self.subcontracting_order) for se_item in self.items: item_code = se_item.original_item or se_item.item_code precision = cint(frappe.db.get_default("float_precision")) or 3 required_qty = sum( - [flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code] + [ + flt(d.required_qty) + for d in subcontracting_order.supplied_items + if d.rm_item_code == item_code + ] ) total_allowed = required_qty + (required_qty * (qty_allowance / 100)) if not required_qty: bom_no = frappe.db.get_value( - "Purchase Order Item", - {"parent": self.purchase_order, "item_code": se_item.subcontracted_item}, + "Subcontracting Order Item", + {"parent": self.subcontracting_order, "item_code": se_item.subcontracted_item}, "bom", ) @@ -848,7 +852,7 @@ class StockEntry(StockController): required_qty = sum( [ flt(d.required_qty) - for d in purchase_order.supplied_items + for d in subcontracting_order.supplied_items if d.rm_item_code == original_item_code ] ) @@ -857,26 +861,39 @@ class StockEntry(StockController): if not required_qty: frappe.throw( - _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format( - se_item.item_code, self.purchase_order + _("Item {0} not found in 'Raw Materials Supplied' table in Subcontracting Order {1}").format( + se_item.item_code, self.subcontracting_order ) ) total_supplied = frappe.db.sql( """select sum(transfer_qty) from `tabStock Entry Detail`, `tabStock Entry` - where `tabStock Entry`.purchase_order = %s + where `tabStock Entry`.subcontracting_order = %s and `tabStock Entry`.docstatus = 1 and `tabStock Entry Detail`.item_code = %s and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", - (self.purchase_order, se_item.item_code), + (self.subcontracting_order, se_item.item_code), )[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): frappe.throw( - _("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format( - se_item.idx, se_item.item_code, total_allowed, self.purchase_order + _( + "Row {0}# Item {1} cannot be transferred more than {2} against Subcontracting Order {3}" + ).format( + se_item.idx, se_item.item_code, total_allowed, self.subcontracting_order ) ) + elif not se_item.sco_rm_detail: + filters = { + "parent": self.subcontracting_order, + "docstatus": 1, + "rm_item_code": se_item.item_code, + "main_item_code": se_item.subcontracted_item, + } + + sco_rm_detail = frappe.db.get_value("Subcontracting Order Supplied Item", filters, "name") + if sco_rm_detail: + se_item.db_set("sco_rm_detail", sco_rm_detail) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: @@ -885,17 +902,17 @@ class StockEntry(StockController): row.idx, frappe.bold(row.item_code) ) ) - elif not row.po_detail: + elif not row.sco_rm_detail: filters = { - "parent": self.purchase_order, + "parent": self.subcontracting_order, "docstatus": 1, "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item, } - po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") - if po_detail: - row.db_set("po_detail", po_detail) + sco_rm_detail = frappe.db.get_value("Subcontracting Order Supplied Item", filters, "name") + if sco_rm_detail: + row.db_set("sco_rm_detail", sco_rm_detail) def validate_bom(self): for d in self.get("items"): @@ -1901,7 +1918,7 @@ class StockEntry(StockController): se_child.is_process_loss = item_row.get("is_process_loss", 0) for field in [ - "po_detail", + "sco_rm_detail", "original_item", "expense_account", "description", @@ -1975,26 +1992,26 @@ class StockEntry(StockController): else: frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) - def update_purchase_order_supplied_items(self): - if self.purchase_order and ( + def update_subcontracting_order_supplied_items(self): + if self.subcontracting_order and ( self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return ): - # Get PO Supplied Items Details + # Get SCO Supplied Items Details item_wh = frappe._dict( frappe.db.sql( """ select rm_item_code, reserve_warehouse - from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - where po.name = poitemsup.parent - and po.name = %s""", - self.purchase_order, + from `tabSubcontracting Order` sco, `tabSubcontracting Order Supplied Item` scoitemsup + where sco.name = scoitemsup.parent + and sco.name = %s""", + self.subcontracting_order, ) ) - supplied_items = get_supplied_items(self.purchase_order) + supplied_items = get_supplied_items(self.subcontracting_order) for name, item in supplied_items.items(): - frappe.db.set_value("Purchase Order Item Supplied", name, item) + frappe.db.set_value("Subcontracting Order Supplied Item", name, item) # Update reserved sub contracted quantity in bin based on Supplied Item Details and for d in self.get("items"): @@ -2479,25 +2496,25 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None): return sample_quantity -def get_supplied_items(purchase_order): +def get_supplied_items(subcontracting_order): fields = [ "`tabStock Entry Detail`.`transfer_qty`", "`tabStock Entry`.`is_return`", - "`tabStock Entry Detail`.`po_detail`", + "`tabStock Entry Detail`.`sco_rm_detail`", "`tabStock Entry Detail`.`item_code`", ] filters = [ ["Stock Entry", "docstatus", "=", 1], - ["Stock Entry", "purchase_order", "=", purchase_order], + ["Stock Entry", "subcontracting_order", "=", subcontracting_order], ] supplied_item_details = {} for row in frappe.get_all("Stock Entry", fields=fields, filters=filters): - if not row.po_detail: + if not row.sco_rm_detail: continue - key = row.po_detail + key = row.sco_rm_detail if key not in supplied_item_details: supplied_item_details.setdefault( key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f72588e034d..0d7d47262a5 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -237,8 +237,8 @@ def validate_item_details(args, item): throw(_("Item {0} is a template, please select one of its variants").format(item.name)) elif args.transaction_type == "buying" and args.doctype != "Material Request": - if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1: - throw(_("Item {0} must be a Sub-contracted Item").format(item.name)) + if args.get("is_subcontracted") == "Yes" and item.is_stock_item: + throw(_("Item {0} must be a Non-Stock Item").format(item.name)) def get_basic_details(args, item, overwrite_warehouse=True):