From f94d607288b4e62b85dc8397c67d26eb6a56e092 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 1 Apr 2022 16:28:34 +0530 Subject: [PATCH 01/41] feat: New module "Subcontracting" --- erpnext/modules.txt | 1 + erpnext/subcontracting/__init__.py | 0 2 files changed, 1 insertion(+) create mode 100644 erpnext/subcontracting/__init__.py diff --git a/erpnext/modules.txt b/erpnext/modules.txt index c6b3159e0fc..e194b4d7c41 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -22,3 +22,4 @@ Payroll Telephony Bulk Transaction E-commerce +Subcontracting \ No newline at end of file diff --git a/erpnext/subcontracting/__init__.py b/erpnext/subcontracting/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 77db843692118a06a491ff4add2085e14a128030 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 5 May 2022 19:41:05 +0530 Subject: [PATCH 02/41] refactor!: Make required changes to create SCO from PO --- .../doctype/purchase_order/purchase_order.js | 17 ++++ .../purchase_order/purchase_order.json | 28 +++--- .../doctype/purchase_order/purchase_order.py | 63 ++++++++++++- .../purchase_order_dashboard.py | 2 +- .../purchase_order_item.json | 26 +++++- erpnext/controllers/buying_controller.py | 5 +- erpnext/public/js/controllers/buying.js | 2 +- erpnext/public/js/utils.js | 2 +- erpnext/stock/doctype/bin/bin.py | 28 +++--- .../stock/doctype/stock_entry/stock_entry.py | 91 +++++++++++-------- erpnext/stock/get_item_details.py | 4 +- 11 files changed, 182 insertions(+), 86 deletions(-) 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): From 68c21d9895d0daf77f5360f189eef70835069ddc Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 26 Apr 2022 17:02:12 +0530 Subject: [PATCH 03/41] feat: Add fields "subcontracting_order" and "sco_rm_detail" in SE and SE Detail --- erpnext/stock/doctype/stock_entry/stock_entry.json | 8 ++++++++ .../doctype/stock_entry_detail/stock_entry_detail.json | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index c38dfaa1c84..7b9eccd4aa6 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -15,6 +15,7 @@ "add_to_transit", "work_order", "purchase_order", + "subcontracting_order", "delivery_note_no", "sales_invoice_no", "pick_list", @@ -153,6 +154,13 @@ "label": "Purchase Order", "options": "Purchase Order" }, + { + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "options": "Subcontracting Order" + }, { "depends_on": "eval:doc.purpose==\"Sales Return\"", "fieldname": "delivery_note_no", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 83aed904ddd..efde46d0604 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -68,6 +68,7 @@ "against_stock_entry", "ste_detail", "po_detail", + "sco_rm_detail", "putaway_rule", "column_break_51", "reference_purchase_receipt", @@ -493,6 +494,15 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "sco_rm_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "SCO Supplied Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { "default": "0", "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", From 29a1cb89c245f68a4db6190ccd2d83ea91c348fe Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 26 Apr 2022 17:29:45 +0530 Subject: [PATCH 04/41] feat: SubcontractingController --- .../controllers/subcontracting_controller.py | 641 ++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 erpnext/controllers/subcontracting_controller.py diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py new file mode 100644 index 00000000000..b393737740f --- /dev/null +++ b/erpnext/controllers/subcontracting_controller.py @@ -0,0 +1,641 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import copy +from collections import defaultdict + +import frappe +from frappe import _ +from frappe.utils import cint, cstr, flt, get_link_to_form + +from erpnext.controllers.stock_controller import StockController +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.utils import get_incoming_rate + + +class SubcontractingController(StockController): + def before_validate(self): + self.remove_empty_rows() + self.set_items_conversion_factor() + + def validate(self): + self.validate_items() + self.create_raw_materials_supplied() + + def remove_empty_rows(self): + for key in ["service_items", "items", "supplied_items"]: + if self.get(key): + idx = 1 + for item in self.get(key)[:]: + if not (item.get("item_code") or item.get("main_item_code")): + self.get(key).remove(item) + else: + item.idx = idx + idx += 1 + + def set_items_conversion_factor(self): + for item in self.get("items"): + if not item.conversion_factor: + item.conversion_factor = 1 + + def validate_items(self): + for item in self.items: + if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"): + msg = f"Item {item.item_name} must be a subcontracted item." + frappe.throw(_(msg)) + if item.bom: + bom = frappe.get_doc("BOM", item.bom) + if not bom.is_active: + msg = f"Please select an active BOM for Item {item.item_name}." + frappe.throw(_(msg)) + if bom.item != item.item_code: + msg = f"Please select an valid BOM for Item {item.item_name}." + frappe.throw(_(msg)) + + def __get_data_before_save(self): + item_dict = {} + if self.doctype == "Subcontracting Receipt" and self._doc_before_save: + for row in self._doc_before_save.get("items"): + item_dict[row.name] = (row.item_code, row.qty) + + return item_dict + + def __identify_change_in_item_table(self): + self.__changed_name = [] + self.__reference_name = [] + + if self.doctype == "Subcontracting Order" or self.is_new(): + self.set(self.raw_material_table, []) + return + + item_dict = self.__get_data_before_save() + if not item_dict: + return True + + for row in self.items: + self.__reference_name.append(row.name) + if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]: + self.__changed_name.append(row.name) + + if item_dict.get(row.name): + del item_dict[row.name] + + self.__changed_name.extend(item_dict.keys()) + + def __get_backflush_based_on(self): + self.backflush_based_on = frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ) + + def initialized_fields(self): + self.available_materials = frappe._dict() + self.__transferred_items = frappe._dict() + self.alternative_item_details = frappe._dict() + self.__get_backflush_based_on() + + def __get_subcontracting_orders(self): + self.subcontracting_orders = [] + + if self.doctype == "Subcontracting Order": + return + + self.subcontracting_orders = [ + item.subcontracting_order for item in self.items if item.subcontracting_order + ] + + def __get_pending_qty_to_receive(self): + """Get qty to be received against the subcontracting order.""" + + self.qty_to_be_received = defaultdict(float) + + if ( + self.doctype != "Subcontracting Order" + and self.backflush_based_on != "BOM" + and self.subcontracting_orders + ): + for row in frappe.get_all( + "Subcontracting Order Item", + fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], + filters={"docstatus": 1, "parent": ("in", self.subcontracting_orders)}, + ): + + self.qty_to_be_received[(row.item_code, row.parent)] += row.qty + + def __get_transferred_items(self): + fields = ["`tabStock Entry`.`subcontracting_order`"] + alias_dict = { + "item_code": "rm_item_code", + "subcontracted_item": "main_item_code", + "basic_rate": "rate", + } + + child_table_fields = [ + "item_code", + "item_name", + "description", + "qty", + "basic_rate", + "amount", + "serial_no", + "uom", + "subcontracted_item", + "stock_uom", + "batch_no", + "conversion_factor", + "s_warehouse", + "t_warehouse", + "item_group", + "sco_rm_detail", + ] + + if self.backflush_based_on == "BOM": + child_table_fields.append("original_item") + + for field in child_table_fields: + fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}") + + filters = [ + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purpose", "=", "Send to Subcontractor"], + ["Stock Entry", "subcontracting_order", "in", self.subcontracting_orders], + ] + + return frappe.get_all("Stock Entry", fields=fields, filters=filters) + + def __set_alternative_item_details(self, row): + if row.get("original_item"): + self.alternative_item_details[row.get("original_item")] = row + + def __get_received_items(self, doctype): + fields = [] + self.sco_field = "subcontracting_order" + + for field in ["name", self.sco_field, "parent"]: + fields.append(f"`tab{doctype} Item`.`{field}`") + + filters = [ + [doctype, "docstatus", "=", 1], + [f"{doctype} Item", self.sco_field, "in", self.subcontracting_orders], + ] + + return frappe.get_all(f"{doctype}", fields=fields, filters=filters) + + def __get_consumed_items(self, doctype, scr_items): + return frappe.get_all( + "Subcontracting Receipt Supplied Item", + fields=[ + "serial_no", + "rm_item_code", + "reference_name", + "batch_no", + "consumed_qty", + "main_item_code", + ], + filters={"docstatus": 1, "reference_name": ("in", list(scr_items)), "parenttype": doctype}, + ) + + def __update_consumed_materials(self, doctype, return_consumed_items=False): + """Deduct the consumed materials from the available materials.""" + + scr_items = self.__get_received_items(doctype) + if not scr_items: + return ([], {}) if return_consumed_items else None + + scr_items = { + item.name: item.get(self.get("sco_field") or "subcontracting_order") for item in scr_items + } + consumed_materials = self.__get_consumed_items(doctype, scr_items.keys()) + + if return_consumed_items: + return (consumed_materials, scr_items) + + for row in consumed_materials: + key = (row.rm_item_code, row.main_item_code, scr_items.get(row.reference_name)) + if not self.available_materials.get(key): + continue + + self.available_materials[key]["qty"] -= row.consumed_qty + if row.serial_no: + self.available_materials[key]["serial_no"] = list( + set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) + ) + + if row.batch_no: + self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty + + def get_available_materials(self): + """Get the available raw materials which has been transferred to the supplier. + available_materials = { + (item_code, subcontracted_item, subcontracting_order): { + 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details + } + } + """ + if not self.subcontracting_orders: + return + + for row in self.__get_transferred_items(): + key = (row.rm_item_code, row.main_item_code, row.subcontracting_order) + + if key not in self.available_materials: + self.available_materials.setdefault( + key, + frappe._dict( + { + "qty": 0, + "serial_no": [], + "batch_no": defaultdict(float), + "item_details": row, + "sco_rm_details": [], + } + ), + ) + + details = self.available_materials[key] + details.qty += row.qty + details.sco_rm_details.append(row.sco_rm_detail) + + if row.serial_no: + details.serial_no.extend(get_serial_nos(row.serial_no)) + + if row.batch_no: + details.batch_no[row.batch_no] += row.qty + + self.__set_alternative_item_details(row) + + self.__transferred_items = copy.deepcopy(self.available_materials) + self.__update_consumed_materials("Subcontracting Receipt") + + def __remove_changed_rows(self): + if not self.__changed_name: + return + + i = 1 + self.set(self.raw_material_table, []) + for item in self._doc_before_save.supplied_items: + if item.reference_name in self.__changed_name: + continue + + if item.reference_name not in self.__reference_name: + continue + + item.idx = i + self.append("supplied_items", item) + + i += 1 + + def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" + fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] + + alias_dict = { + "item_code": "rm_item_code", + "name": "bom_detail_no", + "source_warehouse": "reserve_warehouse", + } + for field in [ + "item_code", + "name", + "rate", + "stock_uom", + "source_warehouse", + "description", + "item_name", + "stock_uom", + ]: + fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}") + + filters = [ + [doctype, "parent", "=", bom_no], + [doctype, "docstatus", "=", 1], + ["BOM", "item", "=", item_code], + [doctype, "sourced_by_supplier", "=", 0], + ] + + return ( + frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] + ) + + def __update_reserve_warehouse(self, row, item): + if self.doctype == "Subcontracting Order": + row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse + + def __set_alternative_item(self, bom_item): + if self.alternative_item_details.get(bom_item.rm_item_code): + bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) + + def __set_serial_nos(self, item_row, rm_obj): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.subcontracting_order) + if self.available_materials.get(key) and self.available_materials[key]["serial_no"]: + used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)] + rm_obj.serial_no = "\n".join(used_serial_nos) + + # Removed the used serial nos from the list + for sn in used_serial_nos: + self.available_materials[key]["serial_no"].remove(sn) + + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + rm_obj.update( + { + "consumed_qty": qty, + "batch_no": batch_no, + "required_qty": qty, + "subcontracting_order": item_row.subcontracting_order, + } + ) + + self.__set_serial_nos(item_row, rm_obj) + + def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): + rm_obj.required_qty = required_qty + rm_obj.consumed_qty = consumed_qty + + def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.subcontracting_order) + + if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: + new_rm_obj = None + for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): + if batch_qty >= qty: + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.available_materials[key]["batch_no"][batch_no] -= qty + return + + elif qty > 0 and batch_qty > 0: + qty -= batch_qty + new_rm_obj = self.append(self.raw_material_table, bom_item) + new_rm_obj.reference_name = item_row.name + self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.available_materials[key]["batch_no"][batch_no] = 0 + + if abs(qty) > 0 and not new_rm_obj: + self.__set_consumed_qty(rm_obj, qty) + else: + self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) + self.__set_serial_nos(item_row, rm_obj) + + def __add_supplied_item(self, item_row, bom_item, qty): + bom_item.conversion_factor = item_row.conversion_factor + rm_obj = self.append(self.raw_material_table, bom_item) + rm_obj.reference_name = item_row.name + + if self.doctype == "Subcontracting Receipt": + args = frappe._dict( + { + "item_code": rm_obj.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(rm_obj.consumed_qty), + "serial_no": rm_obj.serial_no, + "batch_no": rm_obj.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": 1, + } + ) + rm_obj.rate = get_incoming_rate(args) + + if self.doctype == "Subcontracting Order": + rm_obj.required_qty = qty + rm_obj.amount = rm_obj.required_qty * rm_obj.rate + else: + rm_obj.consumed_qty = 0 + rm_obj.subcontracting_order = item_row.subcontracting_order + self.__set_batch_nos(bom_item, item_row, rm_obj, qty) + + def __get_qty_based_on_material_transfer(self, item_row, transfer_item): + key = (item_row.item_code, item_row.subcontracting_order) + + if self.qty_to_be_received == item_row.qty: + return transfer_item.qty + + if self.qty_to_be_received: + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + transfer_item.item_details.required_qty = transfer_item.qty + + if transfer_item.serial_no or frappe.get_cached_value( + "UOM", transfer_item.item_details.stock_uom, "must_be_whole_number" + ): + return frappe.utils.ceil(qty) + + return qty + + def __set_supplied_items(self): + self.bom_items = {} + + has_supplied_items = True if self.get(self.raw_material_table) else False + for row in self.items: + if self.doctype != "Subcontracting Order" and ( + (self.__changed_name and row.name not in self.__changed_name) + or (has_supplied_items and not self.__changed_name) + ): + continue + + if self.doctype == "Subcontracting Order" or self.backflush_based_on == "BOM": + for bom_item in self.__get_materials_from_bom( + row.item_code, row.bom, row.get("include_exploded_items") + ): + qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor + bom_item.main_item_code = row.item_code + self.__update_reserve_warehouse(bom_item, row) + self.__set_alternative_item(bom_item) + self.__add_supplied_item(row, bom_item, qty) + + elif self.backflush_based_on != "BOM": + for key, transfer_item in self.available_materials.items(): + if (key[1], key[2]) == (row.item_code, row.subcontracting_order) and transfer_item.qty > 0: + qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 + transfer_item.qty -= qty + self.__add_supplied_item(row, transfer_item.get("item_details"), qty) + + if self.qty_to_be_received: + self.qty_to_be_received[(row.item_code, row.subcontracting_order)] -= row.qty + + def __prepare_supplied_items(self): + self.initialized_fields() + self.__get_subcontracting_orders() + self.__get_pending_qty_to_receive() + self.get_available_materials() + self.__remove_changed_rows() + self.__set_supplied_items() + + def __validate_batch_no(self, row, key): + if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( + "batch_no" + ): + link = get_link_to_form("Subcontracting Order", row.subcontracting_order) + msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Subcontracting Order {link}' + frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) + + def __validate_serial_no(self, row, key): + if row.get("serial_no"): + serial_nos = get_serial_nos(row.get("serial_no")) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no")) + + if incorrect_sn: + incorrect_sn = "\n".join(incorrect_sn) + link = get_link_to_form("Subcontracting Order", row.subcontracting_order) + msg = f"The Serial Nos {incorrect_sn} has not supplied against the Subcontracting Order {link}" + frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) + + def __validate_supplied_items(self): + if self.doctype != "Subcontracting Receipt": + return + + for row in self.get(self.raw_material_table): + key = (row.rm_item_code, row.main_item_code, row.subcontracting_order) + if not self.__transferred_items or not self.__transferred_items.get(key): + return + + self.__validate_batch_no(row, key) + self.__validate_serial_no(row, key) + + def set_materials_for_subcontracted_items(self, raw_material_table): + self.raw_material_table = raw_material_table + self.__identify_change_in_item_table() + self.__prepare_supplied_items() + self.__validate_supplied_items() + + def create_raw_materials_supplied(self, raw_material_table="supplied_items"): + self.set_materials_for_subcontracted_items(raw_material_table) + + if self.doctype == "Subcontracting Receipt": + for item in self.get("items"): + item.rm_supp_cost = 0.0 + + def __update_consumed_qty_in_sco(self, itemwise_consumed_qty): + fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"] + filters = {"docstatus": 1, "parent": ("in", self.subcontracting_orders)} + + for row in frappe.get_all( + "Subcontracting Order Supplied Item", fields=fields, filters=filters, order_by="idx" + ): + key = (row.rm_item_code, row.main_item_code, row.parent) + consumed_qty = itemwise_consumed_qty.get(key, 0) + + if row.supplied_qty < consumed_qty: + consumed_qty = row.supplied_qty + + itemwise_consumed_qty[key] -= consumed_qty + frappe.db.set_value( + "Subcontracting Order Supplied Item", row.name, "consumed_qty", consumed_qty + ) + + def set_consumed_qty_in_sco(self): + # Update consumed qty back in the subcontracting order + self.__get_subcontracting_orders() + itemwise_consumed_qty = defaultdict(float) + consumed_items, scr_items = self.__update_consumed_materials( + "Subcontracting Receipt", return_consumed_items=True + ) + + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, scr_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty + + self.__update_consumed_qty_in_sco(itemwise_consumed_qty) + + def update_ordered_and_reserved_qty(self): + sco_map = {} + for item in self.get("items"): + if self.doctype == "Subcontracting Receipt" and item.subcontracting_order: + sco_map.setdefault(item.subcontracting_order, []).append(item.subcontracting_order_item) + + for sco, sco_item_rows in sco_map.items(): + if sco and sco_item_rows: + sco_doc = frappe.get_doc("Subcontracting Order", sco) + + if sco_doc.status in ["Closed", "Cancelled"]: + frappe.throw( + _("{0} {1} is cancelled or closed").format(_("Subcontracting Order"), sco), + frappe.InvalidStatusError, + ) + + sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows) + sco_doc.update_reserved_qty_for_subcontracting() + + def make_sl_entries_for_supplier_warehouse(self, sl_entries): + if hasattr(self, "supplied_items"): + for item in self.get("supplied_items"): + # negative quantity is passed, as raw material qty has to be decreased + # when SCR is submitted and it has to be increased when SCR is cancelled + sl_entries.append( + self.get_sl_entries( + item, + { + "item_code": item.rm_item_code, + "warehouse": self.supplier_warehouse, + "actual_qty": -1 * flt(item.consumed_qty), + "dependant_sle_voucher_detail_no": item.reference_name, + }, + ) + ) + + def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False): + self.update_ordered_and_reserved_qty() + + sl_entries = [] + stock_items = self.get_stock_items() + + for item in self.get("items"): + if item.item_code in stock_items and item.warehouse: + scr_qty = flt(item.qty) * flt(item.conversion_factor) + + if scr_qty: + sle = self.get_sl_entries( + item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()} + ) + rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9 + incoming_rate = flt(item.rate, rate_db_precision) + sle.update( + { + "incoming_rate": incoming_rate, + "recalculate_rate": 1, + } + ) + sl_entries.append(sle) + + if flt(item.rejected_qty) != 0: + sl_entries.append( + self.get_sl_entries( + item, + { + "warehouse": item.rejected_warehouse, + "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor), + "serial_no": cstr(item.rejected_serial_no).strip(), + "incoming_rate": 0.0, + "recalculate_rate": 1, + }, + ) + ) + + self.make_sl_entries_for_supplier_warehouse(sl_entries) + self.make_sl_entries( + sl_entries, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) + + def get_supplied_items_cost(self, item_row_id): + supplied_items_cost = 0.0 + for item in self.get("supplied_items"): + if item.reference_name == item_row_id: + item.amount = flt(flt(item.consumed_qty) * flt(item.rate), item.precision("amount")) + supplied_items_cost += item.amount + + return supplied_items_cost + + @property + def sub_contracted_items(self): + if not hasattr(self, "_sub_contracted_items"): + self._sub_contracted_items = [] + item_codes = list(set(item.item_code for item in self.get("items"))) + if item_codes: + items = frappe.get_all( + "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1} + ) + self._sub_contracted_items = [item.name for item in items] + + return self._sub_contracted_items From dcac7eb67c50d57ca639eee36c1b9840b9c1b6d5 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 1 Apr 2022 19:25:44 +0530 Subject: [PATCH 05/41] feat: New DocType "Subcontracting Order Service Item" --- erpnext/subcontracting/doctype/__init__.py | 0 .../__init__.py | 0 .../subcontracting_order_service_item.json | 131 ++++++++++++++++++ .../subcontracting_order_service_item.py | 9 ++ 4 files changed, 140 insertions(+) create mode 100644 erpnext/subcontracting/doctype/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py diff --git a/erpnext/subcontracting/doctype/__init__.py b/erpnext/subcontracting/doctype/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json new file mode 100644 index 00000000000..f213313ef6b --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json @@ -0,0 +1,131 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:23:05.728354", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "section_break_4", + "qty", + "column_break_6", + "rate", + "column_break_8", + "amount", + "section_break_10", + "fg_item", + "column_break_12", + "fg_item_qty" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "reqd": 1, + "width": "60px" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "fg_item", + "fieldtype": "Link", + "label": "Finished Good Item", + "options": "Item", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "fg_item_qty", + "fieldtype": "Float", + "label": "Finished Good Item Quantity", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + } + ], + "istable": 1, + "links": [], + "modified": "2022-04-07 11:43:43.094867", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Service Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py new file mode 100644 index 00000000000..ad6289d9231 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingOrderServiceItem(Document): + pass From f8b759429232df4da6a9bd577e8ac3623fb1db41 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 1 Apr 2022 19:28:25 +0530 Subject: [PATCH 06/41] feat: New DocType "Subcontracting Order Item" --- .../subcontracting_order_item/__init__.py | 0 .../subcontracting_order_item.json | 326 ++++++++++++++++++ .../subcontracting_order_item.py | 9 + 3 files changed, 335 insertions(+) create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json new file mode 100644 index 00000000000..291f47a6340 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -0,0 +1,326 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:26:31.475015", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "bom", + "include_exploded_items", + "column_break_3", + "schedule_date", + "expected_delivery_date", + "description_section", + "description", + "column_break_8", + "image", + "image_view", + "quantity_and_rate_section", + "qty", + "received_qty", + "returned_qty", + "column_break_13", + "stock_uom", + "conversion_factor", + "section_break_16", + "rate", + "amount", + "column_break_19", + "rm_cost_per_qty", + "service_cost_per_qty", + "additional_cost_per_qty", + "warehouse_section", + "warehouse", + "accounting_details_section", + "expense_account", + "manufacture_section", + "manufacturer", + "manufacturer_part_no", + "section_break_34", + "page_break" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1, + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "columns": 2, + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "bold": 1, + "fieldname": "expected_delivery_date", + "fieldtype": "Date", + "label": "Expected Delivery Date", + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image" + }, + { + "fieldname": "image_view", + "fieldtype": "Image", + "label": "Image View", + "options": "image", + "print_hide": 1 + }, + { + "fieldname": "quantity_and_rate_section", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "bold": 1, + "columns": 1, + "default": "1", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "read_only": 1, + "reqd": 1, + "width": "60px" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "print_hide": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "warehouse_section", + "fieldtype": "Section Break", + "label": "Warehouse Details" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse", + "print_hide": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Expense Account", + "options": "Account", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "manufacture_section", + "fieldtype": "Section Break", + "label": "Manufacture" + }, + { + "fieldname": "manufacturer", + "fieldtype": "Link", + "label": "Manufacturer", + "options": "Manufacturer" + }, + { + "fieldname": "manufacturer_part_no", + "fieldtype": "Data", + "label": "Manufacturer Part Number" + }, + { + "depends_on": "item_code", + "fetch_from": "item_code.default_bom", + "fieldname": "bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "BOM", + "options": "BOM", + "print_hide": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "label": "Include Exploded Items", + "print_hide": 1 + }, + { + "fieldname": "service_cost_per_qty", + "fieldtype": "Currency", + "label": "Service Cost Per Qty", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "additional_cost_per_qty", + "fieldtype": "Currency", + "label": "Additional Cost Per Qty", + "read_only": 1 + }, + { + "fieldname": "rm_cost_per_qty", + "fieldtype": "Currency", + "label": "Raw Material Cost Per Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "label": "Page Break", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "section_break_34", + "fieldtype": "Section Break" + }, + { + "depends_on": "received_qty", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-04-11 21:28:06.585338", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py new file mode 100644 index 00000000000..174f5b212c2 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingOrderItem(Document): + pass From f49c51ab7439f668b1a25049e88394a4c8649912 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 1 Apr 2022 19:31:10 +0530 Subject: [PATCH 07/41] feat: New DocType "Subcontracting Order Supplied Item" --- .../__init__.py | 0 .../subcontracting_order_supplied_item.json | 178 ++++++++++++++++++ .../subcontracting_order_supplied_item.py | 9 + 3 files changed, 187 insertions(+) create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json new file mode 100644 index 00000000000..a206a21ca63 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json @@ -0,0 +1,178 @@ +{ + "actions": [], + "creation": "2022-04-01 19:29:30.923800", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "main_item_code", + "rm_item_code", + "column_break_3", + "stock_uom", + "conversion_factor", + "reserve_warehouse", + "column_break_6", + "bom_detail_no", + "reference_name", + "section_break_9", + "rate", + "column_break_11", + "amount", + "section_break_13", + "required_qty", + "supplied_qty", + "column_break_16", + "consumed_qty", + "returned_qty", + "total_supplied_qty" + ], + "fields": [ + { + "columns": 2, + "fieldname": "main_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "rm_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Raw Material Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock Uom", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "reserve_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reserve Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "bom_detail_no", + "fieldtype": "Data", + "label": "BOM Detail No", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference Name", + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "read_only": 1 + }, + { + "fieldname": "supplied_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "consumed_qty", + "fieldtype": "Float", + "label": "Consumed Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "hidden": 1 + }, + { + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Total Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "hide_toolbar": 1, + "istable": 1, + "links": [], + "modified": "2022-04-07 12:58:28.208847", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Supplied Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py new file mode 100644 index 00000000000..5619e3b79ae --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingOrderSuppliedItem(Document): + pass From 249726b845522f9b7858ca2ed1845e284c7a91f9 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 22 Apr 2022 17:09:19 +0530 Subject: [PATCH 08/41] feat: New DocType "Subcontracting Order" --- .../controllers/subcontracting_controller.py | 11 + .../stock/doctype/stock_entry/stock_entry.py | 10 + .../doctype/subcontracting_order/__init__.py | 0 .../subcontracting_order.js | 322 ++++++++++++ .../subcontracting_order.json | 485 ++++++++++++++++++ .../subcontracting_order.py | 372 ++++++++++++++ .../subcontracting_order_dashboard.py | 8 + .../subcontracting_order_list.js | 16 + .../test_subcontracting_order.py | 8 + 9 files changed, 1232 insertions(+) create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index b393737740f..4e0d91147e9 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -627,6 +627,17 @@ class SubcontractingController(StockController): return supplied_items_cost + def set_subcontracting_order_status(self): + if self.doctype == "Subcontracting Order": + self.update_status() + elif self.doctype == "Subcontracting Receipt": + self.__get_subcontracting_orders + + if self.subcontracting_orders: + for sco in set(self.subcontracting_orders): + sco_doc = frappe.get_doc("Subcontracting Order", sco) + sco_doc.update_status() + @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b851795389b..5adb8b273ef 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -136,6 +136,7 @@ class StockEntry(StockController): self.update_work_order() self.validate_subcontracting_order() self.update_subcontracting_order_supplied_items() + self.update_subcontracting_order_status() self.make_gl_entries() @@ -155,6 +156,7 @@ class StockEntry(StockController): def on_cancel(self): self.update_subcontracting_order_supplied_items() + self.update_subcontracting_order_status() if self.work_order and self.purpose == "Material Consumption for Manufacture": self.validate_work_order_status() @@ -2212,6 +2214,14 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + def update_subcontracting_order_status(self): + if self.subcontracting_order and self.purpose == "Send to Subcontractor": + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + update_subcontracting_order_status, + ) + + update_subcontracting_order_status(self.subcontracting_order) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js new file mode 100644 index 00000000000..80fe94483b1 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -0,0 +1,322 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide('erpnext.buying'); + +frappe.ui.form.on('Subcontracting Order', { + setup: (frm) => { + frm.get_field("items").grid.cannot_add_rows = true; + frm.get_field("items").grid.only_sortable(); + + frm.set_indicator_formatter('item_code', + (doc) => (doc.qty <= doc.received_qty) ? 'green' : 'orange'); + + frm.set_query('supplier_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('purchase_order', () => { + return { + filters: { + docstatus: 1, + is_subcontracted: "Yes" + } + }; + }); + + frm.set_query('set_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + + frm.set_query('expense_account', 'items', () => ({ + query: 'erpnext.controllers.queries.get_expense_account', + filters: { + company: frm.doc.company + } + })); + + frm.set_query('bom', 'items', (doc, cdt, cdn) => { + let d = locals[cdt][cdn]; + return { + filters: { + item: d.item_code, + is_active: 1, + docstatus: 1, + company: frm.doc.company + } + }; + }); + + frm.set_query('set_reserve_warehouse', () => { + return { + filters: { + company: frm.doc.company, + name: ['!=', frm.doc.supplier_warehouse], + is_group: 0 + } + }; + }); + }, + + onload: (frm) => { + if (!frm.doc.transaction_date) { + frm.set_value('transaction_date', frappe.datetime.get_today()); + } + }, + + purchase_order: (frm) => { + frm.set_value('service_items', null); + frm.set_value('items', null); + frm.set_value('supplied_items', null); + + if (frm.doc.purchase_order) { + erpnext.utils.map_current_doc({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order', + source_name: frm.doc.purchase_order, + target_doc: frm, + freeze: true, + freeze_message: __('Mapping Subcontracting Order ...'), + }); + } + }, + + refresh: function (frm) { + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function (frm) { + let sco_rm_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100)) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + sco_rm_details.push(d.name); + } + }); + } + + if (sco_rm_details && sco_rm_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.get_materials_from_supplier', + freeze: true, + freeze_message: __('Creating Stock Entry'), + args: { subcontracting_order: frm.doc.name, sco_rm_details: sco_rm_details }, + callback: function (r) { + if (r && r.message) { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } + } +}); + +erpnext.buying.SubcontractingOrderController = class SubcontractingOrderController { + setup() { + this.frm.custom_make_buttons = { + 'Subcontracting Receipt': 'Subcontracting Receipt', + 'Stock Entry': 'Material to Supplier', + }; + } + + refresh(doc) { + var me = this; + + if (doc.docstatus == 1) { + if (doc.status != 'Completed') { + if (flt(doc.per_received) < 100) { + cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create')); + if (me.has_unsupplied_items()) { + cur_frm.add_custom_button(__('Material to Supplier'), + () => { + me.make_stock_entry(); + }, __('Transfer')); + } + } + cur_frm.page.set_inner_btn_group_as_primary(__('Create')); + } + } + } + + items_add(doc, cdt, cdn) { + if (doc.set_warehouse) { + var row = frappe.get_doc(cdt, cdn); + row.warehouse = doc.set_warehouse; + } + } + + set_warehouse(doc) { + this.set_warehouse_in_children(doc.items, "warehouse", doc.set_warehouse); + } + + set_reserve_warehouse(doc) { + this.set_warehouse_in_children(doc.supplied_items, "reserve_warehouse", doc.set_reserve_warehouse); + } + + set_warehouse_in_children(child_table, warehouse_field, warehouse) { + let transaction_controller = new erpnext.TransactionController(); + transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); + } + + make_stock_entry() { + var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false); + var me = this; + + if (items.length >= 1) { + me.raw_material_data = []; + me.show_dialog = 1; + let title = __('Transfer Material to Supplier'); + let fields = [ + { fieldtype: 'Section Break', label: __('Raw Materials') }, + { + fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), + fields: [ + { + fieldtype: 'Data', + fieldname: 'item_code', + label: __('Item'), + read_only: 1, + in_list_view: 1 + }, + { + fieldtype: 'Data', + fieldname: 'rm_item_code', + label: __('Raw Material'), + read_only: 1, + in_list_view: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'qty', + label: __('Quantity'), + in_list_view: 1 + }, + { + fieldtype: 'Data', + read_only: 1, + fieldname: 'warehouse', + label: __('Reserve Warehouse'), + in_list_view: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'rate', + label: __('Rate'), + hidden: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'amount', + label: __('Amount'), + hidden: 1 + }, + { + fieldtype: 'Link', + read_only: 1, + fieldname: 'uom', + label: __('UOM'), + hidden: 1 + } + ], + data: me.raw_material_data, + get_data: () => me.raw_material_data + } + ]; + + me.dialog = new frappe.ui.Dialog({ + title: title, fields: fields + }); + + if (me.frm.doc['supplied_items']) { + me.frm.doc['supplied_items'].forEach((item) => { + if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { + me.raw_material_data.push({ + 'name': item.name, + 'item_code': item.main_item_code, + 'rm_item_code': item.rm_item_code, + 'item_name': item.rm_item_code, + 'qty': item.required_qty - item.supplied_qty, + 'warehouse': item.reserve_warehouse, + 'rate': item.rate, + 'amount': item.amount, + 'stock_uom': item.stock_uom + }); + me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); + } + }); + } + + me.dialog.get_field('sub_con_rm_items').check_all_rows(); + + me.dialog.show(); + this.dialog.set_primary_action(__('Transfer'), () => { + me.values = me.dialog.get_values(); + if (me.values) { + me.values.sub_con_rm_items.map((row, i) => { + if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { + let row_id = i + 1; + frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id])); + } + }); + me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()); + me.dialog.hide(); + } + }); + } + + me.dialog.get_close_btn().on('click', () => { + me.dialog.hide(); + }); + } + + has_unsupplied_items() { + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + } + + make_subcontracting_receipt() { + frappe.model.open_mapped_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', + frm: cur_frm, + freeze_message: __('Creating Subcontracting Receipt ...') + }); + } + + make_rm_stock_entry(rm_items) { + frappe.call({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_rm_stock_entry', + args: { + subcontracting_order: cur_frm.doc.name, + rm_items: rm_items + }, + callback: (r) => { + var doclist = frappe.model.sync(r.message); + frappe.set_route('Form', doclist[0].doctype, doclist[0].name); + } + }); + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.buying.SubcontractingOrderController({ frm: cur_frm })); \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json new file mode 100644 index 00000000000..c6e76c76d76 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -0,0 +1,485 @@ +{ + "actions": [], + "allow_auto_repeat": 1, + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2022-04-01 22:39:17.662819", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "purchase_order", + "supplier", + "supplier_name", + "supplier_warehouse", + "column_break_7", + "company", + "transaction_date", + "schedule_date", + "amended_from", + "address_and_contact_section", + "supplier_address", + "address_display", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "column_break_19", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "section_break_24", + "column_break_25", + "set_warehouse", + "items", + "section_break_32", + "total_qty", + "column_break_29", + "total", + "service_items_section", + "service_items", + "raw_materials_supplied_section", + "set_reserve_warehouse", + "supplied_items", + "additional_costs_section", + "distribute_additional_costs_based_on", + "additional_costs", + "total_additional_costs", + "order_status_section", + "status", + "column_break_39", + "per_received", + "printing_settings_section", + "select_print_heading", + "column_break_43", + "letter_head" + ], + "fields": [ + { + "allow_on_submit": 1, + "default": "{supplier_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "SC-ORD-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Subcontracting Purchase Order", + "options": "Purchase Order", + "reqd": 1 + }, + { + "bold": 1, + "fieldname": "supplier", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Supplier", + "options": "Supplier", + "print_hide": 1, + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fetch_from": "supplier.supplier_name", + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Supplier Name", + "read_only": 1, + "reqd": 1 + }, + { + "depends_on": "supplier", + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "label": "Supplier Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "default": "Today", + "fetch_from": "purchase_order.transaction_date", + "fetch_if_empty": 1, + "fieldname": "transaction_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1, + "search_index": 1 + }, + { + "allow_on_submit": 1, + "fetch_from": "purchase_order.schedule_date", + "fetch_if_empty": 1, + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Subcontracting Order", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "address_and_contact_section", + "fieldtype": "Section Break", + "label": "Address and Contact" + }, + { + "fetch_from": "supplier.supplier_primary_address", + "fetch_if_empty": 1, + "fieldname": "supplier_address", + "fieldtype": "Link", + "label": "Supplier Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Small Text", + "label": "Supplier Address Details", + "read_only": 1 + }, + { + "fetch_from": "supplier.supplier_primary_contact", + "fetch_if_empty": 1, + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Supplier Contact", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "contact_display", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Contact Name", + "read_only": 1 + }, + { + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "label": "Contact Mobile No", + "read_only": 1 + }, + { + "fieldname": "contact_email", + "fieldtype": "Small Text", + "label": "Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Company Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Small Text", + "label": "Shipping Address Details", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Company Billing Address", + "options": "Address" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address Details", + "read_only": 1 + }, + { + "fieldname": "section_break_24", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "depends_on": "purchase_order", + "description": "Sets 'Warehouse' in each row of the Items table.", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "label": "Set Target Warehouse", + "options": "Warehouse", + "print_hide": 1 + }, + { + "allow_bulk_edit": 1, + "depends_on": "purchase_order", + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Subcontracting Order Item", + "reqd": 1 + }, + { + "fieldname": "section_break_32", + "fieldtype": "Section Break" + }, + { + "depends_on": "purchase_order", + "fieldname": "total_qty", + "fieldtype": "Float", + "label": "Total Quantity", + "read_only": 1 + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "depends_on": "purchase_order", + "fieldname": "total", + "fieldtype": "Currency", + "label": "Total", + "options": "currency", + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "purchase_order", + "fieldname": "service_items_section", + "fieldtype": "Section Break", + "label": "Service Items" + }, + { + "fieldname": "service_items", + "fieldtype": "Table", + "label": "Service Items", + "options": "Subcontracting Order Service Item", + "read_only": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "supplied_items", + "depends_on": "supplied_items", + "fieldname": "raw_materials_supplied_section", + "fieldtype": "Section Break", + "label": "Raw Materials Supplied" + }, + { + "depends_on": "supplied_items", + "description": "Sets 'Reserve Warehouse' in each row of the Supplied Items table.", + "fieldname": "set_reserve_warehouse", + "fieldtype": "Link", + "label": "Set Reserve Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "supplied_items", + "fieldtype": "Table", + "label": "Supplied Items", + "no_copy": 1, + "options": "Subcontracting Order Supplied Item", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "total_additional_costs", + "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", + "fieldname": "additional_costs_section", + "fieldtype": "Section Break", + "label": "Additional Costs" + }, + { + "fieldname": "additional_costs", + "fieldtype": "Table", + "label": "Additional Costs", + "options": "Landed Cost Taxes and Charges" + }, + { + "fieldname": "total_additional_costs", + "fieldtype": "Currency", + "label": "Total Additional Costs", + "print_hide_if_no_value": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "order_status_section", + "fieldtype": "Section Break", + "label": "Order Status" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled", + "print_hide": 1, + "read_only": 1, + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "column_break_39", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "per_received", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "% Received", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "printing_settings_section", + "fieldtype": "Section Break", + "label": "Printing Settings", + "print_hide": 1, + "print_width": "50%", + "width": "50%" + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "column_break_43", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "default": "Qty", + "fieldname": "distribute_additional_costs_based_on", + "fieldtype": "Select", + "label": "Distribute Additional Costs Based On ", + "options": "Qty\nAmount" + } + ], + "icon": "fa fa-file-text", + "is_submittable": 1, + "links": [], + "modified": "2022-04-11 21:02:44.097841", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "read": 1, + "report": 1, + "role": "Stock User" + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "role": "Purchase Manager", + "write": 1 + } + ], + "search_fields": "status, transaction_date, supplier", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "timeline_field": "supplier", + "title_field": "supplier_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py new file mode 100644 index 00000000000..d12c9e825c4 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -0,0 +1,372 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe import _ +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt + +from erpnext.controllers.subcontracting_controller import SubcontractingController +from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.utils import get_bin + + +class SubcontractingOrder(SubcontractingController): + def before_validate(self): + super(SubcontractingOrder, self).before_validate() + + def validate(self): + super(SubcontractingOrder, self).validate() + self.validate_purchase_order_for_subcontracting() + self.validate_items() + self.validate_service_items() + self.validate_supplied_items() + self.set_missing_values() + self.reset_default_field_value("set_warehouse", "items", "warehouse") + + def on_submit(self): + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() + self.update_status() + + def on_cancel(self): + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() + self.update_status() + + def validate_purchase_order_for_subcontracting(self): + if self.purchase_order: + po = frappe.get_doc("Purchase Order", self.purchase_order) + if not po.is_subcontracted: + frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting.")) + + if po.docstatus != 1: + msg = f"Please submit Purchase Order {po.name} before proceeding." + frappe.throw(_(msg)) + + if po.per_received == 100: + msg = f"Cannot create more Subcontracting Orders against the Purchase Order {po.name}." + frappe.throw(_(msg)) + else: + self.service_items = self.items = self.supplied_items = None + frappe.throw(_("Please select a Subcontracting Purchase Order.")) + + def validate_service_items(self): + for item in self.service_items: + if frappe.get_value("Item", item.item_code, "is_stock_item"): + msg = f"Service Item {item.item_name} must be a non-stock item." + frappe.throw(_(msg)) + + def validate_supplied_items(self): + if self.supplier_warehouse: + for item in self.supplied_items: + if self.supplier_warehouse == item.reserve_warehouse: + msg = f"Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {item.main_item_code}." + frappe.throw(_(msg)) + + def set_missing_values(self): + self.set_missing_values_in_additional_costs() + self.set_missing_values_in_service_items() + self.set_missing_values_in_supplied_items() + self.set_missing_values_in_items() + + def set_missing_values_in_additional_costs(self): + if self.get("additional_costs"): + self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) + + if self.total_additional_costs: + if self.distribute_additional_costs_based_on == "Amount": + total_amt = sum(flt(item.amount) for item in self.get("items")) + for item in self.items: + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty + else: + total_qty = sum(flt(item.qty) for item in self.get("items")) + additional_cost_per_qty = self.total_additional_costs / total_qty + for item in self.items: + item.additional_cost_per_qty = additional_cost_per_qty + else: + self.total_additional_costs = 0 + + def set_missing_values_in_service_items(self): + for idx, item in enumerate(self.get("service_items")): + self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty + + def set_missing_values_in_supplied_items(self): + for item in self.get("items"): + bom = frappe.get_doc("BOM", item.bom) + rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) + item.rm_cost_per_qty = rm_cost / flt(bom.quantity) + + def set_missing_values_in_items(self): + total_qty = total = 0 + for item in self.items: + item.rate = ( + item.rm_cost_per_qty + item.service_cost_per_qty + (item.additional_cost_per_qty or 0) + ) + item.amount = item.qty * item.rate + total_qty += flt(item.qty) + total += flt(item.amount) + else: + self.total_qty = total_qty + self.total = total + + def update_ordered_qty_for_subcontracting(self, sco_item_rows=None): + item_wh_list = [] + for item in self.get("items"): + if ( + (not sco_item_rows or item.name in sco_item_rows) + and [item.item_code, item.warehouse] not in item_wh_list + and frappe.get_cached_value("Item", item.item_code, "is_stock_item") + and item.warehouse + ): + item_wh_list.append([item.item_code, item.warehouse]) + for item_code, warehouse in item_wh_list: + update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) + + def update_reserved_qty_for_subcontracting(self): + for item in self.supplied_items: + if item.rm_item_code: + stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() + + def populate_items_table(self): + items = [] + + for si in self.service_items: + if si.fg_item: + item = frappe.get_doc("Item", si.fg_item) + bom = frappe.db.get_value("BOM", {"item": item.item_code, "is_active": 1, "is_default": 1}) + + items.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "schedule_date": self.schedule_date, + "description": item.description, + "qty": si.fg_item_qty, + "stock_uom": item.stock_uom, + "bom": bom, + }, + ) + else: + frappe.throw( + _("Please select Finished Good Item for Service Item {0}").format( + si.item_name or si.item_code + ) + ) + else: + for item in items: + self.append("items", item) + else: + self.set_missing_values() + + def update_status(self, status=None, update_modified=False): + if self.docstatus >= 1 and not status: + if self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif self.per_received >= 100: + status = "Completed" + elif self.per_received > 0 and self.per_received < 100: + status = "Partially Received" + else: + total_required_qty = total_supplied_qty = 0 + for item in self.supplied_items: + total_required_qty += item.required_qty + total_supplied_qty += item.supplied_qty or 0 + if total_supplied_qty: + status = "Partial Material Transferred" + if total_supplied_qty >= total_required_qty: + status = "Material Transferred" + elif self.docstatus == 2: + status = "Cancelled" + + if status: + frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified) + + +@frappe.whitelist() +def make_subcontracting_receipt(source_name, target_doc=None): + return get_mapped_subcontracting_receipt(source_name, target_doc) + + +def get_mapped_subcontracting_receipt(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.received_qty) + target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) + + target_doc = get_mapped_doc( + "Subcontracting Order", + source_name, + { + "Subcontracting Order": { + "doctype": "Subcontracting Receipt", + "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "validation": { + "docstatus": ["=", 1], + }, + }, + "Subcontracting Order Item": { + "doctype": "Subcontracting Receipt Item", + "field_map": { + "name": "subcontracting_order_item", + "parent": "subcontracting_order", + "bom": "bom", + }, + "postprocess": update_item, + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + }, + }, + target_doc, + ) + + return target_doc + + +def get_item_details(items): + item = frappe.qb.DocType("Item") + item_list = ( + frappe.qb.from_(item) + .select(item.item_code, item.description, item.allow_alternative_item) + .where(item.name.isin(items)) + .run(as_dict=True) + ) + + item_details = {} + for item in item_list: + item_details[item.item_code] = item + + return item_details + + +@frappe.whitelist() +def make_rm_stock_entry(subcontracting_order, rm_items): + rm_items_list = rm_items + + if isinstance(rm_items, str): + rm_items_list = json.loads(rm_items) + elif not rm_items: + frappe.throw(_("No Items available for transfer")) + + if rm_items_list: + fg_items = list(set(item["item_code"] for item in rm_items_list)) + else: + frappe.throw(_("No Items selected for transfer")) + + if subcontracting_order: + subcontracting_order = frappe.get_doc("Subcontracting Order", subcontracting_order) + + if fg_items: + items = tuple(set(item["rm_item_code"] for item in rm_items_list)) + item_wh = get_item_details(items) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = "Send to Subcontractor" + stock_entry.subcontracting_order = subcontracting_order.name + stock_entry.supplier = subcontracting_order.supplier + stock_entry.supplier_name = subcontracting_order.supplier_name + stock_entry.supplier_address = subcontracting_order.supplier_address + stock_entry.address_display = subcontracting_order.address_display + stock_entry.company = subcontracting_order.company + stock_entry.to_warehouse = subcontracting_order.supplier_warehouse + stock_entry.set_stock_entry_type() + + for item_code in fg_items: + for rm_item_data in rm_items_list: + if rm_item_data["item_code"] == item_code: + rm_item_code = rm_item_data["rm_item_code"] + items_dict = { + rm_item_code: { + "sco_rm_detail": rm_item_data.get("name"), + "item_name": rm_item_data["item_name"], + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item_data["qty"], + "from_warehouse": rm_item_data["warehouse"], + "stock_uom": rm_item_data["stock_uom"], + "serial_no": rm_item_data.get("serial_no"), + "batch_no": rm_item_data.get("batch_no"), + "main_item_code": rm_item_data["item_code"], + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + } + } + stock_entry.add_to_stock_entry_detail(items_dict) + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer")) + return subcontracting_order.name + + +def add_items_in_ste(ste_doc, row, qty, sco_rm_details, batch_no=None): + item = ste_doc.append("items", row.item_details) + + sco_rm_detail = list(set(row.sco_rm_details).intersection(sco_rm_details)) + item.update( + { + "qty": qty, + "batch_no": batch_no, + "basic_rate": row.item_details["rate"], + "sco_rm_detail": sco_rm_detail[0] if sco_rm_detail else "", + "s_warehouse": row.item_details["t_warehouse"], + "t_warehouse": row.item_details["s_warehouse"], + "item_code": row.item_details["rm_item_code"], + "subcontracted_item": row.item_details["main_item_code"], + "serial_no": "\n".join(row.serial_no) if row.serial_no else "", + } + ) + + +def make_return_stock_entry_for_subcontract(available_materials, sco_doc, sco_rm_details): + ste_doc = frappe.new_doc("Stock Entry") + ste_doc.purpose = "Material Transfer" + + ste_doc.subcontracting_order = sco_doc.name + ste_doc.company = sco_doc.company + ste_doc.is_return = 1 + + for key, value in available_materials.items(): + if not value.qty: + continue + + if value.batch_no: + for batch_no, qty in value.batch_no.items(): + if qty > 0: + add_items_in_ste(ste_doc, value, value.qty, sco_rm_details, batch_no) + else: + add_items_in_ste(ste_doc, value, value.qty, sco_rm_details) + + ste_doc.set_stock_entry_type() + ste_doc.calculate_rate_and_amount() + + return ste_doc + + +@frappe.whitelist() +def get_materials_from_supplier(subcontracting_order, sco_rm_details): + if isinstance(sco_rm_details, str): + sco_rm_details = json.loads(sco_rm_details) + + doc = frappe.get_cached_doc("Subcontracting Order", subcontracting_order) + doc.initialized_fields() + doc.subcontracting_orders = [doc.name] + doc.get_available_materials() + + if not doc.available_materials: + frappe.throw( + _("Materials are already received against the Subcontracting Order {0}").format( + subcontracting_order + ) + ) + + return make_return_stock_entry_for_subcontract(doc.available_materials, doc, sco_rm_details) + + +@frappe.whitelist() +def update_subcontracting_order_status(sco): + if isinstance(sco, str): + sco = frappe.get_doc("Subcontracting Order", sco) + + sco.update_status() diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py new file mode 100644 index 00000000000..f17d8cd961c --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py @@ -0,0 +1,8 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "subcontracting_order", + "transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}], + } diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js new file mode 100644 index 00000000000..a2b724546b1 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -0,0 +1,16 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.listview_settings['Subcontracting Order'] = { + get_indicator: function (doc) { + const status_colors = { + "Draft": "grey", + "Open": "orange", + "Partially Received": "yellow", + "Completed": "green", + "Partial Material Transferred": "purple", + "Material Transferred": "blue", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py new file mode 100644 index 00000000000..f58c8307e49 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + +class TestSubcontractingOrder(FrappeTestCase): + pass \ No newline at end of file From 409df263e89da4e11e90277def8ae18d17178910 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 19 Apr 2022 14:57:31 +0530 Subject: [PATCH 09/41] refactor!: change "is_subcontracted" field type from "Select" to "Check" --- .../purchase_invoice/purchase_invoice.js | 6 ++--- .../purchase_invoice/purchase_invoice.json | 9 +++---- .../purchase_invoice/test_purchase_invoice.py | 6 ++--- .../purchase_invoice_item.json | 2 +- .../report/tax_detail/test_tax_detail.json | 2 +- .../doctype/purchase_order/purchase_order.js | 4 +-- .../purchase_order/purchase_order.json | 13 ++++++---- .../doctype/purchase_order/purchase_order.py | 6 ++--- .../purchase_order/test_purchase_order.py | 20 +++++++------- .../doctype/purchase_order/test_records.json | 4 +-- .../purchase_order_item.json | 10 ++++--- .../supplier_quotation.json | 5 ++-- .../supplier_quotation/test_records.json | 2 +- .../subcontract_order_summary.js | 2 +- .../subcontract_order_summary.py | 2 +- .../subcontracted_item_to_be_received.py | 2 +- .../test_subcontracted_item_to_be_received.py | 2 +- ...tracted_raw_materials_to_be_transferred.py | 2 +- ...tracted_raw_materials_to_be_transferred.py | 2 +- erpnext/controllers/accounts_controller.py | 2 +- erpnext/controllers/buying_controller.py | 11 +++----- erpnext/controllers/subcontracting.py | 2 +- erpnext/manufacturing/doctype/bom/test_bom.py | 2 +- .../production_plan/production_plan.py | 2 +- erpnext/patches.txt | 1 + .../change_is_subcontracted_fieldtype.py | 26 +++++++++++++++++++ erpnext/public/js/controllers/buying.js | 2 +- erpnext/public/js/controllers/transaction.js | 2 +- erpnext/public/js/utils.js | 4 +-- .../item_alternative/test_item_alternative.py | 2 +- .../purchase_receipt/purchase_receipt.js | 6 ++--- .../purchase_receipt/purchase_receipt.json | 9 +++---- .../purchase_receipt/test_purchase_receipt.py | 10 +++---- .../purchase_receipt/test_records.json | 2 +- .../purchase_receipt_item.json | 2 +- .../stock/doctype/stock_entry/stock_entry.js | 2 +- .../test_stock_ledger_entry.py | 2 +- erpnext/stock/get_item_details.py | 8 +++--- erpnext/stock/stock_ledger.py | 2 +- .../subcontracting_order.js | 2 +- erpnext/tests/test_subcontracting.py | 26 +++++++++---------- 41 files changed, 127 insertions(+), 101 deletions(-) create mode 100644 erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5f6e61090bd..ee29d2a7448 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -141,7 +141,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. }) }, __("Get Items From")); } - this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes"); + this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) { frappe.model.with_doc("Supplier", me.frm.doc.supplier, function() { @@ -571,10 +571,10 @@ frappe.ui.form.on("Purchase Invoice", { }, is_subcontracted: function(frm) { - if (frm.doc.is_subcontracted === "Yes") { + if (frm.doc.is_subcontracted) { erpnext.buying.get_default_bom(frm); } - frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes"); + frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); }, update_stock: function(frm) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index bd0116443ff..9f87c5ab54e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -543,11 +543,10 @@ "fieldtype": "Column Break" }, { - "default": "No", + "default": "0", "fieldname": "is_subcontracted", - "fieldtype": "Select", - "label": "Raw Materials Supplied", - "options": "No\nYes", + "fieldtype": "Check", + "label": "Is Subcontracted", "print_hide": 1 }, { @@ -1366,7 +1365,7 @@ "width": "50px" }, { - "depends_on": "eval:doc.update_stock && doc.is_subcontracted==\"Yes\"", + "depends_on": "eval:doc.update_stock && doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 843f66d546b..73390dd6f45 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -901,7 +901,7 @@ class TestPurchaseInvoice(unittest.TestCase): ) pi = make_purchase_invoice( - item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted="Yes" + item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1 ) self.assertEqual(len(pi.get("supplied_items")), 2) @@ -1611,7 +1611,7 @@ def make_purchase_invoice(**args): pi.conversion_rate = args.conversion_rate or 1 pi.is_return = args.is_return pi.return_against = args.return_against - pi.is_subcontracted = args.is_subcontracted or "No" + pi.is_subcontracted = args.is_subcontracted or 0 pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pi.cost_center = args.parent_cost_center @@ -1674,7 +1674,7 @@ def make_purchase_invoice_against_cost_center(**args): pi.is_return = args.is_return pi.is_return = args.is_return pi.credit_to = args.return_against or "Creditors - _TC" - pi.is_subcontracted = args.is_subcontracted or "No" + pi.is_subcontracted = args.is_subcontracted or 0 if args.supplier_warehouse: pi.supplier_warehouse = "_Test Warehouse 1 - _TC" diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index f9b2efd053b..6651195e5f2 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -623,7 +623,7 @@ }, { "default": "0", - "depends_on": "eval:parent.is_subcontracted == 'Yes'", + "depends_on": "eval:parent.is_subcontracted", "fieldname": "include_exploded_items", "fieldtype": "Check", "label": "Include Exploded Items", diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.json b/erpnext/accounts/report/tax_detail/test_tax_detail.json index 3a4b1754554..e4903167cba 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.json +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.json @@ -302,7 +302,7 @@ "is_opening": "No", "is_paid": 0, "is_return": 0, - "is_subcontracted": "No", + "is_subcontracted": 0, "items": [ { "allow_zero_valuation_rate": 0, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 1f6de1aa7b1..2cad1fb0817 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -185,7 +185,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if (doc.status != "On Hold") { if(flt(doc.per_received) < 100 && allow_receipt) { cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); - if(doc.is_subcontracted==="Yes" && me.has_unsupplied_items()) { + if(doc.is_subcontracted && me.has_unsupplied_items()) { cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer")); } @@ -653,7 +653,7 @@ function set_schedule_date(frm) { frappe.provide("erpnext.buying"); frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) { - if (frm.doc.is_subcontracted === "Yes") { + if (frm.doc.is_subcontracted) { erpnext.buying.get_default_bom(frm); } }); diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 307b57607e0..5e4bc60e8fb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -448,15 +448,18 @@ "print_hide": 1 }, { - "default": "No", + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break" + }, + { + "default": "0", "fieldname": "is_subcontracted", - "fieldtype": "Select", - "label": "Supply Raw Materials", - "options": "No\nYes", + "fieldtype": "Check", + "label": "Is Subcontracted", "print_hide": 1 }, { - "depends_on": "eval:doc.is_subcontracted==\"Yes\"", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index e8b8b87b98d..1945079171f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -307,7 +307,7 @@ class PurchaseOrder(BuyingController): self.set_status(update=True, status=status) self.update_requested_qty() self.update_ordered_qty() - if self.is_subcontracted == "Yes": + if self.is_subcontracted: self.update_reserved_qty_for_subcontract() self.notify_update() @@ -324,7 +324,7 @@ class PurchaseOrder(BuyingController): self.update_ordered_qty() self.validate_budget() - if self.is_subcontracted == "Yes": + if self.is_subcontracted: self.update_reserved_qty_for_subcontract() frappe.get_doc("Authorization Control").validate_approving_authority( @@ -344,7 +344,7 @@ class PurchaseOrder(BuyingController): if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() - if self.is_subcontracted == "Yes": + if self.is_subcontracted: self.update_reserved_qty_for_subcontract() self.check_on_hold_or_closed_status() diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index e4fb970c3f7..1a7f2dd5d97 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -390,7 +390,7 @@ class TestPurchaseOrder(FrappeTestCase): frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() def test_update_child_uom_conv_factor_change(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) trans_item = json.dumps( @@ -573,7 +573,7 @@ class TestPurchaseOrder(FrappeTestCase): automatically_fetch_payment_terms(enable=0) def test_subcontracting(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) self.assertEqual(len(po.get("supplied_items")), 2) def test_warehouse_company_validation(self): @@ -617,7 +617,7 @@ class TestPurchaseOrder(FrappeTestCase): "doctype": "Purchase Order", "company": "_Test Company", "supplier": "_Test Supplier", - "is_subcontracted": "No", + "is_subcontracted": 0, "schedule_date": add_days(nowdate(), 1), "currency": frappe.get_cached_value("Company", "_Test Company", "default_currency"), "conversion_factor": 1, @@ -764,7 +764,7 @@ class TestPurchaseOrder(FrappeTestCase): ) # Submit PO - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) bin2 = frappe.db.get_value( "Bin", @@ -919,7 +919,7 @@ class TestPurchaseOrder(FrappeTestCase): po = create_purchase_order( item_code=item_code, qty=1, - is_subcontracted="Yes", + is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1, ) @@ -936,7 +936,7 @@ class TestPurchaseOrder(FrappeTestCase): po1 = create_purchase_order( item_code=item_code, qty=1, - is_subcontracted="Yes", + is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=0, ) @@ -957,7 +957,7 @@ class TestPurchaseOrder(FrappeTestCase): po = create_purchase_order( item_code=item_code, qty=order_qty, - is_subcontracted="Yes", + is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC", ) @@ -1050,7 +1050,7 @@ class TestPurchaseOrder(FrappeTestCase): po = create_purchase_order( item_code=item_code, qty=order_qty, - is_subcontracted="Yes", + is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True, ) @@ -1283,7 +1283,7 @@ def create_purchase_order(**args): po.schedule_date = add_days(nowdate(), 1) po.company = args.company or "_Test Company" po.supplier = args.supplier or "_Test Supplier" - po.is_subcontracted = args.is_subcontracted or "No" + po.is_subcontracted = args.is_subcontracted or 0 po.currency = args.currency or frappe.get_cached_value("Company", po.company, "default_currency") po.conversion_factor = args.conversion_factor or 1 po.supplier_warehouse = args.supplier_warehouse or None @@ -1309,7 +1309,7 @@ def create_purchase_order(**args): if not args.do_not_save: po.insert() if not args.do_not_submit: - if po.is_subcontracted == "Yes": + if po.is_subcontracted: supp_items = po.get("supplied_items") for d in supp_items: if not d.reserve_warehouse: diff --git a/erpnext/buying/doctype/purchase_order/test_records.json b/erpnext/buying/doctype/purchase_order/test_records.json index 74b8f1b429b..896050ce43a 100644 --- a/erpnext/buying/doctype/purchase_order/test_records.json +++ b/erpnext/buying/doctype/purchase_order/test_records.json @@ -8,7 +8,7 @@ "doctype": "Purchase Order", "base_grand_total": 5000.0, "grand_total": 5000.0, - "is_subcontracted": "Yes", + "is_subcontracted": 1, "naming_series": "_T-Purchase Order-", "base_net_total": 5000.0, "items": [ @@ -42,7 +42,7 @@ "doctype": "Purchase Order", "base_grand_total": 5000.0, "grand_total": 5000.0, - "is_subcontracted": "No", + "is_subcontracted": 0, "naming_series": "_T-Purchase Order-", "base_net_total": 5000.0, "items": [ 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 b4cdb182119..7f797cfd2fe 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -574,6 +574,7 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_subcontracted", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", @@ -583,6 +584,7 @@ }, { "default": "0", + "depends_on": "eval:parent.is_subcontracted", "fieldname": "include_exploded_items", "fieldtype": "Check", "hidden": 1, @@ -849,20 +851,20 @@ "print_hide": 1 }, { - "depends_on": "eval:parent.is_subcontracted == 'Yes'", + "depends_on": "eval:parent.is_subcontracted", "fieldname": "fg_item", "fieldtype": "Link", "label": "Finished Good Item", - "mandatory_depends_on": "eval:parent.is_subcontracted == 'Yes'", + "mandatory_depends_on": "eval:parent.is_subcontracted", "options": "Item" }, { "default": "1", - "depends_on": "eval:parent.is_subcontracted == 'Yes'", + "depends_on": "eval:parent.is_subcontracted", "fieldname": "fg_item_qty", "fieldtype": "Float", "label": "Finished Good Item Qty", - "mandatory_depends_on": "eval:parent.is_subcontracted == 'Yes'" + "mandatory_depends_on": "eval:parent.is_subcontracted" } ], "idx": 1, diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 567e41fb61f..8d1939a101b 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -773,11 +773,10 @@ "fieldtype": "Column Break" }, { - "default": "No", + "default": "0", "fieldname": "is_subcontracted", - "fieldtype": "Select", + "fieldtype": "Check", "label": "Is Subcontracted", - "options": "\nYes\nNo", "print_hide": 1 }, { diff --git a/erpnext/buying/doctype/supplier_quotation/test_records.json b/erpnext/buying/doctype/supplier_quotation/test_records.json index 0f835d2a40a..8acac3210d5 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_records.json +++ b/erpnext/buying/doctype/supplier_quotation/test_records.json @@ -7,7 +7,7 @@ "doctype": "Supplier Quotation", "base_grand_total": 5000.0, "grand_total": 5000.0, - "is_subcontracted": "No", + "is_subcontracted": 0, "naming_series": "_T-Supplier Quotation-", "base_net_total": 5000.0, "items": [ diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js index 5ba52f1b21e..6889322fb93 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -35,7 +35,7 @@ frappe.query_reports["Subcontract Order Summary"] = { return { filters: { docstatus: 1, - is_subcontracted: 'Yes', + is_subcontracted: 1, company: frappe.query_report.get_filter_value('company') } } diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 1b2705a7be3..3d666375764 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -45,7 +45,7 @@ def get_subcontracted_orders(report_filters): def get_filters(report_filters): filters = [ ["Purchase Order", "docstatus", "=", 1], - ["Purchase Order", "is_subcontracted", "=", "Yes"], + ["Purchase Order", "is_subcontracted", "=", 1], [ "Purchase Order", "transaction_date", diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py index 004657b6e86..2e90de66efe 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py @@ -78,7 +78,7 @@ def get_data(data, filters): def get_po(filters): record_filters = [ - ["is_subcontracted", "=", "Yes"], + ["is_subcontracted", "=", 1], ["supplier", "=", filters.supplier], ["transaction_date", "<=", filters.to_date], ["transaction_date", ">=", filters.from_date], diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index 26e4243eeee..57f8741b5bf 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -17,7 +17,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestSubcontractedItemToBeReceived(FrappeTestCase): def test_pending_and_received_qty(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") + po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) transfer_param = [] make_stock_entry( item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index 98b18da4acb..6b8a3b140a7 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -72,7 +72,7 @@ def get_po_items_to_supply(filters): ], filters=[ ["Purchase Order", "per_received", "<", "100"], - ["Purchase Order", "is_subcontracted", "=", "Yes"], + ["Purchase Order", "is_subcontracted", "=", 1], ["Purchase Order", "supplier", "=", filters.supplier], ["Purchase Order", "transaction_date", "<=", filters.to_date], ["Purchase Order", "transaction_date", ">=", filters.from_date], diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 401176d5cef..2791a26db78 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -19,7 +19,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestSubcontractedItemToBeTransferred(FrappeTestCase): def test_pending_and_transferred_qty(self): po = create_purchase_order( - item_code="_Test FG Item", is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) # Material Receipt of RMs diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3a20d3f232f..8a9318e184e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2586,7 +2586,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() - if parent.is_subcontracted == "Yes": + if parent.is_subcontracted: parent.update_reserved_qty_for_subcontract() parent.create_raw_materials_supplied("supplied_items") parent.save() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4823e8b05cb..6fdb002be03 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -167,7 +167,7 @@ class BuyingController(StockController, Subcontracting): _("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx) ) - if item.get("from_warehouse") and self.get("is_subcontracted") == "Yes": + if item.get("from_warehouse") and self.get("is_subcontracted"): frappe.throw( _( "Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor" @@ -339,10 +339,7 @@ class BuyingController(StockController, Subcontracting): return supplied_items_cost def validate_for_subcontracting(self): - if not self.is_subcontracted and self.sub_contracted_items: - frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No")) - - if self.is_subcontracted == "Yes": + if self.is_subcontracted: if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype)) @@ -363,14 +360,14 @@ class BuyingController(StockController, Subcontracting): item.bom = None def create_raw_materials_supplied(self, raw_material_table): - if self.is_subcontracted == "Yes": + if self.is_subcontracted: self.set_materials_for_subcontracted_items(raw_material_table) elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: for item in self.get("items"): item.rm_supp_cost = 0.0 - if self.is_subcontracted == "No" and self.get("supplied_items"): + if not self.is_subcontracted and self.get("supplied_items"): self.set("supplied_items", []) @property diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index 70830882efa..4bce06ff9b0 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -407,7 +407,7 @@ class Subcontracting: def set_consumed_qty_in_po(self): # Update consumed qty back in the purchase order - if self.is_subcontracted != "Yes": + if not self.is_subcontracted: return self.__get_purchase_orders() diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 524f45bfc23..62fc0724e03 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -251,7 +251,7 @@ class TestBOM(FrappeTestCase): self.assertEqual(bom.items[2].rate, 0) # test in Purchase Order sourced_by_supplier is not added to Supplied Item po = create_purchase_order( - item_code=item_code, qty=1, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1]) supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 89f9ca6d83a..60b32b84b05 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -501,7 +501,7 @@ class ProductionPlan(Document): po = frappe.new_doc("Purchase Order") po.supplier = supplier po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate() - po.is_subcontracted = "Yes" + po.is_subcontracted = 1 for row in po_list: po_data = { "item_code": row.production_item, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 028834a0ec4..a3bf78b532e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -4,6 +4,7 @@ erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v13_0.add_bin_unique_constraint erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_autoname_naming +erpnext.patches.v14_0.change_is_subcontracted_fieldtype execute:frappe.reload_doc("accounts", "doctype", "POS Payment Method") #2020-05-28 execute:frappe.reload_doc("HR", "doctype", "HR Settings") #2020-01-16 #2020-07-24 erpnext.patches.v4_2.update_requested_and_ordered_qty #2021-03-31 diff --git a/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py new file mode 100644 index 00000000000..ba919a756a8 --- /dev/null +++ b/erpnext/patches/v14_0/change_is_subcontracted_fieldtype.py @@ -0,0 +1,26 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + + +def execute(): + for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"]: + frappe.db.sql( + """ + UPDATE `tab{doctype}` + SET is_subcontracted = 0 + where is_subcontracted in ('', NULL, 'No')""".format( + doctype=doctype + ) + ) + frappe.db.sql( + """ + UPDATE `tab{doctype}` + SET is_subcontracted = 1 + where is_subcontracted = 'Yes'""".format( + doctype=doctype + ) + ) + + frappe.reload_doc(frappe.get_meta(doctype).module, "doctype", frappe.scrub(doctype)) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a925470d607..0920ca04ee6 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -81,7 +81,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac } this.frm.set_query("item_code", "items", function() { - if (me.frm.doc.is_subcontracted == "Yes") { + if (me.frm.doc.is_subcontracted) { return{ query: "erpnext.controllers.queries.item_query", filters:{ 'supplier': me.frm.doc.supplier, 'is_stock_item': 0 } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 23c2bd405c1..57cbe91fa09 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -239,7 +239,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => set_value('currency', currency), () => set_value('price_list_currency', currency), () => set_value('status', 'Draft'), - () => set_value('is_subcontracted', 'No'), + () => set_value('is_subcontracted', 0), () => { if(this.frm.doc.company && !this.frm.doc.amended_from) { this.frm.trigger("company"); diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 82604267045..eded16529cc 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -483,8 +483,8 @@ erpnext.utils.update_child_items = function(opts) { if (frm.doc.doctype == 'Sales Order') { filters = {"is_sales_item": 1}; } else if (frm.doc.doctype == 'Purchase Order') { - if (frm.doc.is_subcontracted == "Yes") { - filters = {"is_stock_item": 0}; + if (frm.doc.is_subcontracted) { + filters = {"is_sub_contracted_item": 1}; } else { filters = {"is_purchase_item": 1}; } diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index d829b2cbf39..32c58c5ae1d 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -41,7 +41,7 @@ class TestItemAlternative(FrappeTestCase): supplier_warehouse = "Test Supplier Warehouse - _TC" po = create_purchase_order( item="Test Finished Goods - A", - is_subcontracted="Yes", + is_subcontracted=1, qty=5, rate=3000, supplier_warehouse=supplier_warehouse, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 0182ed55a18..51ec598f726 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -200,7 +200,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status")) } - this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted==="Yes"); + this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); } make_purchase_invoice() { @@ -298,10 +298,10 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt frappe.provide("erpnext.buying"); frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) { - if (frm.doc.is_subcontracted === "Yes") { + if (frm.doc.is_subcontracted) { erpnext.buying.get_default_bom(frm); } - frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes"); + frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); }); frappe.ui.form.on('Purchase Receipt Item', { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 6d4b4a19bd2..6e5f6f5b529 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -437,17 +437,16 @@ "fieldtype": "Column Break" }, { - "default": "No", + "default": "0", "fieldname": "is_subcontracted", - "fieldtype": "Select", - "label": "Raw Materials Consumed", + "fieldtype": "Check", + "label": "Is Subcontracted", "oldfieldname": "is_subcontracted", "oldfieldtype": "Select", - "options": "No\nYes", "print_hide": 1 }, { - "depends_on": "eval:doc.is_subcontracted==\"Yes\"", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index a6f82b08dc0..bfbdd562921 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -327,7 +327,7 @@ class TestPurchaseReceipt(FrappeTestCase): target="_Test Warehouse 1 - _TC", basic_rate=100, ) - pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted="Yes") + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted=1) self.assertEqual(len(pr.get("supplied_items")), 2) rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) @@ -362,7 +362,7 @@ class TestPurchaseReceipt(FrappeTestCase): item_code="_Test FG Item", qty=10, rate=0, - is_subcontracted="Yes", + is_subcontracted=1, company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", supplier_warehouse="Work In Progress - TCP1", @@ -401,7 +401,7 @@ class TestPurchaseReceipt(FrappeTestCase): item_code=item_code, qty=1, include_exploded_items=0, - is_subcontracted="Yes", + is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC", ) @@ -1122,7 +1122,7 @@ class TestPurchaseReceipt(FrappeTestCase): po = create_purchase_order( item_code=item_code, qty=order_qty, - is_subcontracted="Yes", + is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC", ) @@ -1465,7 +1465,7 @@ def make_purchase_receipt(**args): pr.set_posting_time = 1 pr.company = args.company or "_Test Company" pr.supplier = args.supplier or "_Test Supplier" - pr.is_subcontracted = args.is_subcontracted or "No" + pr.is_subcontracted = args.is_subcontracted or 0 pr.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pr.currency = args.currency or "INR" pr.is_return = args.is_return diff --git a/erpnext/stock/doctype/purchase_receipt/test_records.json b/erpnext/stock/doctype/purchase_receipt/test_records.json index 724e3d729a2..990ad12b30e 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_records.json +++ b/erpnext/stock/doctype/purchase_receipt/test_records.json @@ -92,7 +92,7 @@ "currency": "INR", "doctype": "Purchase Receipt", "base_grand_total": 5000.0, - "is_subcontracted": "Yes", + "is_subcontracted": 1, "base_net_total": 5000.0, "items": [ { diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index e5994b2dd48..03a4201ce5c 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -648,7 +648,7 @@ }, { "default": "0", - "depends_on": "eval:parent.is_subcontracted == 'Yes'", + "depends_on": "eval:parent.is_subcontracted", "fieldname": "include_exploded_items", "fieldtype": "Check", "label": "Include Exploded Items", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 1aafcee5bf8..a94087821a5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -793,7 +793,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle return { "filters": { "docstatus": 1, - "is_subcontracted": "Yes", + "is_subcontracted": 1, "company": me.frm.doc.company } }; diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 42956a129be..6561362c3af 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -436,7 +436,7 @@ class TestStockLedgerEntry(FrappeTestCase): item_code=subcontracted_item, qty=10, rate=20, - is_subcontracted="Yes", + is_subcontracted=1, ) self.assertEqual(pr1.items[0].valuation_rate, 120) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 0d7d47262a5..dfd9f8ac635 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -50,7 +50,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru "transaction_date": None, "conversion_rate": 1.0, "buying_price_list": None, - "is_subcontracted": "Yes" / "No", + "is_subcontracted": 0/1, "ignore_pricing_rule": 0/1 "project": "" "set_warehouse": "" @@ -124,7 +124,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.transaction_date and item.lead_time_days: out.schedule_date = out.lead_time_date = add_days(args.transaction_date, item.lead_time_days) - if args.get("is_subcontracted") == "Yes": + if args.get("is_subcontracted"): out.bom = args.get("bom") or get_default_bom(args.item_code) get_gross_profit(out) @@ -237,7 +237,7 @@ 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_stock_item: + if args.get("is_subcontracted") and item.is_stock_item: throw(_("Item {0} must be a Non-Stock Item").format(item.name)) @@ -258,7 +258,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "transaction_date": None, "conversion_rate": 1.0, "buying_price_list": None, - "is_subcontracted": "Yes" / "No", + "is_subcontracted": 0/1, "ignore_pricing_rule": 0/1 "project": "", barcode: "", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 967b2b2294d..3e0ddab6d3b 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -715,7 +715,7 @@ class update_entries_after(object): ) # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice - if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == "Yes": + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc.update_valuation_rate(reset_outgoing_rate=False) for d in doc.items + doc.supplied_items: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 80fe94483b1..c9e4577cea3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -24,7 +24,7 @@ frappe.ui.form.on('Subcontracting Order', { return { filters: { docstatus: 1, - is_subcontracted: "Yes" + is_subcontracted: 1 } }; }); diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py index 07291e851b5..bf12181c527 100644 --- a/erpnext/tests/test_subcontracting.py +++ b/erpnext/tests/test_subcontracting.py @@ -50,7 +50,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -112,7 +112,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -175,7 +175,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -239,7 +239,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -298,7 +298,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -363,7 +363,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -421,7 +421,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -492,7 +492,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -529,7 +529,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -609,7 +609,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -675,7 +675,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -751,7 +751,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: @@ -834,7 +834,7 @@ class TestSubcontracting(unittest.TestCase): itemwise_details = make_stock_in_entry(rm_items=rm_items) po = create_purchase_order( - rm_items=items, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC" + rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" ) for d in rm_items: From 3daf62dce862c02c33c76a3d670e906ea3901a8b Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 18 Apr 2022 08:21:01 +0530 Subject: [PATCH 10/41] feat: New DocType "Subcontracting Receipt Item" --- .../subcontracting_receipt_item/__init__.py | 0 .../subcontracting_receipt_item.json | 475 ++++++++++++++++++ .../subcontracting_receipt_item.py | 9 + 3 files changed, 484 insertions(+) create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt_item/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json new file mode 100644 index 00000000000..e2785ce0cdd --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -0,0 +1,475 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-04-13 16:05:55.395695", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "section_break_4", + "description", + "brand", + "image_column", + "image", + "image_view", + "received_and_accepted", + "received_qty", + "qty", + "rejected_qty", + "returned_qty", + "col_break2", + "stock_uom", + "conversion_factor", + "tracking_section", + "col_break_tracking_section", + "rate_and_amount", + "rate", + "amount", + "column_break_19", + "rm_cost_per_qty", + "service_cost_per_qty", + "additional_cost_per_qty", + "rm_supp_cost", + "warehouse_and_reference", + "warehouse", + "rejected_warehouse", + "subcontracting_order", + "column_break_40", + "schedule_date", + "quality_inspection", + "subcontracting_order_item", + "subcontracting_receipt_item", + "section_break_45", + "bom", + "serial_no", + "col_break5", + "batch_no", + "rejected_serial_no", + "expense_account", + "manufacture_details", + "manufacturer", + "column_break_16", + "manufacturer_part_no", + "accounting_dimensions_section", + "project", + "dimension_col_break", + "cost_center", + "section_break_80", + "page_break" + ], + "fields": [ + { + "bold": 1, + "columns": 3, + "fieldname": "item_code", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "print_width": "100px", + "reqd": 1, + "search_index": 1, + "width": "100px" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image" + }, + { + "fieldname": "image_view", + "fieldtype": "Image", + "label": "Image View", + "options": "image", + "print_hide": 1 + }, + { + "fieldname": "received_and_accepted", + "fieldtype": "Section Break", + "label": "Received and Accepted" + }, + { + "bold": 1, + "default": "0", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Quantity", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "columns": 2, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Accepted Quantity", + "no_copy": 1, + "print_width": "100px", + "width": "100px" + }, + { + "columns": 1, + "fieldname": "rejected_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rejected Quantity", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "col_break2", + "fieldtype": "Column Break", + "print_hide": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_hide": 1, + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "rate_and_amount", + "fieldtype": "Section Break", + "label": "Rate and Amount" + }, + { + "bold": 1, + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "rm_cost_per_qty", + "fieldtype": "Currency", + "label": "Raw Material Cost Per Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "service_cost_per_qty", + "fieldtype": "Currency", + "label": "Service Cost Per Qty", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "additional_cost_per_qty", + "fieldtype": "Currency", + "label": "Additional Cost Per Qty", + "read_only": 1 + }, + { + "fieldname": "warehouse_and_reference", + "fieldtype": "Section Break", + "label": "Warehouse and Reference" + }, + { + "bold": 1, + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Accepted Warehouse", + "options": "Warehouse", + "print_hide": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "label": "Rejected Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "100px", + "width": "100px" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "quality_inspection", + "fieldtype": "Link", + "label": "Quality Inspection", + "no_copy": 1, + "options": "Quality Inspection", + "print_hide": 1 + }, + { + "fieldname": "column_break_40", + "fieldtype": "Column Break" + }, + { + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "no_copy": 1, + "options": "Subcontracting Order", + "print_width": "150px", + "read_only": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_45", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "serial_no", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Serial No", + "no_copy": 1 + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "batch_no", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Batch No", + "no_copy": 1, + "options": "Batch", + "print_hide": 1 + }, + { + "depends_on": "eval:!doc.is_fixed_asset", + "fieldname": "rejected_serial_no", + "fieldtype": "Small Text", + "label": "Rejected Serial No", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "subcontracting_order_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Subcontracting Order Item", + "no_copy": 1, + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "col_break5", + "fieldtype": "Column Break" + }, + { + "fieldname": "bom", + "fieldtype": "Link", + "label": "BOM", + "no_copy": 1, + "options": "BOM", + "print_hide": 1 + }, + { + "fetch_from": "item_code.brand", + "fieldname": "brand", + "fieldtype": "Link", + "hidden": 1, + "label": "Brand", + "options": "Brand", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "rm_supp_cost", + "fieldtype": "Currency", + "hidden": 1, + "label": "Raw Materials Supplied Cost", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "width": "150px" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Expense Account", + "options": "Account", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "manufacture_details", + "fieldtype": "Section Break", + "label": "Manufacture" + }, + { + "fieldname": "manufacturer", + "fieldtype": "Link", + "label": "Manufacturer", + "options": "Manufacturer" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "manufacturer_part_no", + "fieldtype": "Data", + "label": "Manufacturer Part Number" + }, + { + "fieldname": "subcontracting_receipt_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Subcontracting Receipt Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "image_column", + "fieldtype": "Column Break" + }, + { + "fieldname": "tracking_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break_tracking_section", + "fieldtype": "Column Break" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project", + "print_hide": 1 + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "default": ":Company", + "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center", + "print_hide": 1 + }, + { + "fieldname": "section_break_80", + "fieldtype": "Section Break" + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "label": "Page Break", + "print_hide": 1 + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2022-04-21 12:07:55.899701", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Receipt Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py new file mode 100644 index 00000000000..374f95baf39 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingReceiptItem(Document): + pass From 3b17584beea4efeec256b9af2c4e255058d38beb Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 21 Apr 2022 20:27:53 +0530 Subject: [PATCH 11/41] feat: New DocType "Subcontracting Receipt Supplied Item" --- .../__init__.py | 0 .../subcontracting_receipt_supplied_item.json | 198 ++++++++++++++++++ .../subcontracting_receipt_supplied_item.py | 9 + 3 files changed, 207 insertions(+) create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json new file mode 100644 index 00000000000..100a8060e8c --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json @@ -0,0 +1,198 @@ +{ + "actions": [], + "creation": "2022-04-18 10:45:16.538479", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "main_item_code", + "rm_item_code", + "item_name", + "bom_detail_no", + "col_break1", + "description", + "stock_uom", + "conversion_factor", + "reference_name", + "secbreak_1", + "rate", + "col_break2", + "amount", + "secbreak_2", + "required_qty", + "col_break3", + "consumed_qty", + "current_stock", + "secbreak_3", + "batch_no", + "col_break4", + "serial_no", + "subcontracting_order" + ], + "fields": [ + { + "fieldname": "main_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "rm_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Raw Material Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Description", + "print_width": "300px", + "read_only": 1, + "width": "300px" + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "no_copy": 1, + "options": "Batch" + }, + { + "fieldname": "serial_no", + "fieldtype": "Text", + "label": "Serial No", + "no_copy": 1 + }, + { + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Available Qty For Consumption", + "print_hide": 1, + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "consumed_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty to be Consumed", + "reqd": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock Uom", + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "current_stock", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Current Stock", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Reference Name", + "read_only": 1 + }, + { + "fieldname": "bom_detail_no", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "BOM Detail No", + "read_only": 1 + }, + { + "fieldname": "secbreak_1", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break2", + "fieldtype": "Column Break" + }, + { + "fieldname": "secbreak_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break3", + "fieldtype": "Column Break" + }, + { + "fieldname": "secbreak_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break4", + "fieldtype": "Column Break" + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "hidden": 1, + "label": "Subcontracting Order", + "no_copy": 1, + "options": "Subcontracting Order", + "print_hide": 1, + "read_only": 1 + } + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2022-04-18 10:45:16.538479", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Receipt Supplied Item", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "states": [] +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py new file mode 100644 index 00000000000..f4d2805d4b1 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingReceiptSuppliedItem(Document): + pass From 70a1f4062497818ef4ea1d6bc44e682af33f7038 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 21 Apr 2022 20:28:06 +0530 Subject: [PATCH 12/41] feat: New DocType "Subcontracting Receipt" --- .../controllers/sales_and_purchase_return.py | 9 +- erpnext/stock/doctype/serial_no/serial_no.py | 11 +- .../subcontracting_receipt/__init__.py | 0 .../subcontracting_receipt.js | 157 +++++ .../subcontracting_receipt.json | 645 ++++++++++++++++++ .../subcontracting_receipt.py | 197 ++++++ .../subcontracting_receipt_dashboard.py | 15 + .../subcontracting_receipt_list.js | 14 + .../test_subcontracting_receipt.py | 9 + 9 files changed, 1052 insertions(+), 5 deletions(-) create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js create mode 100644 erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index bdde3a1fd8c..9642c24a9e0 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -343,7 +343,7 @@ def make_return_doc(doctype, source_name, target_doc=None): # look for Print Heading "Debit Note" doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note")) - for tax in doc.get("taxes"): + for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": tax.tax_amount = -1 * tax.tax_amount @@ -382,8 +382,11 @@ def make_return_doc(doctype, source_name, target_doc=None): for d in doc.get("packed_items"): d.qty = d.qty * -1 - doc.discount_amount = -1 * source.discount_amount - doc.run_method("calculate_taxes_and_totals") + if doc.get("discount_amount"): + doc.discount_amount = -1 * source.discount_amount + + if doctype != "Subcontracting Receipt": + doc.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1 * source_doc.qty diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 316c897da02..c302454c664 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -687,7 +687,10 @@ def update_serial_nos_after_submit(controller, parentfield): update_rejected_serial_nos = ( True - if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) + if ( + controller.doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt") + and d.rejected_qty + ) else False ) accepted_serial_nos_updated = False @@ -700,7 +703,11 @@ def update_serial_nos_after_submit(controller, parentfield): qty = d.stock_qty else: warehouse = d.warehouse - qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty + qty = ( + d.qty + if controller.doctype in ["Stock Reconciliation", "Subcontracting Receipt"] + else d.stock_qty + ) for sle in stock_ledger_entries: if sle.voucher_detail_no == d.name: if ( diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js new file mode 100644 index 00000000000..b98f979c668 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -0,0 +1,157 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide('erpnext.buying'); + +frappe.ui.form.on('Subcontracting Receipt', { + setup: (frm) => { + frm.get_field('supplied_items').grid.cannot_add_rows = true; + frm.get_field('supplied_items').grid.only_sortable(); + + frm.set_query('set_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('rejected_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('supplier_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + + frm.set_query('rejected_warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + }, + + refresh: (frm) => { + if (frm.doc.docstatus > 0) { + frm.add_custom_button(__("Stock Ledger"), function () { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + company: frm.doc.company, + show_cancelled_entries: frm.doc.docstatus === 2 + }; + frappe.set_route("query-report", "Stock Ledger"); + }, __("View")); + + frm.add_custom_button(__('Accounting Ledger'), function () { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + company: frm.doc.company, + group_by: "Group by Voucher (Consolidated)", + show_cancelled_entries: frm.doc.docstatus === 2 + }; + frappe.set_route("query-report", "General Ledger"); + }, __("View")); + } + + if (!frm.doc.is_return && frm.doc.docstatus == 1) { + frm.add_custom_button('Subcontract Return', function () { + frappe.model.open_mapped_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return', + frm: frm + }); + }, __('Create')); + frm.page.set_inner_btn_group_as_primary(__('Create')); + } + + if (frm.doc.docstatus == 0) { + frm.add_custom_button(__('Subcontracting Order'), function () { + if (!frm.doc.supplier) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Supplier") + }); + } + + erpnext.utils.map_current_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', + source_doctype: "Subcontracting Order", + target: frm, + setters: { + supplier: frm.doc.supplier, + }, + get_query_filters: { + docstatus: 1, + per_received: ["<", 100], + company: frm.doc.company + } + }) + }, __("Get Items From")); + } + }, + + set_warehouse: (frm) => { + set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse); + }, + + rejected_warehouse: (frm) => { + set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse); + }, +}); + +frappe.ui.form.on('Subcontracting Receipt Item', { + item_code(frm) { + set_missing_values(frm); + }, + + qty(frm) { + set_missing_values(frm); + }, + + rate(frm) { + set_missing_values(frm); + }, +}); + +frappe.ui.form.on('Subcontracting Receipt Supplied Item', { + consumed_qty(frm) { + set_missing_values(frm); + }, +}); + +let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => { + let transaction_controller = new erpnext.TransactionController(); + transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); +} + +let set_missing_values = (frm) => { + frappe.call({ + doc: frm.doc, + method: 'set_missing_values', + callback: (r) => { + if (!r.exc) frm.refresh(); + }, + }); +}; \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json new file mode 100644 index 00000000000..e9638144a79 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -0,0 +1,645 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2022-04-18 11:20:44.226738", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "supplier", + "supplier_name", + "column_break1", + "company", + "posting_date", + "posting_time", + "is_return", + "return_against", + "section_addresses", + "supplier_address", + "contact_person", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "col_break_address", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "sec_warehouse", + "set_warehouse", + "rejected_warehouse", + "col_break_warehouse", + "supplier_warehouse", + "items_section", + "items", + "section_break0", + "total_qty", + "column_break_27", + "total", + "raw_material_details", + "get_current_stock", + "supplied_items", + "section_break_46", + "in_words", + "bill_no", + "bill_date", + "accounting_details_section", + "provisional_expense_account", + "more_info", + "status", + "column_break_39", + "per_returned", + "section_break_47", + "amended_from", + "range", + "column_break4", + "represents_company", + "subscription_detail", + "auto_repeat", + "printing_settings", + "letter_head", + "language", + "instructions", + "column_break_97", + "select_print_heading", + "other_details", + "remarks", + "transporter_info", + "transporter_name", + "column_break5", + "lr_no", + "lr_date" + ], + "fields": [ + { + "allow_on_submit": 1, + "default": "{supplier_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "MAT-SCR-.YYYY.-\nMAT-SCR-RET-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "bold": 1, + "fieldname": "supplier", + "fieldtype": "Link", + "in_global_search": 1, + "label": "Supplier", + "options": "Supplier", + "print_hide": 1, + "print_width": "150px", + "reqd": 1, + "search_index": 1, + "width": "150px" + }, + { + "bold": 1, + "depends_on": "supplier", + "fetch_from": "supplier.supplier_name", + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Supplier Name", + "read_only": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "no_copy": 1, + "print_width": "100px", + "reqd": 1, + "search_index": 1, + "width": "100px" + }, + { + "description": "Time at which materials were received", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "print_width": "150px", + "remember_last_selected_value": 1, + "reqd": 1, + "width": "150px" + }, + { + "collapsible": 1, + "fieldname": "section_addresses", + "fieldtype": "Section Break", + "label": "Address and Contact" + }, + { + "fieldname": "supplier_address", + "fieldtype": "Link", + "label": "Select Supplier Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Small Text", + "label": "Address", + "read_only": 1 + }, + { + "fieldname": "contact_display", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Contact", + "read_only": 1 + }, + { + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "label": "Mobile No", + "read_only": 1 + }, + { + "fieldname": "contact_email", + "fieldtype": "Small Text", + "label": "Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "col_break_address", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Select Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Small Text", + "label": "Shipping Address", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "sec_warehouse", + "fieldtype": "Section Break" + }, + { + "description": "Sets 'Accepted Warehouse' in each row of the Items table.", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "label": "Accepted Warehouse", + "options": "Warehouse", + "print_hide": 1 + }, + { + "description": "Sets 'Rejected Warehouse' in each row of the Items table.", + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "label": "Rejected Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1 + }, + { + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break" + }, + { + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "label": "Supplier Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "options": "fa fa-shopping-cart" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Subcontracting Receipt Item", + "reqd": 1 + }, + { + "depends_on": "supplied_items", + "fieldname": "get_current_stock", + "fieldtype": "Button", + "label": "Get Current Stock", + "options": "get_current_stock", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "supplied_items", + "depends_on": "supplied_items", + "fieldname": "raw_material_details", + "fieldtype": "Section Break", + "label": "Raw Materials Consumed", + "options": "fa fa-table", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "supplied_items", + "fieldtype": "Table", + "label": "Consumed Items", + "no_copy": 1, + "options": "Subcontracting Receipt Supplied Item", + "print_hide": 1 + }, + { + "fieldname": "section_break0", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_qty", + "fieldtype": "Float", + "label": "Total Quantity", + "read_only": 1 + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "total", + "fieldtype": "Currency", + "label": "Total", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "section_break_46", + "fieldtype": "Section Break" + }, + { + "fieldname": "in_words", + "fieldtype": "Data", + "label": "In Words", + "length": 240, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "bill_no", + "fieldtype": "Data", + "hidden": 1, + "label": "Bill No", + "print_hide": 1 + }, + { + "fieldname": "bill_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Bill Date", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Information", + "options": "fa fa-file-text" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "reqd": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Subcontracting Receipt", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "width": "150px" + }, + { + "fieldname": "range", + "fieldtype": "Data", + "hidden": 1, + "label": "Range", + "print_hide": 1 + }, + { + "fieldname": "column_break4", + "fieldtype": "Column Break", + "print_hide": 1, + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "subscription_detail", + "fieldtype": "Section Break", + "label": "Auto Repeat Detail" + }, + { + "fieldname": "auto_repeat", + "fieldtype": "Link", + "label": "Auto Repeat", + "no_copy": 1, + "options": "Auto Repeat", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "printing_settings", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "language", + "fieldtype": "Data", + "label": "Print Language", + "read_only": 1 + }, + { + "fieldname": "column_break_97", + "fieldtype": "Column Break" + }, + { + "fieldname": "other_details", + "fieldtype": "HTML", + "hidden": 1, + "label": "Other Details", + "options": "
Other Details
", + "print_hide": 1, + "print_width": "30%", + "width": "30%" + }, + { + "fieldname": "instructions", + "fieldtype": "Small Text", + "label": "Instructions" + }, + { + "fieldname": "remarks", + "fieldtype": "Small Text", + "label": "Remarks", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "transporter_name", + "fieldname": "transporter_info", + "fieldtype": "Section Break", + "label": "Transporter Details", + "options": "fa fa-truck" + }, + { + "fieldname": "transporter_name", + "fieldtype": "Data", + "label": "Transporter Name" + }, + { + "fieldname": "column_break5", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "lr_no", + "fieldtype": "Data", + "label": "Vehicle Number", + "no_copy": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "lr_date", + "fieldtype": "Date", + "label": "Vehicle Date", + "no_copy": 1, + "print_width": "100px", + "width": "100px" + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Select Billing Address", + "options": "Address" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address", + "read_only": 1 + }, + { + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Represents Company", + "options": "Company", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "provisional_expense_account", + "fieldtype": "Link", + "hidden": 1, + "label": "Provisional Expense Account", + "options": "Account" + }, + { + "default": "0", + "fieldname": "is_return", + "fieldtype": "Check", + "label": "Is Return", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "is_return", + "fieldname": "return_against", + "fieldtype": "Link", + "label": "Return Against Subcontracting Receipt", + "no_copy": 1, + "options": "Subcontracting Receipt", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_39", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:(!doc.__islocal && doc.is_return==0)", + "fieldname": "per_returned", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "% Returned", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_47", + "fieldtype": "Section Break" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2022-04-18 13:15:12.011682", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Receipt", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "read": 1, + "report": 1, + "role": "Accounts User" + }, + { + "permlevel": 1, + "read": 1, + "role": "Stock Manager", + "write": 1 + } + ], + "search_fields": "status, posting_date, supplier", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "timeline_field": "supplier", + "title_field": "title", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py new file mode 100644 index 00000000000..d80bbdf453a --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -0,0 +1,197 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import cint, flt, getdate, nowdate + +from erpnext.controllers.subcontracting_controller import SubcontractingController + + +class SubcontractingReceipt(SubcontractingController): + def __init__(self, *args, **kwargs): + super(SubcontractingReceipt, self).__init__(*args, **kwargs) + self.status_updater = [ + { + "target_dt": "Subcontracting Order Item", + "join_field": "subcontracting_order_item", + "target_field": "received_qty", + "target_parent_dt": "Subcontracting Order", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_dt": "Subcontracting Receipt Item", + "source_field": "received_qty", + "percent_join_field": "subcontracting_order", + "overflow_type": "receipt", + }, + ] + + def update_status_updater_args(self): + if cint(self.is_return): + self.status_updater.extend( + [ + { + "source_dt": "Subcontracting Receipt Item", + "target_dt": "Subcontracting Order Item", + "join_field": "subcontracting_order_item", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "extra_cond": """ and exists (select name from `tabSubcontracting Receipt` + where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""", + }, + { + "source_dt": "Subcontracting Receipt Item", + "target_dt": "Subcontracting Receipt Item", + "join_field": "subcontracting_receipt_item", + "target_field": "returned_qty", + "target_parent_dt": "Subcontracting Receipt", + "target_parent_field": "per_returned", + "target_ref_field": "received_qty", + "source_field": "-1 * received_qty", + "percent_join_field_parent": "return_against", + }, + ] + ) + + def before_validate(self): + super(SubcontractingReceipt, self).before_validate() + self.set_items_cost_center() + self.set_items_expense_account() + + def validate(self): + super(SubcontractingReceipt, self).validate() + self.set_missing_values() + self.validate_posting_time() + self.validate_rejected_warehouse() + + if self._action == "submit": + self.make_batches("warehouse") + + if getdate(self.posting_date) > getdate(nowdate()): + frappe.throw(_("Posting Date cannot be future date")) + + self.reset_default_field_value("set_warehouse", "items", "warehouse") + self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") + self.get_current_stock() + + def on_submit(self): + self.update_status_updater_args() + self.update_prevdoc_status() + self.set_subcontracting_order_status() + self.set_consumed_qty_in_sco() + self.update_stock_ledger() + + from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + + update_serial_nos_after_submit(self, "items") + + self.make_gl_entries() + self.repost_future_sle_and_gle() + self.update_status() + + def on_cancel(self): + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") + self.update_status_updater_args() + self.update_prevdoc_status() + self.update_stock_ledger() + self.make_gl_entries_on_cancel() + self.repost_future_sle_and_gle() + self.delete_auto_created_batches() + self.set_consumed_qty_in_sco() + self.set_subcontracting_order_status() + self.update_status() + + @frappe.whitelist() + def set_missing_values(self): + self.set_missing_values_in_supplied_items() + self.set_missing_values_in_items() + + def set_missing_values_in_supplied_items(self): + for item in self.get("supplied_items") or []: + item.amount = item.rate * item.consumed_qty + + def set_missing_values_in_items(self): + rm_supp_cost = {} + for item in self.get("supplied_items") or []: + if item.reference_name in rm_supp_cost: + rm_supp_cost[item.reference_name] += item.amount + else: + rm_supp_cost[item.reference_name] = item.amount + + total_qty = total_amount = 0 + for item in self.items: + if item.name in rm_supp_cost: + item.rm_supp_cost = rm_supp_cost[item.name] + item.rm_cost_per_qty = item.rm_supp_cost / item.qty + rm_supp_cost.pop(item.name) + + if self.is_new() and item.rm_supp_cost > 0: + item.rate = item.rm_cost_per_qty + (item.service_cost_per_qty or 0) + item.additional_cost_per_qty + + item.received_qty = item.qty + (item.rejected_qty or 0) + item.amount = item.qty * item.rate + total_qty += item.qty + total_amount += item.amount + else: + self.total_qty = total_qty + self.total = total_amount + + def validate_rejected_warehouse(self): + if not self.rejected_warehouse: + for item in self.items: + if item.rejected_qty: + frappe.throw( + _("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code) + ) + + def set_items_cost_center(self): + if self.company: + cost_center = frappe.get_cached_value("Company", self.company, "cost_center") + + for item in self.items: + if not item.cost_center: + item.cost_center = cost_center + + def set_items_expense_account(self): + if self.company: + expense_account = self.get_company_default("default_expense_account", ignore_validation=True) + + for item in self.items: + if not item.expense_account: + item.expense_account = expense_account + + @frappe.whitelist() + def get_current_stock(self): + for item in self.get("supplied_items"): + if self.supplier_warehouse: + actual_qty = frappe.db.get_value( + "Bin", + {"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse}, + "actual_qty", + ) + item.current_stock = flt(actual_qty) or 0 + + def update_status(self, status=None, update_modified=False): + if self.docstatus >= 1 and not status: + if self.docstatus == 1: + if self.is_return: + status = "Return" + return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) + return_against.run_method("update_status") + else: + if self.per_returned == 100: + status = "Return Issued" + elif self.status == "Draft": + status = "Completed" + elif self.docstatus == 2: + status = "Cancelled" + + if status: + frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified) + + +@frappe.whitelist() +def make_subcontract_return(source_name, target_doc=None): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + return make_return_doc("Subcontracting Receipt", source_name, target_doc) \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py new file mode 100644 index 00000000000..a9e51937d7b --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py @@ -0,0 +1,15 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "subcontracting_receipt_no", + "internal_links": { + "Subcontracting Order": ["items", "subcontracting_order"], + "Project": ["items", "project"], + "Quality Inspection": ["items", "quality_inspection"], + }, + "transactions": [ + {"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]}, + ], + } diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js new file mode 100644 index 00000000000..6d961de8c64 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.listview_settings['Subcontracting Receipt'] = { + get_indicator: function (doc) { + const status_colors = { + "Draft": "grey", + "Return": "gray", + "Return Issued": "grey", + "Completed": "green", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py new file mode 100644 index 00000000000..bc41dca319f --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestSubcontractingReceipt(FrappeTestCase): + pass From 574181f3d789e9010920d04d8746d242edf67100 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 26 Apr 2022 18:20:57 +0530 Subject: [PATCH 13/41] test: SubcontractingController --- .../tests/test_subcontracting_controller.py | 1024 +++++++++++++++++ 1 file changed, 1024 insertions(+) create mode 100644 erpnext/controllers/tests/test_subcontracting_controller.py diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py new file mode 100644 index 00000000000..ff588be643b --- /dev/null +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -0,0 +1,1024 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import copy +from collections import defaultdict + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import cint + +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + get_materials_from_supplier, + make_rm_stock_entry, + make_subcontracting_receipt, +) + + +class TestSubcontractingController(FrappeTestCase): + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def test_remove_empty_rows(self): + sco = get_subcontracting_order() + len_before = len(sco.service_items) + sco.service_items[0].item_code = None + sco.remove_empty_rows() + self.assertEqual((len_before - 1), len(sco.service_items)) + + def test_create_raw_materials_supplied(self): + sco = get_subcontracting_order() + sco.supplied_items = None + sco.create_raw_materials_supplied() + self.assertIsNotNone(sco.supplied_items) + + def test_sco_with_bom(self): + """ + - Set backflush based on BOM. + - Create SCO for the item Subcontracted Item SA1 and add same item two times. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Create SCR against the SCO and check serial nos and batch no. + """ + + set_backflush_based_on("BOM") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + for key, value in get_supplied_items(scr).items(): + transferred_detais = itemwise_details.get(key) + + for field in ["qty", "serial_no", "batch_no"]: + if value.get(field): + transfer, consumed = (transferred_detais.get(field), value.get(field)) + if field == "serial_no": + transfer, consumed = (sorted(transfer), sorted(consumed)) + + self.assertEqual(transfer, consumed) + + def test_sco_with_material_transfer(self): + """ + - Set backflush based on Material Transfer. + - Create SCO for the item Subcontracted Item SA1 and Subcontracted Item SA5. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. + - Create partial SCR against the SCO and check serial nos and batch no. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 5", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA5", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items.append( + { + "main_item_code": "Subcontracted Item SA5", + "item_code": "Subcontracted SRM Item 4", + "qty": 6, + } + ) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.remove(scr1.items[1]) + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + transferred_detais = itemwise_details.get(key) + + for field in ["qty", "serial_no", "batch_no"]: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.save() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + transferred_detais = itemwise_details.get(key) + + for field in ["qty", "serial_no", "batch_no"]: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + def test_subcontracting_with_same_components_different_fg(self): + """ + - Set backflush based on Material Transfer. + - Create SCO for the item Subcontracted Item SA2 and Subcontracted Item SA3. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of components for the item Subcontracted Item SA2. + - Create partial SCR against the SCO and check serial nos. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 3", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA3", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] += 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 3 + scr1.remove(scr1.items[1]) + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 4) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4])) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.items[0].qty = 2 + scr2.remove(scr2.items[1]) + scr2.save() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 2) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6])) + + scr3 = make_subcontracting_receipt(sco.name) + scr3.save() + scr3.submit() + + for key, value in get_supplied_items(scr3).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 6) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12])) + + def test_return_non_consumed_materials(self): + """ + - Set backflush based on Material Transfer. + - Create SCO for item Subcontracted Item SA2. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. + - Create SCR for full qty against the SCO and change the qty of raw material. + - After that return the non consumed material back to the store from supplier's warehouse. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] += 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + scr1.supplied_items[0].consumed_qty = 5 + scr1.supplied_items[0].serial_no = "\n".join( + sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5]) + ) + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + transferred_detais = itemwise_details.get(key) + self.assertEqual(value.qty, 5) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5])) + + sco.load_from_db() + self.assertEqual(sco.supplied_items[0].consumed_qty, 5) + doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) + self.assertEqual(doc.items[0].qty, 1) + self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC") + self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC") + self.assertEqual( + get_serial_nos(doc.items[0].serial_no), + itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6], + ) + + def test_item_with_batch_based_on_bom(self): + """ + - Set backflush based on BOM. + - Create SCO for item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 SCR against the SCO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the SCR. + """ + + set_backflush_based_on("BOM") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 1", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 2", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 1.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 2 + add_second_row_in_scr(scr1) + scr1.flags.ignore_mandatory = True + scr1.save() + scr1.set_missing_values() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + self.assertEqual(value.qty, 4) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.items[0].qty = 2 + add_second_row_in_scr(scr2) + scr2.flags.ignore_mandatory = True + scr2.save() + scr2.set_missing_values() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + self.assertEqual(value.qty, 4) + + scr3 = make_subcontracting_receipt(sco.name) + scr3.items[0].qty = 2 + scr3.flags.ignore_mandatory = True + scr3.save() + scr3.set_missing_values() + scr3.submit() + + for key, value in get_supplied_items(scr3).items(): + self.assertEqual(value.qty, 2) + + def test_item_with_batch_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 SCR against the SCO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the SCR. + - In the first SCR the batched raw materials will be consumed 2 extra qty. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 1", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 2", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 2 + add_second_row_in_scr(scr1) + scr1.flags.ignore_mandatory = True + scr1.save() + scr1.set_missing_values() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + qty = 4 if key != "Subcontracted SRM Item 3" else 6 + self.assertEqual(value.qty, qty) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.items[0].qty = 2 + add_second_row_in_scr(scr2) + scr2.flags.ignore_mandatory = True + scr2.save() + scr2.set_missing_values() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + self.assertEqual(value.qty, 4) + + scr3 = make_subcontracting_receipt(sco.name) + scr3.items[0].qty = 2 + scr3.flags.ignore_mandatory = True + scr3.save() + scr3.set_missing_values() + scr3.submit() + + for key, value in get_supplied_items(scr3).items(): + self.assertEqual(value.qty, 1) + + def test_partial_transfer_serial_no_components_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial SCR against the SCO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create SCR for remaining qty against the SCO and change the qty manually. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] = 5 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 5 + scr1.flags.ignore_mandatory = True + scr1.save() + scr1.set_missing_values() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3])) + + scr1.load_from_db() + scr1.supplied_items[0].consumed_qty = 5 + scr1.supplied_items[0].serial_no = "\n".join( + itemwise_details[scr1.supplied_items[0].rm_item_code]["serial_no"] + ) + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + def test_incorrect_serial_no_components_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create SCR and change the serial no which is not transferred. + - System should throw the error and not allowed to save the SCR. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + scr1.supplied_items[0].serial_no = "ABCD" + self.assertRaises(frappe.ValidationError, scr1.save) + scr1.delete() + + def test_partial_transfer_batch_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial SCR against the SCO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create SCR for remaining qty against the SCO and change the qty manually. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 6", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA6", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] = 5 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 5 + scr1.save() + + transferred_batch_no = "" + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + transferred_batch_no = details.batch_no + self.assertEqual(value.batch_no, details.batch_no) + + scr1.load_from_db() + scr1.supplied_items[0].consumed_qty = 5 + scr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + +def add_second_row_in_scr(scr): + item_dict = {} + for column in [ + "item_code", + "item_name", + "qty", + "uom", + "warehouse", + "stock_uom", + "subcontracting_order", + "subcontracting_order_finished_good_item", + "conversion_factor", + "rate", + "expense_account", + "sco_rm_detail", + ]: + item_dict[column] = scr.items[0].get(column) + + scr.append("items", item_dict) + + +def get_supplied_items(scr_doc): + supplied_items = {} + for row in scr_doc.get("supplied_items"): + if row.rm_item_code not in supplied_items: + supplied_items.setdefault( + row.rm_item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) + + details = supplied_items[row.rm_item_code] + update_item_details(row, details) + + return supplied_items + + +def make_stock_in_entry(**args): + args = frappe._dict(args) + + items = {} + for row in args.rm_items: + row = frappe._dict(row) + + doc = make_stock_entry( + target=row.warehouse or "_Test Warehouse - _TC", + item_code=row.item_code, + qty=row.qty or 1, + basic_rate=row.rate or 100, + ) + + if row.item_code not in items: + items.setdefault( + row.item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) + + child_row = doc.items[0] + details = items[child_row.item_code] + update_item_details(child_row, details) + + return items + + +def update_item_details(child_row, details): + details.qty += ( + child_row.get("qty") + if child_row.doctype == "Stock Entry Detail" + else child_row.get("consumed_qty") + ) + + if child_row.serial_no: + details.serial_no.extend(get_serial_nos(child_row.serial_no)) + + if child_row.batch_no: + details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") + + +def make_stock_transfer_entry(**args): + args = frappe._dict(args) + + items = [] + for row in args.rm_items: + row = frappe._dict(row) + + item = { + "item_code": row.main_item_code or args.main_item_code, + "rm_item_code": row.item_code, + "qty": row.qty or 1, + "item_name": row.item_code, + "rate": row.rate or 100, + "stock_uom": row.stock_uom or "Nos", + "warehouse": row.warehuose or "_Test Warehouse - _TC", + } + + item_details = args.itemwise_details.get(row.item_code) + + if item_details and item_details.serial_no: + serial_nos = item_details.serial_no[0 : cint(row.qty)] + item["serial_no"] = "\n".join(serial_nos) + item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) + + if item_details and item_details.batch_no: + for batch_no, batch_qty in item_details.batch_no.items(): + if batch_qty >= row.qty: + item["batch_no"] = batch_no + item_details.batch_no[batch_no] -= row.qty + break + + items.append(item) + + ste_dict = make_rm_stock_entry(args.sco_no, items) + doc = frappe.get_doc(ste_dict) + doc.insert() + doc.submit() + + return doc + + +def make_subcontracted_items(): + sub_contracted_items = { + "Subcontracted Item SA1": {}, + "Subcontracted Item SA2": {}, + "Subcontracted Item SA3": {}, + "Subcontracted Item SA4": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SBAT.####", + }, + "Subcontracted Item SA5": {}, + "Subcontracted Item SA6": {}, + "Subcontracted Item SA7": {}, + } + + for item, properties in sub_contracted_items.items(): + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1, "is_sub_contracted_item": 1}) + make_item(item, properties) + + +def make_raw_materials(): + raw_materials = { + "Subcontracted SRM Item 1": {}, + "Subcontracted SRM Item 2": {"has_serial_no": 1, "serial_no_series": "SRI.####"}, + "Subcontracted SRM Item 3": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT.####", + }, + "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + } + + for item, properties in raw_materials.items(): + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1}) + make_item(item, properties) + + +def make_service_item(item, properties={}): + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 0}) + make_item(item, properties) + + +def make_service_items(): + service_items = { + "Subcontracted Service Item 1": {}, + "Subcontracted Service Item 2": {}, + "Subcontracted Service Item 3": {}, + "Subcontracted Service Item 4": {}, + "Subcontracted Service Item 5": {}, + "Subcontracted Service Item 6": {}, + "Subcontracted Service Item 7": {}, + } + + for item, properties in service_items.items(): + make_service_item(item, properties) + + +def make_bom_for_subcontracted_items(): + boms = { + "Subcontracted Item SA1": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA2": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA3": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA4": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], + "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], + "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], + } + + for item_code, raw_materials in boms.items(): + if not frappe.db.exists("BOM", {"item": item_code}): + make_bom(item=item_code, raw_materials=raw_materials, rate=100) + + +def set_backflush_based_on(based_on): + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on + ) + + +def get_subcontracting_order(**args): + from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import ( + create_subcontracting_order, + ) + + args = frappe._dict(args) + + if args.get("po_name"): + po = frappe.get_doc("Purchase Order", args.get("po_name")) + + if po.is_subcontracted: + return create_subcontracting_order(po_name=po.name, **args) + + if not args.service_items: + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 7", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + else: + service_items = args.service_items + + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC", + ) + + return create_subcontracting_order(po_name=po.name, **args) + + +def get_rm_items(supplied_items): + rm_items = [] + + for item in supplied_items: + rm_items.append( + { + "main_item_code": item.main_item_code, + "item_code": item.rm_item_code, + "qty": item.required_qty, + "rate": item.rate, + "stock_uom": item.stock_uom, + "warehouse": item.reserve_warehouse, + } + ) + + return rm_items + + +def make_subcontracted_item(**args): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + args = frappe._dict(args) + + if not frappe.db.exists("Item", args.item_code): + make_item( + args.item_code, + { + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": args.get("has_batch_no") or 0, + }, + ) + + if not args.raw_materials: + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item( + "Test Extra Item 1", + { + "is_stock_item": 1, + }, + ) + + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item( + "Test Extra Item 2", + { + "is_stock_item": 1, + }, + ) + + args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] + + if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): + make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) \ No newline at end of file From 8bc653b633895b40d278bf2745b1a223592d2cca Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 26 Apr 2022 18:23:12 +0530 Subject: [PATCH 14/41] test: SubcontractingOrder --- .../test_subcontracting_order.py | 526 +++++++++++++++++- 1 file changed, 524 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index f58c8307e49..5644045df9a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -1,8 +1,530 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import copy + +import frappe from frappe.tests.utils import FrappeTestCase +from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_rm_items, + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_stock_in_entry, + make_stock_transfer_entry, + make_subcontracted_item, + make_subcontracted_items, + set_backflush_based_on, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_rm_stock_entry, + make_subcontracting_receipt, +) + + class TestSubcontractingOrder(FrappeTestCase): - pass \ No newline at end of file + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def test_populate_items_table(self): + sco = get_subcontracting_order() + sco.items = None + sco.populate_items_table() + self.assertEqual(len(sco.service_items), len(sco.items)) + + def test_set_missing_values(self): + sco = get_subcontracting_order() + before = {sco.total_qty, sco.total, sco.total_additional_costs} + sco.total_qty = sco.total = sco.total_additional_costs = 0 + sco.set_missing_values() + after = {sco.total_qty, sco.total, sco.total_additional_costs} + self.assertSetEqual(before, after) + + def test_update_status(self): + # Draft + sco = get_subcontracting_order(do_not_submit=1) + self.assertEqual(sco.status, "Draft") + + # Open + sco.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Open") + + # Partial Material Transferred + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] -= 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + sco.load_from_db() + self.assertEqual(sco.status, "Partial Material Transferred") + + # Material Transferred + rm_items[0]["qty"] = 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + sco.load_from_db() + self.assertEqual(sco.status, "Material Transferred") + + # Partially Received + scr = make_subcontracting_receipt(sco.name) + scr.items[0].qty -= 1 + scr.save() + scr.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Partially Received") + + # Completed + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Completed") + + def test_make_rm_stock_entry(self): + sco = get_subcontracting_order() + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + self.assertEqual(len(ste.items), len(rm_items)) + + def test_make_rm_stock_entry_for_serial_items(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 5", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA5", + "fg_item_qty": 6, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + self.assertEqual(len(ste.items), len(rm_items)) + + def test_make_rm_stock_entry_for_batch_items(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 6", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA6", + "fg_item_qty": 6, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + self.assertEqual(len(ste.items), len(rm_items)) + + def test_update_reserved_qty_for_subcontracting(self): + # Make stock available for raw materials + make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code="_Test Item Home Desktop 100", + qty=30, + basic_rate=100, + ) + + bin1 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], + as_dict=1, + ) + + # Create SCO + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + bin2 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], + as_dict=1, + ) + + self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) + self.assertNotEqual(bin1.modified, bin2.modified) + + # Create stock transfer + rm_items = [ + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item", + "item_name": "_Test Item", + "qty": 6, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 600, + "stock_uom": "Nos", + } + ] + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.to_warehouse = "_Test Warehouse 1 - _TC" + ste.save() + ste.submit() + + bin3 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + make_stock_entry( + target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code="_Test Item Home Desktop 100", + qty=40, + basic_rate=100, + ) + + # Make SCR against the SCO + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + bin4 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + # Cancel SCR + scr.reload() + scr.cancel() + bin5 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + # Cancel Stock Entry + ste.cancel() + bin6 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + + # Cancel PO + sco.reload() + sco.cancel() + bin7 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + def test_exploded_items(self): + item_code = "_Test Subcontracted FG Item 11" + make_subcontracted_item(item_code=item_code) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": 1, + }, + ] + + sco1 = get_subcontracting_order(service_items=service_items, include_exploded_items=1) + item_name = frappe.db.get_value("BOM", {"item": item_code}, "name") + bom = frappe.get_doc("BOM", item_name) + exploded_items = sorted([item.item_code for item in bom.exploded_items]) + supplied_items = sorted([item.rm_item_code for item in sco1.supplied_items]) + self.assertEqual(exploded_items, supplied_items) + + sco2 = get_subcontracting_order(service_items=service_items, include_exploded_items=0) + supplied_items1 = sorted([item.rm_item_code for item in sco2.supplied_items]) + bom_items = sorted([item.item_code for item in bom.items]) + self.assertEqual(supplied_items1, bom_items) + + def test_backflush_based_on_stock_entry(self): + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code=item_code) + make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1}) + + set_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 5 + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": order_qty, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": order_qty, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 1", + qty=10, + basic_rate=100, + ) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 1", + "item_name": "_Test Item", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 20, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "Test Extra Item 1", + "item_name": "Test Extra Item 1", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "Test Extra Item 2", + "stock_uom": "Nos", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "item_name": "Test Extra Item 2", + }, + ] + + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.submit() + + scr = make_subcontracting_receipt(sco.name) + received_qty = 2 + + # partial receipt + scr.get("items")[0].qty = received_qty + scr.save() + scr.submit() + + transferred_items = sorted( + [item.item_code for item in ste.get("items") if ste.subcontracting_order == sco.name] + ) + issued_items = sorted([item.rm_item_code for item in scr.get("supplied_items")]) + + self.assertEqual(transferred_items, issued_items) + self.assertEqual(scr.get_supplied_items_cost(scr.get("items")[0].name), 2000) + + transferred_rm_map = frappe._dict() + for item in rm_items: + transferred_rm_map[item.get("rm_item_code")] = item + + set_backflush_based_on("BOM") + + def test_supplied_qty(self): + item_code = "_Test Subcontracted FG Item 5" + make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1}) + + make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) + + set_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 250 + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": order_qty, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": order_qty, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": order_qty, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": order_qty, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + + # Material receipt entry for the raw materials which will be send to supplier + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 4", + qty=500, + basic_rate=100, + ) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 4", + "item_name": "_Test Item", + "qty": 250, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": sco.supplied_items[0].name, + }, + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 4", + "item_name": "_Test Item", + "qty": 250, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + ] + + # Raw Materials transfer entry from stores to supplier's warehouse + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.submit() + + # Test sco_rm_detail field has value or not + for item_row in ste.items: + self.assertEqual(item_row.sco_rm_detail, sco.supplied_items[item_row.idx - 1].name) + + sco.load_from_db() + for row in sco.supplied_items: + # Valid that whether transferred quantity is matching with supplied qty or not in the subcontracting order + self.assertEqual(row.supplied_qty, 250.0) + + set_backflush_based_on("BOM") + + +def create_subcontracting_order(**args): + args = frappe._dict(args) + sco = get_mapped_subcontracting_order(source_name=args.po_name) + + for item in sco.items: + item.include_exploded_items = args.get("include_exploded_items", 1) + + if args.get("warehouse"): + for item in sco.items: + item.warehouse = args.warehouse + else: + warehouse = frappe.get_value("Purchase Order", args.po_name, "set_warehouse") + if warehouse: + for item in sco.items: + item.warehouse = warehouse + else: + po = frappe.get_doc("Purchase Order", args.po_name) + warehouses = [] + for item in po.items: + warehouses.append(item.warehouse) + else: + for idx, val in enumerate(sco.items): + val.warehouse = warehouses[idx] + + if not args.do_not_save: + sco.insert() + if not args.do_not_submit: + sco.submit() + + return sco \ No newline at end of file From 785d59876253a6696a60a3724b2f3d3209f80c10 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 9 May 2022 11:28:40 +0530 Subject: [PATCH 15/41] test: SubcontractingReceipt --- .../test_subcontracting_receipt.py | 304 +++++++++++++++++- 1 file changed, 302 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index bc41dca319f..8680311c792 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -1,9 +1,309 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe + +import copy + +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import flt + +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_rm_items, + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_stock_in_entry, + make_stock_transfer_entry, + make_subcontracted_item, + make_subcontracted_items, + set_backflush_based_on, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import make_subcontracting_receipt class TestSubcontractingReceipt(FrappeTestCase): - pass + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def test_subcontracting(self): + set_backflush_based_on("BOM") + make_stock_entry( + item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", + qty=100, + target="_Test Warehouse 1 - _TC", + basic_rate=100, + ) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + rm_supp_cost = sum(item.amount for item in scr.get("supplied_items")) + self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) + + def test_subcontracting_gle_fg_item_rate_zero(self): + from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries + + set_backflush_based_on("BOM") + make_stock_entry( + item_code="_Test Item", + target="Work In Progress - TCP1", + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="Work In Progress - TCP1", + qty=100, + basic_rate=100, + company="_Test Company with perpetual inventory", + ) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 0, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + gl_entries = get_gl_entries("Subcontracting Receipt", scr.name) + self.assertFalse(gl_entries) + + def test_subcontracting_over_receipt(self): + """ + Behaviour: Raise multiple SCRs against one SCO that in total + receive more than the required qty in the SCO. + Expected Result: Error Raised for Over Receipt against SCO. + """ + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_rm_stock_entry as make_subcontract_transfer_entry, + ) + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_subcontracting_receipt, + ) + from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import ( + make_subcontracted_item, + ) + + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code=item_code) + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": "_Test Subcontracted FG Item 1", + "fg_item_qty": 1, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, + include_exploded_items=0, + ) + + # stock raw materials in a warehouse before transfer + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 + ) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": sco.supplied_items[0].rm_item_code, + "item_name": "_Test FG Item", + "qty": sco.supplied_items[0].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": sco.supplied_items[1].rm_item_code, + "item_name": "Test Extra Item 1", + "qty": sco.supplied_items[1].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + ] + ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items)) + ste.to_warehouse = "_Test Warehouse 1 - _TC" + ste.save() + ste.submit() + + scr1 = make_subcontracting_receipt(sco.name) + scr2 = make_subcontracting_receipt(sco.name) + + scr1.submit() + self.assertRaises(frappe.ValidationError, scr2.submit) + + def test_subcontracted_scr_for_multi_transfer_batches(self): + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_rm_stock_entry, + make_subcontracting_receipt, + ) + + set_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 3" + + make_item( + "Sub Contracted Raw Material 3", + {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1}, + ) + + make_subcontracted_item( + item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"] + ) + + order_qty = 500 + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 3", + "qty": order_qty, + "rate": 100, + "fg_item": "_Test Subcontracted FG Item 3", + "fg_item_qty": order_qty, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + ste1 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=300, + basic_rate=100, + ) + ste2 = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 3", + qty=200, + basic_rate=100, + ) + + transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200} + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 300, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": sco.supplied_items[0].name, + }, + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 3", + "item_name": "_Test Item", + "qty": 200, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": sco.supplied_items[0].name, + }, + ] + + se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + self.assertEqual(len(se.items), 2) + se.items[0].batch_no = ste1.items[0].batch_no + se.items[1].batch_no = ste2.items[0].batch_no + se.submit() + + supplied_qty = frappe.db.get_value( + "Subcontracting Order Supplied Item", + {"parent": sco.name, "rm_item_code": "Sub Contracted Raw Material 3"}, + "supplied_qty", + ) + + self.assertEqual(supplied_qty, 500.00) + + scr = make_subcontracting_receipt(sco.name) + scr.save() + self.assertEqual(len(scr.supplied_items), 2) + + for row in scr.supplied_items: + self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) + + +def get_items(**args): + args = frappe._dict(args) + return [ + { + "conversion_factor": 1.0, + "description": "_Test Item", + "doctype": "Subcontracting Receipt Item", + "item_code": "_Test Item", + "item_name": "_Test Item", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + { + "conversion_factor": 1.0, + "description": "_Test Item Home Desktop 100", + "doctype": "Subcontracting Receipt Item", + "item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "parentfield": "items", + "qty": 5.0, + "rate": 50.0, + "received_qty": 5.0, + "rejected_qty": 0.0, + "stock_uom": "_Test UOM", + "warehouse": args.warehouse or "_Test Warehouse 1 - _TC", + "cost_center": args.cost_center or "Main - _TC", + }, + ] From dafaed3cbd6f7d52119e2600c675aa5fa64b04ba Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 4 May 2022 15:20:29 +0530 Subject: [PATCH 16/41] refactor!: Purchase Order --- .../doctype/purchase_order/purchase_order.js | 206 +------- .../doctype/purchase_order/purchase_order.py | 150 ------ .../purchase_order/test_purchase_order.py | 452 +----------------- .../doctype/purchase_order/test_records.json | 34 -- .../purchase_order_item.json | 2 - 5 files changed, 2 insertions(+), 842 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2cad1fb0817..d347026b63b 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -7,17 +7,6 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Purchase Order", { setup: function(frm) { - - frm.set_query("reserve_warehouse", "supplied_items", function() { - return { - filters: { - "company": frm.doc.company, - "name": ['!=', frm.doc.supplier_warehouse], - "is_group": 0 - } - } - }); - frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) @@ -59,39 +48,6 @@ frappe.ui.form.on("Purchase Order", { frm.set_value("tax_withholding_category", frm.supplier_tds); } }, - - refresh: function(frm) { - frm.trigger('get_materials_from_supplier'); - }, - - get_materials_from_supplier: function(frm) { - let po_details = []; - - if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { - frm.doc.supplied_items.forEach(d => { - if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { - po_details.push(d.name) - } - }); - } - - if (po_details && po_details.length) { - frm.add_custom_button(__('Return of Components'), () => { - frm.call({ - method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', - freeze: true, - freeze_message: __('Creating Stock Entry'), - args: { purchase_order: frm.doc.name, po_details: po_details }, - callback: function(r) { - if (r && r.message) { - const doc = frappe.model.sync(r.message); - frappe.set_route("Form", doc[0].doctype, doc[0].name); - } - } - }); - }, __('Create')); - } - } }); frappe.ui.form.on("Purchase Order Item", { @@ -112,13 +68,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e this.frm.custom_make_buttons = { 'Purchase Receipt': 'Purchase Receipt', 'Purchase Invoice': 'Purchase Invoice', - 'Stock Entry': 'Material to Supplier', 'Payment Entry': 'Payment', 'Subcontracting Order': 'Subcontracting Order' } super.setup(); - } refresh(doc, cdt, cdn) { @@ -185,10 +139,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if (doc.status != "On Hold") { if(flt(doc.per_received) < 100 && allow_receipt) { cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); - if(doc.is_subcontracted && me.has_unsupplied_items()) { - 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')); } @@ -258,142 +208,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e set_schedule_date(this.frm); } - has_unsupplied_items() { - return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); - } - - make_stock_entry() { - var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }); - var me = this; - - if(items.length >= 1){ - me.raw_material_data = []; - me.show_dialog = 1; - let title = __('Transfer Material to Supplier'); - let fields = [ - {fieldtype:'Section Break', label: __('Raw Materials')}, - {fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), - fields: [ - { - fieldtype:'Data', - fieldname:'item_code', - label: __('Item'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - fieldname:'rm_item_code', - label: __('Raw Material'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'qty', - label: __('Quantity'), - read_only:1, - in_list_view:1 - }, - { - fieldtype:'Data', - read_only:1, - fieldname:'warehouse', - label: __('Reserve Warehouse'), - in_list_view:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'rate', - label: __('Rate'), - hidden:1 - }, - { - fieldtype:'Float', - read_only:1, - fieldname:'amount', - label: __('Amount'), - hidden:1 - }, - { - fieldtype:'Link', - read_only:1, - fieldname:'uom', - label: __('UOM'), - hidden:1 - } - ], - data: me.raw_material_data, - get_data: function() { - return me.raw_material_data; - } - } - ] - - me.dialog = new frappe.ui.Dialog({ - title: title, fields: fields - }); - - if (me.frm.doc['supplied_items']) { - me.frm.doc['supplied_items'].forEach((item, index) => { - if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { - me.raw_material_data.push ({ - 'name':item.name, - 'item_code': item.main_item_code, - 'rm_item_code': item.rm_item_code, - 'item_name': item.rm_item_code, - 'qty': item.required_qty - item.supplied_qty, - 'warehouse':item.reserve_warehouse, - 'rate':item.rate, - 'amount':item.amount, - 'stock_uom':item.stock_uom - }); - me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); - } - }) - } - - me.dialog.get_field('sub_con_rm_items').check_all_rows() - - me.dialog.show() - this.dialog.set_primary_action(__('Transfer'), function() { - me.values = me.dialog.get_values(); - if(me.values) { - me.values.sub_con_rm_items.map((row,i) => { - if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { - let row_id = i+1; - frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id])); - } - }) - me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) - me.dialog.hide() - } - }); - } - - me.dialog.get_close_btn().on('click', () => { - me.dialog.hide(); - }); - - } - - _make_rm_stock_entry(rm_items) { - frappe.call({ - method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry", - args: { - purchase_order: cur_frm.doc.name, - rm_items: rm_items - } - , - callback: function(r) { - var doclist = frappe.model.sync(r.message); - frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }); - } - make_inter_company_order(frm) { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order", @@ -632,28 +446,10 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc, } } -cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) { - var d = locals[cdt][cdn] - return { - filters: [ - ['BOM', 'item', '=', d.item_code], - ['BOM', 'is_active', '=', '1'], - ['BOM', 'docstatus', '=', '1'], - ['BOM', 'company', '=', doc.company] - ] - } -} - function set_schedule_date(frm) { if(frm.doc.schedule_date){ erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "items", "schedule_date"); } } -frappe.provide("erpnext.buying"); - -frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) { - if (frm.doc.is_subcontracted) { - erpnext.buying.get_default_bom(frm); - } -}); +frappe.provide("erpnext.buying"); \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 1945079171f..234bec17ef0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -24,7 +24,6 @@ from erpnext.controllers.buying_controller import BuyingController from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty -from erpnext.stock.utils import get_bin form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -70,7 +69,6 @@ class PurchaseOrder(BuyingController): self.validate_for_subcontracting() self.validate_minimum_order_qty() 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( self.doctype, self.supplier, self.company, self.inter_company_order_reference @@ -307,9 +305,6 @@ class PurchaseOrder(BuyingController): self.set_status(update=True, status=status) self.update_requested_qty() self.update_ordered_qty() - if self.is_subcontracted: - self.update_reserved_qty_for_subcontract() - self.notify_update() clear_doctype_notifications(self) @@ -324,9 +319,6 @@ class PurchaseOrder(BuyingController): self.update_ordered_qty() self.validate_budget() - if self.is_subcontracted: - self.update_reserved_qty_for_subcontract() - frappe.get_doc("Authorization Control").validate_approving_authority( self.doctype, self.company, self.base_grand_total ) @@ -344,9 +336,6 @@ class PurchaseOrder(BuyingController): if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() - if self.is_subcontracted: - self.update_reserved_qty_for_subcontract() - self.check_on_hold_or_closed_status() frappe.db.set(self, "status", "Cancelled") @@ -416,12 +405,6 @@ class PurchaseOrder(BuyingController): if item.delivered_by_supplier == 1: item.received_qty = item.qty - def update_reserved_qty_for_subcontract(self): - for d in self.supplied_items: - if d.rm_item_code: - stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse) - stock_bin.update_reserved_qty_for_sub_contracting() - def update_receiving_percentage(self): total_qty, received_qty = 0.0, 0.0 for item in self.items: @@ -599,78 +582,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions return doc -@frappe.whitelist() -def make_rm_stock_entry(purchase_order, rm_items): - rm_items_list = rm_items - - if isinstance(rm_items, str): - rm_items_list = json.loads(rm_items) - elif not rm_items: - frappe.throw(_("No Items available for transfer")) - - if rm_items_list: - fg_items = list(set(d["item_code"] for d in rm_items_list)) - else: - frappe.throw(_("No Items selected for transfer")) - - if purchase_order: - purchase_order = frappe.get_doc("Purchase Order", purchase_order) - - if fg_items: - items = tuple(set(d["rm_item_code"] for d in rm_items_list)) - item_wh = get_item_details(items) - - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Send to Subcontractor" - stock_entry.purchase_order = purchase_order.name - stock_entry.supplier = purchase_order.supplier - stock_entry.supplier_name = purchase_order.supplier_name - stock_entry.supplier_address = purchase_order.supplier_address - stock_entry.address_display = purchase_order.address_display - stock_entry.company = purchase_order.company - stock_entry.to_warehouse = purchase_order.supplier_warehouse - stock_entry.set_stock_entry_type() - - for item_code in fg_items: - for rm_item_data in rm_items_list: - if rm_item_data["item_code"] == item_code: - rm_item_code = rm_item_data["rm_item_code"] - items_dict = { - rm_item_code: { - "po_detail": rm_item_data.get("name"), - "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item_data["qty"], - "from_warehouse": rm_item_data["warehouse"], - "stock_uom": rm_item_data["stock_uom"], - "serial_no": rm_item_data.get("serial_no"), - "batch_no": rm_item_data.get("batch_no"), - "main_item_code": rm_item_data["item_code"], - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), - } - } - stock_entry.add_to_stock_entry_detail(items_dict) - return stock_entry.as_dict() - else: - frappe.throw(_("No Items selected for transfer")) - return purchase_order.name - - -def get_item_details(items): - item_details = {} - for d in frappe.db.sql( - """select item_code, description, allow_alternative_item from `tabItem` - where name in ({0})""".format( - ", ".join(["%s"] * len(items)) - ), - items, - as_dict=1, - ): - item_details[d.item_code] = d - - return item_details - - def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -700,67 +611,6 @@ def make_inter_company_sales_order(source_name, target_doc=None): return make_inter_company_transaction("Purchase Order", source_name, target_doc) -@frappe.whitelist() -def get_materials_from_supplier(purchase_order, po_details): - if isinstance(po_details, str): - po_details = json.loads(po_details) - - doc = frappe.get_cached_doc("Purchase Order", purchase_order) - doc.initialized_fields() - doc.purchase_orders = [doc.name] - doc.get_available_materials() - - if not doc.available_materials: - frappe.throw( - _("Materials are already received against the purchase order {0}").format(purchase_order) - ) - - return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details) - - -def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): - ste_doc = frappe.new_doc("Stock Entry") - ste_doc.purpose = "Material Transfer" - ste_doc.purchase_order = po_doc.name - ste_doc.company = po_doc.company - ste_doc.is_return = 1 - - for key, value in available_materials.items(): - if not value.qty: - continue - - if value.batch_no: - for batch_no, qty in value.batch_no.items(): - if qty > 0: - add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) - else: - add_items_in_ste(ste_doc, value, value.qty, po_details) - - ste_doc.set_stock_entry_type() - ste_doc.calculate_rate_and_amount() - - return ste_doc - - -def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): - item = ste_doc.append("items", row.item_details) - - po_detail = list(set(row.po_details).intersection(po_details)) - item.update( - { - "qty": qty, - "batch_no": batch_no, - "basic_rate": row.item_details["rate"], - "po_detail": po_detail[0] if po_detail else "", - "s_warehouse": row.item_details["t_warehouse"], - "t_warehouse": row.item_details["s_warehouse"], - "item_code": row.item_details["rm_item_code"], - "subcontracted_item": row.item_details["main_item_code"], - "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) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 1a7f2dd5d97..e1da54e4105 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -13,9 +13,6 @@ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_rm_stock_entry as make_subcontract_transfer_entry, -) from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.item.test_item import make_item @@ -24,7 +21,6 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_pi_from_pr, ) -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestPurchaseOrder(FrappeTestCase): @@ -389,31 +385,6 @@ class TestPurchaseOrder(FrappeTestCase): new_item_with_tax.delete() frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() - def test_update_child_uom_conv_factor_change(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) - - trans_item = json.dumps( - [ - { - "item_code": po.get("items")[0].item_code, - "rate": po.get("items")[0].rate, - "qty": po.get("items")[0].qty, - "uom": "_Test UOM 1", - "conversion_factor": 2, - "docname": po.get("items")[0].name, - } - ] - ) - update_child_qty_rate("Purchase Order", trans_item, po.name) - po.reload() - - total_reqd_qty_after_change = sum( - d.get("required_qty") for d in po.as_dict().get("supplied_items") - ) - - self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) - def test_update_qty(self): po = create_purchase_order() @@ -572,10 +543,6 @@ class TestPurchaseOrder(FrappeTestCase): ) automatically_fetch_payment_terms(enable=0) - def test_subcontracting(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - self.assertEqual(len(po.get("supplied_items")), 2) - def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany @@ -740,379 +707,6 @@ class TestPurchaseOrder(FrappeTestCase): pi.insert() self.assertTrue(pi.get("payment_schedule")) - def test_reserved_qty_subcontract_po(self): - # Make stock available for raw materials - make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=30, - basic_rate=100, - ) - - bin1 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], - as_dict=1, - ) - - # Submit PO - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - - bin2 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], - as_dict=1, - ) - - self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) - self.assertNotEqual(bin1.modified, bin2.modified) - - # Create stock transfer - rm_item = [ - { - "item_code": "_Test FG Item", - "rm_item_code": "_Test Item", - "item_name": "_Test Item", - "qty": 6, - "warehouse": "_Test Warehouse - _TC", - "rate": 100, - "amount": 600, - "stock_uom": "Nos", - } - ] - rm_item_string = json.dumps(rm_item) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.to_warehouse = "_Test Warehouse 1 - _TC" - se.save() - se.submit() - - bin3 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # close PO - po.update_status("Closed") - bin4 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Re-open PO - po.update_status("Submitted") - bin5 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=40, - basic_rate=100, - ) - - # make Purchase Receipt against PO - pr = make_purchase_receipt(po.name) - pr.supplier_warehouse = "_Test Warehouse 1 - _TC" - pr.save() - pr.submit() - - bin6 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel PR - pr.cancel() - bin7 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Make Purchase Invoice - pi = make_pi_from_po(po.name) - pi.update_stock = 1 - pi.supplier_warehouse = "_Test Warehouse 1 - _TC" - pi.insert() - pi.submit() - bin8 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel PR - pi.cancel() - bin9 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Cancel Stock Entry - se.cancel() - bin10 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - - # Cancel PO - po.reload() - po.cancel() - bin11 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - def test_exploded_items_in_subcontracted(self): - item_code = "_Test Subcontracted FG Item 11" - make_subcontracted_item(item_code=item_code) - - po = create_purchase_order( - item_code=item_code, - qty=1, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - include_exploded_items=1, - ) - - name = frappe.db.get_value("BOM", {"item": item_code}, "name") - bom = frappe.get_doc("BOM", name) - - exploded_items = sorted( - [d.item_code for d in bom.exploded_items if not d.get("sourced_by_supplier")] - ) - supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) - self.assertEqual(exploded_items, supplied_items) - - po1 = create_purchase_order( - item_code=item_code, - qty=1, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - include_exploded_items=0, - ) - - supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items]) - bom_items = sorted([d.item_code for d in bom.items if not d.get("sourced_by_supplier")]) - - self.assertEqual(supplied_items1, bom_items) - - def test_backflush_based_on_stock_entry(self): - item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code=item_code) - make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1}) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 5 - po = create_purchase_order( - item_code=item_code, - qty=order_qty, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - ) - - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 1", - qty=10, - basic_rate=100, - ) - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 1", - "item_name": "_Test Item", - "qty": 10, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": "_Test Item Home Desktop 100", - "item_name": "_Test Item Home Desktop 100", - "qty": 20, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": "Test Extra Item 1", - "item_name": "Test Extra Item 1", - "qty": 10, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": "Test Extra Item 2", - "stock_uom": "Nos", - "qty": 10, - "warehouse": "_Test Warehouse - _TC", - "item_name": "Test Extra Item 2", - }, - ] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - pr = make_purchase_receipt(po.name) - - received_qty = 2 - # partial receipt - pr.get("items")[0].qty = received_qty - pr.save() - pr.submit() - - transferred_items = sorted( - [d.item_code for d in se.get("items") if se.purchase_order == po.name] - ) - issued_items = sorted([d.rm_item_code for d in pr.get("supplied_items")]) - - self.assertEqual(transferred_items, issued_items) - self.assertEqual(pr.get("items")[0].rm_supp_cost, 2000) - - transferred_rm_map = frappe._dict() - for item in rm_items: - transferred_rm_map[item.get("rm_item_code")] = item - - update_backflush_based_on("BOM") - - def test_supplied_qty_against_subcontracted_po(self): - item_code = "_Test Subcontracted FG Item 5" - make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1}) - - make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 250 - po = create_purchase_order( - item_code=item_code, - qty=order_qty, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - do_not_save=True, - ) - - # Add same subcontracted items multiple times - po.append( - "items", - { - "item_code": item_code, - "qty": order_qty, - "schedule_date": add_days(nowdate(), 1), - "warehouse": "_Test Warehouse - _TC", - }, - ) - - po.set_missing_values() - po.submit() - - # Material receipt entry for the raw materials which will be send to supplier - make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 4", - qty=500, - basic_rate=100, - ) - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 4", - "item_name": "_Test Item", - "qty": 250, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - "name": po.supplied_items[0].name, - }, - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 4", - "item_name": "_Test Item", - "qty": 250, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - ] - - # Raw Materials transfer entry from stores to supplier's warehouse - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - # Test po_detail field has value or not - for item_row in se.items: - self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name) - - po_doc = frappe.get_doc("Purchase Order", po.name) - for row in po_doc.supplied_items: - # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order - self.assertEqual(row.supplied_qty, 250.0) - - update_backflush_based_on("BOM") - def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -1211,50 +805,6 @@ def make_pr_against_po(po, received_qty=0): return pr -def make_subcontracted_item(**args): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - args = frappe._dict(args) - - if not frappe.db.exists("Item", args.item_code): - make_item( - args.item_code, - { - "is_stock_item": 1, - "is_sub_contracted_item": 1, - "has_batch_no": args.get("has_batch_no") or 0, - }, - ) - - if not args.raw_materials: - if not frappe.db.exists("Item", "Test Extra Item 1"): - make_item( - "Test Extra Item 1", - { - "is_stock_item": 1, - }, - ) - - if not frappe.db.exists("Item", "Test Extra Item 2"): - make_item( - "Test Extra Item 2", - { - "is_stock_item": 1, - }, - ) - - args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] - - if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): - make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - - -def update_backflush_based_on(based_on): - doc = frappe.get_doc("Buying Settings") - doc.backflush_raw_materials_of_subcontract_based_on = based_on - doc.save() - - def get_same_items(): return [ { @@ -1341,4 +891,4 @@ def get_requested_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC") test_dependencies = ["BOM", "Item Price"] -test_records = frappe.get_test_records("Purchase Order") +test_records = frappe.get_test_records("Purchase Order") \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/test_records.json b/erpnext/buying/doctype/purchase_order/test_records.json index 896050ce43a..4df994a68c6 100644 --- a/erpnext/buying/doctype/purchase_order/test_records.json +++ b/erpnext/buying/doctype/purchase_order/test_records.json @@ -1,38 +1,4 @@ [ - { - "advance_paid": 0.0, - "buying_price_list": "_Test Price List", - "company": "_Test Company", - "conversion_rate": 1.0, - "currency": "INR", - "doctype": "Purchase Order", - "base_grand_total": 5000.0, - "grand_total": 5000.0, - "is_subcontracted": 1, - "naming_series": "_T-Purchase Order-", - "base_net_total": 5000.0, - "items": [ - { - "base_amount": 5000.0, - "conversion_factor": 1.0, - "description": "_Test FG Item", - "doctype": "Purchase Order Item", - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "parentfield": "items", - "qty": 10.0, - "rate": 500.0, - "schedule_date": "2013-03-01", - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": "_Test Warehouse - _TC" - } - ], - "supplier": "_Test Supplier", - "supplier_name": "_Test Supplier", - "transaction_date": "2013-02-12", - "schedule_date": "2013-02-13" - }, { "advance_paid": 0.0, "buying_price_list": "_Test Price List", 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 7f797cfd2fe..12eef79dff9 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -574,7 +574,6 @@ "read_only": 1 }, { - "depends_on": "eval:parent.is_subcontracted", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", @@ -584,7 +583,6 @@ }, { "default": "0", - "depends_on": "eval:parent.is_subcontracted", "fieldname": "include_exploded_items", "fieldtype": "Check", "hidden": 1, From 34bda14b5b0707704dfd724a95b51e1e8b2bf2af Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 4 May 2022 15:21:31 +0530 Subject: [PATCH 17/41] refactor!: Buying Controller --- erpnext/controllers/buying_controller.py | 91 +----------------------- 1 file changed, 3 insertions(+), 88 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 6fdb002be03..398154e31be 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -12,7 +12,6 @@ from erpnext.accounts.party import get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController -from erpnext.controllers.subcontracting import Subcontracting from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import get_incoming_rate @@ -21,7 +20,7 @@ class QtyMismatchError(ValidationError): pass -class BuyingController(StockController, Subcontracting): +class BuyingController(StockController): def get_feed(self): if self.get("supplier_name"): return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) @@ -52,7 +51,6 @@ class BuyingController(StockController, Subcontracting): # sub-contracting self.validate_for_subcontracting() - self.create_raw_materials_supplied("supplied_items") self.set_landed_cost_voucher_amount() if self.doctype in ("Purchase Receipt", "Purchase Invoice"): @@ -253,11 +251,9 @@ class BuyingController(StockController, Subcontracting): ) qty_in_stock_uom = flt(item.qty * item.conversion_factor) - item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) item.valuation_rate = ( item.base_net_amount + item.item_tax_amount - + item.rm_supp_cost + flt(item.landed_cost_voucher_amount) ) / qty_in_stock_uom else: @@ -313,76 +309,15 @@ class BuyingController(StockController, Subcontracting): alert=1, ) - def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): - supplied_items_cost = 0.0 - for d in self.get("supplied_items"): - if d.reference_name == item_row_id: - if reset_outgoing_rate and frappe.get_cached_value("Item", d.rm_item_code, "is_stock_item"): - rate = get_incoming_rate( - { - "item_code": d.rm_item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * d.consumed_qty, - "serial_no": d.serial_no, - "batch_no": d.batch_no, - } - ) - - if rate > 0: - d.rate = rate - - d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) - supplied_items_cost += flt(d.amount) - - return supplied_items_cost - def validate_for_subcontracting(self): if self.is_subcontracted: if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype)) - - for item in self.get("items"): - if item in self.sub_contracted_items and not item.bom: - frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - - if self.doctype != "Purchase Order": - return - - for row in self.get("supplied_items"): - if not row.reserve_warehouse: - msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" - frappe.throw(_(msg)) else: for item in self.get("items"): - if item.bom: + if item.get("bom"): item.bom = None - def create_raw_materials_supplied(self, raw_material_table): - if self.is_subcontracted: - self.set_materials_for_subcontracted_items(raw_material_table) - - elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - for item in self.get("items"): - item.rm_supp_cost = 0.0 - - if not self.is_subcontracted and self.get("supplied_items"): - self.set("supplied_items", []) - - @property - def sub_contracted_items(self): - if not hasattr(self, "_sub_contracted_items"): - self._sub_contracted_items = [] - item_codes = list(set(item.item_code for item in self.get("items"))) - if item_codes: - items = frappe.get_all( - "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1} - ) - self._sub_contracted_items = [item.name for item in items] - - return self._sub_contracted_items - def set_qty_as_per_stock_uom(self): for d in self.get("items"): if d.meta.get_field("stock_qty"): @@ -502,7 +437,7 @@ class BuyingController(StockController, Subcontracting): sle.update( { "incoming_rate": incoming_rate, - "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0, + "recalculate_rate": 1 if (self.is_subcontracted and d.fg_item) or d.from_warehouse else 0, } ) sl_entries.append(sle) @@ -530,7 +465,6 @@ class BuyingController(StockController, Subcontracting): ) ) - self.make_sl_entries_for_supplier_warehouse(sl_entries) self.make_sl_entries( sl_entries, allow_negative_stock=allow_negative_stock, @@ -557,25 +491,6 @@ class BuyingController(StockController, Subcontracting): ) po_obj.update_ordered_qty(po_item_rows) - if self.is_subcontracted: - po_obj.update_reserved_qty_for_subcontract() - - def make_sl_entries_for_supplier_warehouse(self, sl_entries): - if hasattr(self, "supplied_items"): - for d in self.get("supplied_items"): - # negative quantity is passed, as raw material qty has to be decreased - # when PR is submitted and it has to be increased when PR is cancelled - sl_entries.append( - self.get_sl_entries( - d, - { - "item_code": d.rm_item_code, - "warehouse": self.supplier_warehouse, - "actual_qty": -1 * flt(d.consumed_qty), - "dependant_sle_voucher_detail_no": d.reference_name, - }, - ) - ) def on_submit(self): if self.get("is_return"): From 73484448f2e479cc469db23a6490a2b05dd42760 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 6 May 2022 10:48:34 +0530 Subject: [PATCH 18/41] refactor!: Stock Entry --- .../stock/doctype/stock_entry/stock_entry.js | 21 ++- .../doctype/stock_entry/stock_entry.json | 10 +- .../stock/doctype/stock_entry/stock_entry.py | 129 ++++++++++++------ 3 files changed, 106 insertions(+), 54 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a94087821a5..dc4d245d1aa 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -611,7 +611,21 @@ frappe.ui.form.on('Stock Entry', { apply_putaway_rule: function (frm) { if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); - } + }, + + subcontracting_order: (frm) => { + if (frm.doc.subcontracting_order) { + erpnext.utils.map_current_doc({ + method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', + source_name: frm.doc.subcontracting_order, + target_doc: frm, + freeze: true, + }); + } + else { + frm.set_value("items", []); + } + }, }); frappe.ui.form.on('Stock Entry Detail', { @@ -789,11 +803,10 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle return erpnext.queries.item({is_stock_item: 1}); }; - this.frm.set_query("purchase_order", function() { + this.frm.set_query("subcontracting_order", function() { return { "filters": { "docstatus": 1, - "is_subcontracted": 1, "company": me.frm.doc.company } }; @@ -814,7 +827,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle } } - this.frm.add_fetch("purchase_order", "supplier", "supplier"); + this.frm.add_fetch("subcontracting_order", "supplier", "supplier"); frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' } this.frm.set_query("supplier_address", erpnext.queries.address_query) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 7b9eccd4aa6..db505eabf2c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -148,11 +148,11 @@ "search_index": 1 }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "purchase_order", - "fieldtype": "Link", - "label": "Purchase Order", - "options": "Purchase Order" + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order", + "read_only": 1 }, { "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5adb8b273ef..ac9ce8718a7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -867,15 +867,19 @@ class StockEntry(StockController): 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`.subcontracting_order = %s - and `tabStock Entry`.docstatus = 1 - and `tabStock Entry Detail`.item_code = %s - and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", - (self.subcontracting_order, se_item.item_code), - )[0][0] + + parent = frappe.qb.DocType("Stock Entry") + child = frappe.qb.DocType("Stock Entry Detail") + + total_supplied = ( + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) + .select(Sum(child.transfer_qty)) + .where(parent.docstatus == 1) + .where(parent.subcontracting_order == self.subcontracting_order) + .where(child.item_code == se_item.item_code) + ).run()[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): frappe.throw( @@ -1261,11 +1265,13 @@ class StockEntry(StockController): args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]) if ( - self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code") + self.purpose == "Send to Subcontractor" + and self.get("subcontracting_order") + and args.get("item_code") ): subcontract_items = frappe.get_all( - "Purchase Order Item Supplied", - {"parent": self.purchase_order, "rm_item_code": args.get("item_code")}, + "Subcontracting Order Supplied Item", + {"parent": self.subcontracting_order, "rm_item_code": args.get("item_code")}, "main_item_code", ) @@ -1359,27 +1365,27 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - # Get PO Supplied Items Details - if self.purchase_order and self.purpose == "Send to Subcontractor": - # Get PO 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, - ) - ) + # Get SCO Supplied Items Details + if self.subcontracting_order and self.purpose == "Send to Subcontractor": + # Get SCO Supplied Items Details + parent = frappe.qb.DocType("Subcontracting Order") + child = frappe.qb.DocType("Subcontracting Order Supplied Item") + + item_wh = ( + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) + .select(child.rm_item_code, child.reserve_warehouse) + .where(parent.name == self.subcontracting_order) + ).run(as_list=True) + + item_wh = frappe._dict(item_wh) for item in item_dict.values(): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse - # Get Reserve Warehouse from PO - if self.purchase_order and self.purpose == "Send to Subcontractor": + # Get Reserve Warehouse from SCO + if self.subcontracting_order and self.purpose == "Send to Subcontractor": item["from_warehouse"] = item_wh.get(item.item_code) item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" @@ -1996,20 +2002,21 @@ class StockEntry(StockController): def update_subcontracting_order_supplied_items(self): if self.subcontracting_order and ( - self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return + self.purpose in ["Send to Subcontractor", "Material Transfer"] ): # Get SCO Supplied Items Details - item_wh = frappe._dict( - frappe.db.sql( - """ - select rm_item_code, reserve_warehouse - from `tabSubcontracting Order` sco, `tabSubcontracting Order Supplied Item` scoitemsup - where sco.name = scoitemsup.parent - and sco.name = %s""", - self.subcontracting_order, - ) - ) + parent = frappe.qb.DocType("Subcontracting Order") + child = frappe.qb.DocType("Subcontracting Order Supplied Item") + item_wh = ( + frappe.qb.from_(parent) + .inner_join(child) + .on(parent.name == child.parent) + .select(child.rm_item_code, child.reserve_warehouse) + .where(parent.name == self.subcontracting_order) + ).run(as_list=True) + + item_wh = frappe._dict(item_wh) supplied_items = get_supplied_items(self.subcontracting_order) for name, item in supplied_items.items(): @@ -2363,12 +2370,12 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): return operating_cost_per_unit -def get_used_alternative_items(purchase_order=None, work_order=None): +def get_used_alternative_items(subcontracting_order=None, work_order=None): cond = "" - if purchase_order: - cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format( - purchase_order + if subcontracting_order: + cond = "and ste.purpose = 'Send to Subcontractor' and ste.subcontracting_order = '{0}'".format( + subcontracting_order ) elif work_order: cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( @@ -2422,7 +2429,6 @@ def get_valuation_rate_for_finished_good_entry(work_order): @frappe.whitelist() def get_uom_details(item_code, uom, qty): """Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}` - :param args: dict with `item_code`, `uom` and `qty`""" conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") @@ -2542,3 +2548,36 @@ def get_supplied_items(subcontracting_order): ) return supplied_item_details + + +@frappe.whitelist() +def get_items_from_subcontracting_order(source_name, target_doc=None): + sco = frappe.get_doc("Subcontracting Order", source_name) + + if sco.docstatus == 1: + if target_doc and isinstance(target_doc, str): + target_doc = frappe.get_doc(json.loads(target_doc)) + + if target_doc.items: + target_doc.items = [] + + warehouses = {} + for item in sco.items: + warehouses[item.name] = item.warehouse + + for item in sco.supplied_items: + target_doc.append( + "items", + { + "s_warehouse": warehouses.get(item.reference_name), + "t_warehouse": sco.supplier_warehouse, + "item_code": item.rm_item_code, + "qty": item.required_qty, + "transfer_qty": item.required_qty, + "uom": item.stock_uom, + "stock_uom": item.stock_uom, + "conversion_factor": 1, + }, + ) + + return target_doc From f09fc46059731de85a16c752f78f146994769453 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 6 May 2022 17:32:03 +0530 Subject: [PATCH 19/41] refactor!: Purchase Receipt --- .../purchase_receipt/purchase_receipt.js | 3 - .../purchase_receipt/purchase_receipt.py | 13 - .../purchase_receipt/test_purchase_receipt.py | 276 ------------------ .../purchase_receipt/test_records.json | 32 -- .../purchase_receipt_item.json | 3 +- 5 files changed, 2 insertions(+), 325 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 51ec598f726..36948d0add7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -298,9 +298,6 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt frappe.provide("erpnext.buying"); frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) { - if (frm.doc.is_subcontracted) { - erpnext.buying.get_default_bom(frm); - } frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1e1c0b9f7c7..0e774426a25 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -231,7 +231,6 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() - self.set_consumed_qty_in_po() def check_next_docstatus(self): submit_rv = frappe.db.sql( @@ -267,18 +266,6 @@ class PurchaseReceipt(BuyingController): self.repost_future_sle_and_gle() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.delete_auto_created_batches() - self.set_consumed_qty_in_po() - - @frappe.whitelist() - def get_current_stock(self): - for d in self.get("supplied_items"): - if self.supplier_warehouse: - bin = frappe.db.sql( - "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", - (d.rm_item_code, self.supplier_warehouse), - as_dict=1, - ) - d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0 def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index bfbdd562921..05021ce057a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2,10 +2,6 @@ # License: GNU General Public License v3. See license.txt -import json -import unittest -from collections import defaultdict - import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today @@ -311,142 +307,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) - def test_subcontracting(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) - - make_stock_entry( - item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop 100", - qty=100, - target="_Test Warehouse 1 - _TC", - basic_rate=100, - ) - pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted=1) - self.assertEqual(len(pr.get("supplied_items")), 2) - - rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) - self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) - - pr.cancel() - - def test_subcontracting_gle_fg_item_rate_zero(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) - - se1 = make_stock_entry( - item_code="_Test Item", - target="Work In Progress - TCP1", - qty=100, - basic_rate=100, - company="_Test Company with perpetual inventory", - ) - - se2 = make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="Work In Progress - TCP1", - qty=100, - basic_rate=100, - company="_Test Company with perpetual inventory", - ) - - pr = make_purchase_receipt( - item_code="_Test FG Item", - qty=10, - rate=0, - is_subcontracted=1, - company="_Test Company with perpetual inventory", - warehouse="Stores - TCP1", - supplier_warehouse="Work In Progress - TCP1", - ) - - gl_entries = get_gl_entries("Purchase Receipt", pr.name) - - self.assertFalse(gl_entries) - - pr.cancel() - se1.cancel() - se2.cancel() - - def test_subcontracting_over_receipt(self): - """ - Behaviour: Raise multiple PRs against one PO that in total - receive more than the required qty in the PO. - Expected Result: Error Raised for Over Receipt against PO. - """ - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt - from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_rm_stock_entry as make_subcontract_transfer_entry, - ) - from erpnext.buying.doctype.purchase_order.test_purchase_order import ( - create_purchase_order, - make_subcontracted_item, - update_backflush_based_on, - ) - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - update_backflush_based_on("Material Transferred for Subcontract") - item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code=item_code) - - po = create_purchase_order( - item_code=item_code, - qty=1, - include_exploded_items=0, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - ) - - # stock raw materials in a warehouse before transfer - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 - ) - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": po.supplied_items[0].rm_item_code, - "item_name": "_Test FG Item", - "qty": po.supplied_items[0].required_qty, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": po.supplied_items[1].rm_item_code, - "item_name": "Test Extra Item 1", - "qty": po.supplied_items[1].required_qty, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - ] - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.to_warehouse = "_Test Warehouse 1 - _TC" - se.save() - se.submit() - - pr1 = make_purchase_receipt(po.name) - pr2 = make_purchase_receipt(po.name) - - pr1.submit() - self.assertRaises(frappe.ValidationError, pr2.submit) - frappe.db.rollback() - def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr_row_1_serial_no = pr.get("items")[0].serial_no @@ -1095,103 +955,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() pr1.cancel() - def test_subcontracted_pr_for_multi_transfer_batches(self): - from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_purchase_receipt, - make_rm_stock_entry, - ) - from erpnext.buying.doctype.purchase_order.test_purchase_order import ( - create_purchase_order, - update_backflush_based_on, - ) - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - update_backflush_based_on("Material Transferred for Subcontract") - item_code = "_Test Subcontracted FG Item 3" - - make_item( - "Sub Contracted Raw Material 3", - {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1}, - ) - - create_subcontracted_item( - item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"] - ) - - order_qty = 500 - po = create_purchase_order( - item_code=item_code, - qty=order_qty, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - ) - - ste1 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 3", - qty=300, - basic_rate=100, - ) - ste2 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 3", - qty=200, - basic_rate=100, - ) - - transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200} - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 3", - "item_name": "_Test Item", - "qty": 300, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - "name": po.supplied_items[0].name, - }, - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 3", - "item_name": "_Test Item", - "qty": 200, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - "name": po.supplied_items[0].name, - }, - ] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) - self.assertEqual(len(se.items), 2) - se.items[0].batch_no = ste1.items[0].batch_no - se.items[1].batch_no = ste2.items[0].batch_no - se.submit() - - supplied_qty = frappe.db.get_value( - "Purchase Order Item Supplied", - {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, - "supplied_qty", - ) - - self.assertEqual(supplied_qty, 500.00) - - pr = make_purchase_receipt(po.name) - pr.save() - self.assertEqual(len(pr.supplied_items), 2) - - for row in pr.supplied_items: - self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) - - update_backflush_based_on("BOM") - - pr.delete() - se.cancel() - ste2.cancel() - ste1.cancel() - po.cancel() - def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO @@ -1520,44 +1283,5 @@ def make_purchase_receipt(**args): pr.submit() return pr - -def create_subcontracted_item(**args): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - args = frappe._dict(args) - - if not frappe.db.exists("Item", args.item_code): - make_item( - args.item_code, - { - "is_stock_item": 1, - "is_sub_contracted_item": 1, - "has_batch_no": args.get("has_batch_no") or 0, - }, - ) - - if not args.raw_materials: - if not frappe.db.exists("Item", "Test Extra Item 1"): - make_item( - "Test Extra Item 1", - { - "is_stock_item": 1, - }, - ) - - if not frappe.db.exists("Item", "Test Extra Item 2"): - make_item( - "Test Extra Item 2", - { - "is_stock_item": 1, - }, - ) - - args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] - - if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): - make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - - test_dependencies = ["BOM", "Item Price", "Location"] test_records = frappe.get_test_records("Purchase Receipt") diff --git a/erpnext/stock/doctype/purchase_receipt/test_records.json b/erpnext/stock/doctype/purchase_receipt/test_records.json index 990ad12b30e..e7ea9af6b9d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_records.json +++ b/erpnext/stock/doctype/purchase_receipt/test_records.json @@ -83,37 +83,5 @@ } ], "supplier": "_Test Supplier" - }, - - { - "buying_price_list": "_Test Price List", - "company": "_Test Company", - "conversion_rate": 1.0, - "currency": "INR", - "doctype": "Purchase Receipt", - "base_grand_total": 5000.0, - "is_subcontracted": 1, - "base_net_total": 5000.0, - "items": [ - { - "base_amount": 5000.0, - "conversion_factor": 1.0, - "description": "_Test FG Item", - "doctype": "Purchase Receipt Item", - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "parentfield": "items", - "qty": 10.0, - "rate": 500.0, - "received_qty": 10.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": "_Test Warehouse - _TC", - "cost_center": "Main - _TC" - } - ], - "supplier": "_Test Supplier", - "supplier_warehouse": "_Test Warehouse - _TC" } ] \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 03a4201ce5c..0e0605f00b4 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -644,7 +644,8 @@ "label": "BOM", "no_copy": 1, "options": "BOM", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "default": "0", From 5fa3f58c06af5679abaf143479e9ed00b325bbf9 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 9 May 2022 17:19:40 +0530 Subject: [PATCH 20/41] refactor!: Purchase Invoice --- .../purchase_invoice/purchase_invoice.js | 7 --- .../purchase_invoice/purchase_invoice.json | 2 +- .../purchase_invoice/purchase_invoice.py | 2 - .../purchase_invoice/test_purchase_invoice.py | 55 ------------------- .../purchase_invoice_item.json | 3 +- 5 files changed, 3 insertions(+), 66 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ee29d2a7448..696e534a8ee 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -570,13 +570,6 @@ frappe.ui.form.on("Purchase Invoice", { }); }, - is_subcontracted: function(frm) { - if (frm.doc.is_subcontracted) { - erpnext.buying.get_default_bom(frm); - } - frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); - }, - update_stock: function(frm) { hide_fields(frm.doc); frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 9f87c5ab54e..181dcc34de4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1365,7 +1365,7 @@ "width": "50px" }, { - "depends_on": "eval:doc.update_stock && doc.is_subcontracted", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index e6a46d0676b..39a53d743c9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -513,7 +513,6 @@ class PurchaseInvoice(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: self.update_stock_ledger() - self.set_consumed_qty_in_po() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") @@ -1403,7 +1402,6 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.update_stock_ledger() self.delete_auto_created_batches() - self.set_consumed_qty_in_po() self.make_gl_entries_on_cancel() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 73390dd6f45..67d46958679 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -468,37 +468,6 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.total, expected_values[i][2]) - def test_purchase_invoice_with_subcontracted_item(self): - wrapper = frappe.copy_doc(test_records[0]) - wrapper.get("items")[0].item_code = "_Test FG Item" - wrapper.insert() - wrapper.load_from_db() - - expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]] - for i, item in enumerate(wrapper.get("items")): - self.assertEqual(item.item_code, expected_values[i][0]) - self.assertEqual(item.item_tax_amount, expected_values[i][1]) - self.assertEqual(item.valuation_rate, expected_values[i][2]) - - self.assertEqual(wrapper.base_net_total, 1250) - - # tax amounts - expected_values = [ - ["_Test Account Shipping Charges - _TC", 100, 1350], - ["_Test Account Customs Duty - _TC", 125, 1350], - ["_Test Account Excise Duty - _TC", 140, 1490], - ["_Test Account Education Cess - _TC", 2.8, 1492.8], - ["_Test Account S&H Education Cess - _TC", 1.4, 1494.2], - ["_Test Account CST - _TC", 29.88, 1524.08], - ["_Test Account VAT - _TC", 156.25, 1680.33], - ["_Test Account Discount - _TC", 168.03, 1512.30], - ] - - for i, tax in enumerate(wrapper.get("taxes")): - self.assertEqual(tax.account_head, expected_values[i][0]) - self.assertEqual(tax.tax_amount, expected_values[i][1]) - self.assertEqual(tax.total, expected_values[i][2]) - def test_purchase_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -885,30 +854,6 @@ class TestPurchaseInvoice(unittest.TestCase): pi.cancel() self.assertEqual(actual_qty_0, get_qty_after_transaction()) - def test_subcontracting_via_purchase_invoice(self): - from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - update_backflush_based_on("BOM") - make_stock_entry( - item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="_Test Warehouse 1 - _TC", - qty=100, - basic_rate=100, - ) - - pi = make_purchase_invoice( - item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1 - ) - - self.assertEqual(len(pi.get("supplied_items")), 2) - - rm_supp_cost = sum(d.amount for d in pi.get("supplied_items")) - self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) - def test_rejected_serial_no(self): pi = make_purchase_invoice( item_code="_Test Serialized Item With Series", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 6651195e5f2..dd62886d965 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -619,7 +619,8 @@ "fieldname": "bom", "fieldtype": "Link", "label": "BOM", - "options": "BOM" + "options": "BOM", + "read_only": 1 }, { "default": "0", From 3469560105d0e3fee72a90d7f24b10476e380dba Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 11 May 2022 09:17:07 +0530 Subject: [PATCH 21/41] refactor!: Accounts Controller --- erpnext/controllers/accounts_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8a9318e184e..1790a0e2ecf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2586,10 +2586,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() - if parent.is_subcontracted: - parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied("supplied_items") - parent.save() else: parent.update_reserved_qty() parent.update_project() From 8486bbf31a96ead1653f23ac4d9f3e7dd78078ef Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 13 May 2022 14:40:14 +0530 Subject: [PATCH 22/41] refactor!: Subcontract Order Summary --- .../subcontract_order_summary.js | 14 ++--- .../subcontract_order_summary.json | 2 +- .../subcontract_order_summary.py | 57 +++++++++---------- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js index 6889322fb93..976ff60440e 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -14,32 +14,32 @@ frappe.query_reports["Subcontract Order Summary"] = { }, { label: __("From Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { label: __("To Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Date", default: frappe.datetime.get_today(), reqd: 1 }, { - label: __("Purchase Order"), + label: __("Subcontracting Order"), fieldname: "name", fieldtype: "Link", - options: "Purchase Order", - get_query: function() { + options: "Subcontracting Order", + get_query: function () { return { filters: { docstatus: 1, - is_subcontracted: 1, + company: frappe.query_report.get_filter_value('company') } } } } ] -}; +}; \ No newline at end of file diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json index 526a8d8ad01..7861e49ccf8 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json @@ -15,7 +15,7 @@ "name": "Subcontract Order Summary", "owner": "Administrator", "prepared_report": 0, - "ref_doctype": "Purchase Order", + "ref_doctype": "Subcontracting Order", "report_name": "Subcontract Order Summary", "report_type": "Script Report", "roles": [ diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 3d666375764..3750daa71ea 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -20,34 +20,33 @@ def get_data(report_filters): if orders: supplied_items = get_supplied_items(orders, report_filters) - po_details = prepare_subcontracted_data(orders, supplied_items) - get_subcontracted_data(po_details, data) + sco_details = prepare_subcontracted_data(orders, supplied_items) + get_subcontracted_data(sco_details, data) return data def get_subcontracted_orders(report_filters): fields = [ - "`tabPurchase Order Item`.`parent` as po_id", - "`tabPurchase Order Item`.`item_code`", - "`tabPurchase Order Item`.`item_name`", - "`tabPurchase Order Item`.`qty`", - "`tabPurchase Order Item`.`name`", - "`tabPurchase Order Item`.`received_qty`", - "`tabPurchase Order`.`status`", + "`tabSubcontracting Order Item`.`parent` as sco_id", + "`tabSubcontracting Order Item`.`item_code`", + "`tabSubcontracting Order Item`.`item_name`", + "`tabSubcontracting Order Item`.`qty`", + "`tabSubcontracting Order Item`.`name`", + "`tabSubcontracting Order Item`.`received_qty`", + "`tabSubcontracting Order`.`status`", ] filters = get_filters(report_filters) - return frappe.get_all("Purchase Order", fields=fields, filters=filters) or [] + return frappe.get_all("Subcontracting Order", fields=fields, filters=filters) or [] def get_filters(report_filters): filters = [ - ["Purchase Order", "docstatus", "=", 1], - ["Purchase Order", "is_subcontracted", "=", 1], + ["Subcontracting Order", "docstatus", "=", 1], [ - "Purchase Order", + "Subcontracting Order", "transaction_date", "between", (report_filters.from_date, report_filters.to_date), @@ -56,7 +55,7 @@ def get_filters(report_filters): for field in ["name", "company"]: if report_filters.get(field): - filters.append(["Purchase Order", field, "=", report_filters.get(field)]) + filters.append(["Subcontracting Order", field, "=", report_filters.get(field)]) return filters @@ -71,16 +70,15 @@ def get_supplied_items(orders, report_filters): "rm_item_code", "required_qty", "supplied_qty", - "returned_qty", "total_supplied_qty", "consumed_qty", "reference_name", ] - filters = {"parent": ("in", [d.po_id for d in orders]), "docstatus": 1} + filters = {"parent": ("in", [d.sco_id for d in orders]), "docstatus": 1} supplied_items = {} - for row in frappe.get_all("Purchase Order Item Supplied", fields=fields, filters=filters): + for row in frappe.get_all("Subcontracting Order Supplied Item", fields=fields, filters=filters): new_key = (row.parent, row.reference_name, row.main_item_code) supplied_items.setdefault(new_key, []).append(row) @@ -89,24 +87,24 @@ def get_supplied_items(orders, report_filters): def prepare_subcontracted_data(orders, supplied_items): - po_details = {} + sco_details = {} for row in orders: - key = (row.po_id, row.name, row.item_code) - if key not in po_details: - po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []})) + key = (row.sco_id, row.name, row.item_code) + if key not in sco_details: + sco_details.setdefault(key, frappe._dict({"sco_item": row, "supplied_items": []})) - details = po_details[key] + details = sco_details[key] if supplied_items.get(key): for supplied_item in supplied_items[key]: details["supplied_items"].append(supplied_item) - return po_details + return sco_details -def get_subcontracted_data(po_details, data): - for key, details in po_details.items(): - res = details.po_item +def get_subcontracted_data(sco_details, data): + for key, details in sco_details.items(): + res = details.sco_item for index, row in enumerate(details.supplied_items): if index != 0: res = {} @@ -118,10 +116,10 @@ def get_subcontracted_data(po_details, data): def get_columns(): return [ { - "label": _("Purchase Order"), - "fieldname": "po_id", + "label": _("Subcontracting Order"), + "fieldname": "sco_id", "fieldtype": "Link", - "options": "Purchase Order", + "options": "Subcontracting Order", "width": 100, }, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80}, @@ -144,5 +142,4 @@ def get_columns(): {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 110}, {"label": _("Supplied Qty"), "fieldname": "supplied_qty", "fieldtype": "Float", "width": 110}, {"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120}, - {"label": _("Returned Qty"), "fieldname": "returned_qty", "fieldtype": "Float", "width": 110}, ] From 3be663b121824e929320e57cba6462cec83e7092 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 13 May 2022 14:43:07 +0530 Subject: [PATCH 23/41] refactor!: Subcontracted Item To Be Received --- .../subcontracted_item_to_be_received.py | 27 +++++----- .../test_subcontracted_item_to_be_received.py | 50 ++++++++++++------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py index 2e90de66efe..30f9dec4d06 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py @@ -19,10 +19,10 @@ def execute(filters=None): def get_columns(): return [ { - "label": _("Purchase Order"), + "label": _("Subcontracting Order"), "fieldtype": "Link", - "fieldname": "purchase_order", - "options": "Purchase Order", + "fieldname": "subcontracting_order", + "options": "Subcontracting Order", "width": 150, }, {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150}, @@ -57,14 +57,14 @@ def get_columns(): def get_data(data, filters): - po = get_po(filters) - po_name = [v.name for v in po] - sub_items = get_purchase_order_item_supplied(po_name) + sco = get_sco(filters) + sco_name = [v.name for v in sco] + sub_items = get_subcontracting_order_item_supplied(sco_name) for item in sub_items: - for order in po: + for order in sco: if order.name == item.parent and item.received_qty < item.qty: row = { - "purchase_order": item.parent, + "subcontracting_order": item.parent, "date": order.transaction_date, "supplier": order.supplier, "fg_item_code": item.item_code, @@ -76,22 +76,21 @@ def get_data(data, filters): data.append(row) -def get_po(filters): +def get_sco(filters): record_filters = [ - ["is_subcontracted", "=", 1], ["supplier", "=", filters.supplier], ["transaction_date", "<=", filters.to_date], ["transaction_date", ">=", filters.from_date], ["docstatus", "=", 1], ] return frappe.get_all( - "Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"] + "Subcontracting Order", filters=record_filters, fields=["name", "transaction_date", "supplier"] ) -def get_purchase_order_item_supplied(po): +def get_subcontracting_order_item_supplied(sco): return frappe.get_all( - "Purchase Order Item", - filters=[("parent", "IN", po)], + "Subcontracting Order Item", + filters=[("parent", "IN", sco)], fields=["parent", "item_code", "item_name", "qty", "received_qty"], ) diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index 57f8741b5bf..80fd657f418 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -7,18 +7,35 @@ import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_to_be_received import ( execute, ) +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, +) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_subcontracting_receipt, +) class TestSubcontractedItemToBeReceived(FrappeTestCase): def test_pending_and_received_qty(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - transfer_param = [] + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 500, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC" + ) make_stock_entry( item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 ) @@ -28,28 +45,27 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): qty=100, basic_rate=100, ) - make_purchase_receipt_against_po(po.name) - po.reload() + make_subcontracting_receipt_against_sco(sco.name) + sco.reload() col, data = execute( filters=frappe._dict( { - "supplier": po.supplier, + "supplier": sco.supplier, "from_date": frappe.utils.get_datetime( - frappe.utils.add_to_date(po.transaction_date, days=-10) + frappe.utils.add_to_date(sco.transaction_date, days=-10) ), - "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)), + "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)), } ) ) self.assertEqual(data[0]["pending_qty"], 5) self.assertEqual(data[0]["received_qty"], 5) - self.assertEqual(data[0]["purchase_order"], po.name) - self.assertEqual(data[0]["supplier"], po.supplier) + self.assertEqual(data[0]["subcontracting_order"], sco.name) + self.assertEqual(data[0]["supplier"], sco.supplier) -def make_purchase_receipt_against_po(po, quantity=5): - pr = make_purchase_receipt(po) - pr.items[0].qty = quantity - pr.supplier_warehouse = "_Test Warehouse 1 - _TC" - pr.insert() - pr.submit() +def make_subcontracting_receipt_against_sco(sco, quantity=5): + scr = make_subcontracting_receipt(sco) + scr.items[0].qty = quantity + scr.insert() + scr.submit() From 05f05ab75b69bac8527e8651825a9e5f836b6c6d Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 13 May 2022 14:46:44 +0530 Subject: [PATCH 24/41] refactor!: Subcontracted Item To Be Transferred --- ...tracted_raw_materials_to_be_transferred.py | 27 ++++--- ...tracted_raw_materials_to_be_transferred.py | 76 ++++++++++--------- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index 6b8a3b140a7..a837b24357e 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -46,10 +46,10 @@ def get_columns(): def get_data(filters): - po_rm_item_details = get_po_items_to_supply(filters) + sco_rm_item_details = get_sco_items_to_supply(filters) data = [] - for row in po_rm_item_details: + for row in sco_rm_item_details: transferred_qty = row.get("transferred_qty") or 0 if transferred_qty < row.get("reqd_qty", 0): pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) @@ -59,23 +59,22 @@ def get_data(filters): return data -def get_po_items_to_supply(filters): +def get_sco_items_to_supply(filters): return frappe.db.get_all( - "Purchase Order", + "Subcontracting Order", fields=[ - "name as purchase_order", + "name as subcontracting_order", "transaction_date as date", "supplier as supplier", - "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code", - "`tabPurchase Order Item Supplied`.required_qty as reqd_qty", - "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty", + "`tabSubcontracting Order Supplied Item`.rm_item_code as rm_item_code", + "`tabSubcontracting Order Supplied Item`.required_qty as reqd_qty", + "`tabSubcontracting Order Supplied Item`.supplied_qty as transferred_qty", ], filters=[ - ["Purchase Order", "per_received", "<", "100"], - ["Purchase Order", "is_subcontracted", "=", 1], - ["Purchase Order", "supplier", "=", filters.supplier], - ["Purchase Order", "transaction_date", "<=", filters.to_date], - ["Purchase Order", "transaction_date", ">=", filters.from_date], - ["Purchase Order", "docstatus", "=", 1], + ["Subcontracting Order", "per_received", "<", "100"], + ["Subcontracting Order", "supplier", "=", filters.supplier], + ["Subcontracting Order", "transaction_date", "<=", filters.to_date], + ["Subcontracting Order", "transaction_date", ">=", filters.from_date], + ["Subcontracting Order", "docstatus", "=", 1], ], ) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 2791a26db78..d29791cebf6 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -3,24 +3,36 @@ # Compiled at: 2019-05-06 10:24:35 # Decompiled by https://python-decompiler.com -import json - import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import ( execute, ) +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, +) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_rm_stock_entry, +) class TestSubcontractedItemToBeTransferred(FrappeTestCase): def test_pending_and_transferred_qty(self): - po = create_purchase_order( - item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" - ) + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 500, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) # Material Receipt of RMs make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100) @@ -28,50 +40,47 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase): item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100 ) - se = transfer_subcontracted_raw_materials(po) + transfer_subcontracted_raw_materials(sco) col, data = execute( filters=frappe._dict( { - "supplier": po.supplier, + "supplier": sco.supplier, "from_date": frappe.utils.get_datetime( - frappe.utils.add_to_date(po.transaction_date, days=-10) + frappe.utils.add_to_date(sco.transaction_date, days=-10) ), - "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)), + "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)), } ) ) - po.reload() + sco.reload() - po_data = [row for row in data if row.get("purchase_order") == po.name] + sco_data = [row for row in data if row.get("subcontracting_order") == sco.name] # Alphabetically sort to be certain of order - po_data = sorted(po_data, key=lambda i: i["rm_item_code"]) + sco_data = sorted(sco_data, key=lambda i: i["rm_item_code"]) - self.assertEqual(len(po_data), 2) - self.assertEqual(po_data[0]["purchase_order"], po.name) + self.assertEqual(len(sco_data), 2) + self.assertEqual(sco_data[0]["subcontracting_order"], sco.name) - self.assertEqual(po_data[0]["rm_item_code"], "_Test Item") - self.assertEqual(po_data[0]["p_qty"], 8) - self.assertEqual(po_data[0]["transferred_qty"], 2) + self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item") + self.assertEqual(sco_data[0]["p_qty"], 8) + self.assertEqual(sco_data[0]["transferred_qty"], 2) - self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100") - self.assertEqual(po_data[1]["p_qty"], 19) - self.assertEqual(po_data[1]["transferred_qty"], 1) - - se.cancel() - po.cancel() + self.assertEqual(sco_data[1]["rm_item_code"], "_Test Item Home Desktop 100") + self.assertEqual(sco_data[1]["p_qty"], 19) + self.assertEqual(sco_data[1]["transferred_qty"], 1) -def transfer_subcontracted_raw_materials(po): - # Order of supplied items fetched in PO is flaky +def transfer_subcontracted_raw_materials(sco): + # Order of supplied items fetched in SCO is flaky transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1} - item_1 = po.supplied_items[0].rm_item_code - item_2 = po.supplied_items[1].rm_item_code + item_1 = sco.supplied_items[0].rm_item_code + item_2 = sco.supplied_items[1].rm_item_code - rm_item = [ + rm_items = [ { - "name": po.supplied_items[0].name, + "name": sco.supplied_items[0].name, "item_code": item_1, "rm_item_code": item_1, "item_name": item_1, @@ -82,7 +91,7 @@ def transfer_subcontracted_raw_materials(po): "stock_uom": "Nos", }, { - "name": po.supplied_items[1].name, + "name": sco.supplied_items[1].name, "item_code": item_2, "rm_item_code": item_2, "item_name": item_2, @@ -93,8 +102,7 @@ def transfer_subcontracted_raw_materials(po): "stock_uom": "Nos", }, ] - rm_item_string = json.dumps(rm_item) - se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) se.from_warehouse = "_Test Warehouse - _TC" se.to_warehouse = "_Test Warehouse - _TC" se.stock_entry_type = "Send to Subcontractor" From 92625902ad9f8b28d4d1cb203c4647984eff8ac5 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 13 May 2022 14:48:34 +0530 Subject: [PATCH 25/41] refactor!: BOM --- erpnext/manufacturing/doctype/bom/test_bom.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 62fc0724e03..e6d4e4446b4 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -9,14 +9,13 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.controllers.tests.test_subcontracting_controller import set_backflush_based_on from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.test_subcontracting import set_backflush_based_on test_records = frappe.get_test_records("BOM") test_dependencies = ["Item", "Quality Inspection Template"] @@ -249,12 +248,29 @@ class TestBOM(FrappeTestCase): bom.submit() # test that sourced_by_supplier rate is zero even after updating cost self.assertEqual(bom.items[2].rate, 0) - # test in Purchase Order sourced_by_supplier is not added to Supplied Item - po = create_purchase_order( - item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" + + from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, + ) + + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": 1, + }, + ] + # test in Subcontracting Order sourced_by_supplier is not added to Supplied Item + sco = get_subcontracting_order( + service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC" ) bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1]) - supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) + supplied_items = sorted([d.rm_item_code for d in sco.supplied_items]) self.assertEqual(bom_items, supplied_items) def test_bom_tree_representation(self): From 6c794afbe7d1aa253ed57dad78d1d4a99258d247 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 13 May 2022 14:50:12 +0530 Subject: [PATCH 26/41] refactor!: Item Alternative --- .../item_alternative/test_item_alternative.py | 61 ++++++++++--------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 32c58c5ae1d..3f66a6a6951 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -1,17 +1,15 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import json - import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import flt -from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_purchase_receipt, - make_rm_stock_entry, +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, + set_backflush_based_on, ) -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry @@ -19,6 +17,10 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_rm_stock_entry, + make_subcontracting_receipt, +) class TestItemAlternative(FrappeTestCase): @@ -27,9 +29,7 @@ class TestItemAlternative(FrappeTestCase): make_items() def test_alternative_item_for_subcontract_rm(self): - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) + set_backflush_based_on("BOM") create_stock_reconciliation( item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 @@ -39,15 +39,22 @@ class TestItemAlternative(FrappeTestCase): ) supplier_warehouse = "Test Supplier Warehouse - _TC" - po = create_purchase_order( - item="Test Finished Goods - A", - is_subcontracted=1, - qty=5, - rate=3000, - supplier_warehouse=supplier_warehouse, - ) - rm_item = [ + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 3000, + "fg_item": "Test Finished Goods - A", + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, supplier_warehouse=supplier_warehouse + ) + rm_items = [ { "item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 1", @@ -70,14 +77,13 @@ class TestItemAlternative(FrappeTestCase): }, ] - rm_item_string = json.dumps(rm_item) reserved_qty_for_sub_contract = frappe.db.get_value( "Bin", {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, "reserved_qty_for_sub_contract", ) - se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) se.to_warehouse = supplier_warehouse se.insert() @@ -101,22 +107,17 @@ class TestItemAlternative(FrappeTestCase): after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) ) - pr = make_purchase_receipt(po.name) - pr.save() + scr = make_subcontracting_receipt(sco.name) + scr.save() - pr = frappe.get_doc("Purchase Receipt", pr.name) + scr = frappe.get_doc("Subcontracting Receipt", scr.name) status = False - for d in pr.supplied_items: - if d.rm_item_code == "Alternate Item For A RW 1": + for item in scr.supplied_items: + if item.rm_item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) - frappe.db.set_value( - "Buying Settings", - None, - "backflush_raw_materials_of_subcontract_based_on", - "Material Transferred for Subcontract", - ) + set_backflush_based_on("Material Transferred for Subcontract") def test_alternative_item_for_production_rm(self): create_stock_reconciliation( From fcc09592b98ebb3e39cf0168d39f39172d5e0728 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 13 May 2022 14:51:49 +0530 Subject: [PATCH 27/41] refactor!: Stock Ledger Entry --- .../test_stock_ledger_entry.py | 55 ------------------- 1 file changed, 55 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 6561362c3af..7920b40c128 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -405,61 +405,6 @@ class TestStockLedgerEntry(FrappeTestCase): lcv.cancel() pr.cancel() - def test_sub_contracted_item_costing(self): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - company = "_Test Company" - rm_item_code = "_Test Item for Reposting" - subcontracted_item = "_Test Subcontracted Item for Reposting" - - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) - make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR") - - # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100 - pr = make_purchase_receipt( - company=company, - posting_date="2020-04-10", - warehouse="Stores - _TC", - item_code=rm_item_code, - qty=10, - rate=100, - ) - - # Purchase Receipt for subcontracted item - pr1 = make_purchase_receipt( - company=company, - posting_date="2020-04-20", - warehouse="Finished Goods - _TC", - supplier_warehouse="Stores - _TC", - item_code=subcontracted_item, - qty=10, - rate=20, - is_subcontracted=1, - ) - - self.assertEqual(pr1.items[0].valuation_rate, 120) - - # Update raw material's valuation via LCV, Additional cost = 50 - lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - - pr1.reload() - self.assertEqual(pr1.items[0].valuation_rate, 125) - - # check outgoing_rate for DN after reposting - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 125) - - # cleanup data - pr1.cancel() - lcv.cancel() - pr.cancel() - def test_back_dated_entry_not_allowed(self): # Back dated stock transactions are only allowed to stock managers frappe.db.set_value( From 323bdf85ce4398759d9bda456b5de18d3ee31d73 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 17 May 2022 15:14:07 +0530 Subject: [PATCH 28/41] feat: SL and GL reposting --- .../item_reposting_for_incorrect_sl_and_gl.py | 2 ++ erpnext/stock/stock_ledger.py | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index f6427ca55a6..75a5477be8f 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -15,6 +15,8 @@ def execute(): ("accounts", "sales_invoice_item"), ("accounts", "purchase_invoice_item"), ("buying", "purchase_receipt_item_supplied"), + ("subcontracting", "subcontracting_receipt_item"), + ("subcontracting", "subcontracting_receipt_supplied_item"), ] for module, doctype in doctypes_to_reload: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 3e0ddab6d3b..f716ff6d9b5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -619,6 +619,7 @@ class update_entries_after(object): "Purchase Invoice", "Delivery Note", "Sales Invoice", + "Subcontracting Receipt", ): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"): from erpnext.controllers.sales_and_purchase_return import ( @@ -635,6 +636,8 @@ class update_entries_after(object): else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" + elif sle.voucher_type == "Subcontracting Receipt": + rate_field = "rate" else: rate_field = "incoming_rate" @@ -648,6 +651,8 @@ class update_entries_after(object): else: if sle.voucher_type in ("Delivery Note", "Sales Invoice"): ref_doctype = "Packed Item" + elif sle == "Subcontracting Receipt": + ref_doctype = "Subcontracting Receipt Supplied Item" else: ref_doctype = "Purchase Receipt Item Supplied" @@ -673,6 +678,8 @@ class update_entries_after(object): self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate) elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): self.update_rate_on_purchase_receipt(sle, outgoing_rate) + elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt": + self.update_rate_on_subcontracting_receipt(sle, outgoing_rate) def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) @@ -714,12 +721,13 @@ class update_entries_after(object): "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate ) - # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice - if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): - doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) - doc.update_valuation_rate(reset_outgoing_rate=False) - for d in doc.items + doc.supplied_items: - d.db_update() + def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate): + if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): + frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate) + else: + frappe.db.set_value( + "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate + ) def get_serialized_values(self, sle): incoming_rate = flt(sle.incoming_rate) From e9b28452e4997055e505b6c4877cf4664ccb262f Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Wed, 18 May 2022 22:42:25 +0530 Subject: [PATCH 29/41] feat: SCR return --- .../controllers/sales_and_purchase_return.py | 48 +++++++++------ .../subcontracting_receipt.js | 2 +- .../test_subcontracting_receipt.py | 59 +++++++++++++++++++ 3 files changed, 91 insertions(+), 18 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 9642c24a9e0..ca968e939e5 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -77,7 +77,7 @@ def validate_returned_items(doc): if doc.doctype != "Purchase Invoice": select_fields += ",serial_no, batch_no" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql( @@ -161,7 +161,7 @@ def validate_returned_items(doc): def validate_quantity(doc, args, ref, valid_items, already_returned_items): fields = ["stock_qty"] - if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]: + if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]: fields.extend(["received_qty", "rejected_qty"]) already_returned_data = already_returned_items.get(args.item_code) or {} @@ -224,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row): if ref_item_row.get("rate", 0) > item_dict["rate"]: item_dict["rate"] = ref_item_row.get("rate", 0) - if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt"]: + if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: item_dict["received_qty"] += ref_item_row.received_qty item_dict["rejected_qty"] += ref_item_row.rejected_qty @@ -239,7 +239,7 @@ def get_ref_item_dict(valid_items, ref_item_row): def get_already_returned_items(doc): column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty, sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" @@ -281,17 +281,21 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) - if doctype in ("Purchase Receipt", "Purchase Invoice"): + if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): party_type = "supplier" else: party_type = "customer" fields = [ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), - "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype), ] - if doctype in ("Purchase Receipt", "Purchase Invoice"): + if doctype != "Subcontracting Receipt": + fields += [ + "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype), + ] + + if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): fields += [ "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), @@ -397,7 +401,7 @@ def make_return_doc(doctype, source_name, target_doc=None): if serial_nos: target_doc.serial_no = "\n".join(serial_nos) - if doctype == "Purchase Receipt": + if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype ) @@ -409,15 +413,24 @@ def make_return_doc(doctype, source_name, target_doc=None): ) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) - target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)) - target_doc.received_stock_qty = -1 * flt( - source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) - ) + if hasattr(target_doc, "stock_qty"): + target_doc.stock_qty = -1 * flt( + source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0) + ) + target_doc.received_stock_qty = -1 * flt( + source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) + ) - target_doc.purchase_order = source_doc.purchase_order - target_doc.purchase_order_item = source_doc.purchase_order_item - target_doc.rejected_warehouse = source_doc.rejected_warehouse - target_doc.purchase_receipt_item = source_doc.name + if doctype == "Subcontracting Receipt": + target_doc.subcontracting_order = source_doc.subcontracting_order + target_doc.subcontracting_order_item = source_doc.subcontracting_order_item + target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.subcontracting_receipt_item = source_doc.name + else: + target_doc.purchase_order = source_doc.purchase_order + target_doc.purchase_order_item = source_doc.purchase_order_item + target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": returned_qty_map = get_returned_qty_map_for_row( @@ -529,7 +542,7 @@ def get_rate_for_return( item_row, ) - if voucher_type in ("Purchase Receipt", "Purchase Invoice"): + if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): select_field = "incoming_rate" else: select_field = "abs(stock_value_difference / actual_qty)" @@ -564,6 +577,7 @@ def get_return_against_item_fields(voucher_type): "Purchase Invoice": "purchase_invoice_item", "Delivery Note": "dn_detail", "Sales Invoice": "sales_invoice_item", + "Subcontracting Receipt": "subcontracting_receipt_item", } return return_against_item_fields[voucher_type] diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index b98f979c668..87a19a1bf85 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -76,7 +76,7 @@ frappe.ui.form.on('Subcontracting Receipt', { }, __("View")); } - if (!frm.doc.is_return && frm.doc.docstatus == 1) { + if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { frm.add_custom_button('Subcontract Return', function () { frappe.model.open_mapped_doc({ method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return', diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 8680311c792..dd1790289ad 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -21,6 +21,7 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( set_backflush_based_on, ) from erpnext.stock.doctype.item.test_item import make_item +from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import make_subcontracting_receipt @@ -272,6 +273,64 @@ class TestSubcontractingReceipt(FrappeTestCase): for row in scr.supplied_items: self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) + def test_subcontracting_order_partial_return(self): + sco = get_subcontracting_order() + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + scr1.submit() + + scr1_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-3) + scr1.load_from_db() + self.assertEqual(scr1_return.status, "Return") + self.assertEqual(scr1.items[0].returned_qty, 3) + + scr2_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-7) + scr1.load_from_db() + self.assertEqual(scr2_return.status, "Return") + self.assertEqual(scr1.status, "Return Issued") + self.assertEqual(scr1.items[0].returned_qty, 10) + + def test_subcontracting_order_over_return(self): + sco = get_subcontracting_order() + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + scr1.submit() + + from erpnext.controllers.status_updater import OverAllowanceError + args = frappe._dict(scr_name=scr1.name, qty=-15) + self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args) + + +def make_return_subcontracting_receipt(**args): + args = frappe._dict(args) + return_doc = make_return_doc("Subcontracting Receipt", args.scr_name) + return_doc.supplier_warehouse = args.supplier_warehouse or args.warehouse or "_Test Warehouse 1 - _TC" + + if args.qty: + for item in return_doc.items: + item.qty = args.qty + + if not args.do_not_save: + return_doc.save() + if not args.do_not_submit: + return_doc.submit() + + return_doc.load_from_db() + return return_doc def get_items(**args): args = frappe._dict(args) From 61296a0658299538c81f4418f580d3e0381e3648 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 26 May 2022 15:08:16 +0530 Subject: [PATCH 30/41] fix: Subcontracting through Production Plan --- .../manufacturing/doctype/production_plan/production_plan.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 9ca05b927f3..132e1ebea50 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -506,7 +506,7 @@ class ProductionPlan(Document): po.is_subcontracted = 1 for row in po_list: po_data = { - "item_code": row.production_item, + "fg_item": row.production_item, "warehouse": row.fg_warehouse, "production_plan_sub_assembly_item": row.name, "bom": row.bom_no, @@ -516,9 +516,6 @@ class ProductionPlan(Document): for field in [ "schedule_date", "qty", - "uom", - "stock_uom", - "item_name", "description", "production_plan_item", ]: From ca9d55a2fdc8828863f412d99f40225a8c1f6f72 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 14 Jun 2022 13:11:46 +0530 Subject: [PATCH 31/41] chore: update err msg for FG Item in PO --- .../buying/doctype/purchase_order/purchase_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index e0cc43ff679..dd10c93b1d5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -196,21 +196,21 @@ class PurchaseOrder(BuyingController): for item in self.items: if not item.fg_item: frappe.throw( - _("Finished Good Item is not specified for service item {0} at row {1}").format( - item.item_code, item.idx + _("Row #{0}: Finished Good Item is not specified for service item {1}").format( + item.idx, item.item_code ) ) 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) + "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}" + ).format(item.idx, item.fg_item, item.item_code) ) 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 + _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format( + item.idx, item.item_code ) ) From 5002f1f1e5e4823730d84c30c991c8ecc1de76ac Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 17 Jun 2022 15:43:59 +0530 Subject: [PATCH 32/41] feat: Add hidden field "is_old_subcontracting_flow" in PO, PR and PI --- .../purchase_invoice/purchase_invoice.json | 15 ++++++++++++--- .../doctype/purchase_order/purchase_order.json | 13 +++++++++++-- erpnext/patches.txt | 1 + ...tracted_value_to_is_old_subcontracting_flow.py | 12 ++++++++++++ .../purchase_receipt/purchase_receipt.json | 13 +++++++++++-- 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 181dcc34de4..1c5c0609f1c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -169,7 +169,8 @@ "column_break_114", "auto_repeat", "update_auto_repeat_reference", - "per_received" + "per_received", + "is_old_subcontracting_flow" ], "fields": [ { @@ -1416,13 +1417,21 @@ "label": "Advance Tax", "options": "Advance Tax", "read_only": 1 - } + }, + { + "default": "0", + "fieldname": "is_old_subcontracting_flow", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Old Subcontracting Flow", + "read_only": 1 + } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-11-25 13:31:02.716727", + "modified": "2022-06-15 15:40:58.527065", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 07320d0a0ad..b622b4f1be8 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -141,7 +141,8 @@ "party_account_currency", "is_internal_supplier", "represents_company", - "inter_company_order_reference" + "inter_company_order_reference", + "is_old_subcontracting_flow" ], "fields": [ { @@ -1161,13 +1162,21 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "default": "0", + "fieldname": "is_old_subcontracting_flow", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Old Subcontracting Flow", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-04-26 18:46:58.863174", + "modified": "2022-06-15 15:40:58.527065", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5a984635fdc..51038a5b39d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -374,3 +374,4 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.set_payroll_entry_status erpnext.patches.v13_0.job_card_status_on_hold +erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow diff --git a/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py new file mode 100644 index 00000000000..607ef69538e --- /dev/null +++ b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + + +def execute(): + for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: + tab = frappe.qb.DocType(doctype).as_("tab") + frappe.qb.update(tab).set(tab.is_old_subcontracting_flow, 1).where( + tab.is_subcontracted == 1 + ).run() diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 923ceb36cd7..b3d38858d09 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -133,7 +133,8 @@ "transporter_name", "column_break5", "lr_no", - "lr_date" + "lr_date", + "is_old_subcontracting_flow" ], "fields": [ { @@ -1142,13 +1143,21 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_old_subcontracting_flow", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Old Subcontracting Flow", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-05-27 15:59:18.550583", + "modified": "2022-06-15 15:43:40.664382", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From ca24b5287e8d8b4d4da6bc6661aaccc94997a9f2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 17 Jun 2022 16:25:18 +0530 Subject: [PATCH 33/41] chore: make "is_subcontracted" field read-only in PR and PI --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 3 ++- erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 1c5c0609f1c..534b879e783 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -548,7 +548,8 @@ "fieldname": "is_subcontracted", "fieldtype": "Check", "label": "Is Subcontracted", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "items_section", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index b3d38858d09..a70415dfc36 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -443,7 +443,8 @@ "label": "Is Subcontracted", "oldfieldname": "is_subcontracted", "oldfieldtype": "Select", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "depends_on": "eval:doc.is_subcontracted", From 6d89b2fa28f4f027c9eb6b3189d73fe3904f65f4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 18 Jun 2022 15:46:59 +0530 Subject: [PATCH 34/41] refactor: backport old subcontracting code --- .../purchase_invoice/purchase_invoice.js | 4 + .../purchase_invoice/purchase_invoice.py | 7 + .../purchase_invoice_item.json | 9 +- .../doctype/purchase_order/purchase_order.js | 225 +++++++++- .../purchase_order/purchase_order.json | 4 - .../doctype/purchase_order/purchase_order.py | 43 +- .../purchase_order_dashboard.py | 2 +- .../purchase_order_item.json | 16 +- .../subcontract_order_summary.js | 20 +- .../subcontract_order_summary.py | 68 +-- .../subcontracted_item_to_be_received.js | 7 + .../subcontracted_item_to_be_received.json | 2 +- .../subcontracted_item_to_be_received.py | 36 +- .../test_subcontracted_item_to_be_received.py | 3 +- ...tracted_raw_materials_to_be_transferred.js | 7 + ...acted_raw_materials_to_be_transferred.json | 2 +- ...tracted_raw_materials_to_be_transferred.py | 51 ++- ...tracted_raw_materials_to_be_transferred.py | 9 +- erpnext/controllers/accounts_controller.py | 4 + erpnext/controllers/buying_controller.py | 50 ++- .../controllers/subcontracting_controller.py | 404 ++++++++++++++---- .../tests/test_subcontracting_controller.py | 6 +- .../v13_0/add_bin_unique_constraint.py | 4 + erpnext/public/js/controllers/buying.js | 10 +- erpnext/public/js/controllers/transaction.js | 3 +- erpnext/public/js/utils.js | 6 +- erpnext/stock/doctype/bin/bin.py | 75 ++-- .../item_alternative/test_item_alternative.py | 2 +- .../purchase_receipt/purchase_receipt.js | 4 + .../purchase_receipt/purchase_receipt.py | 3 + .../purchase_receipt_item.json | 4 +- .../stock/doctype/stock_entry/stock_entry.js | 27 +- .../doctype/stock_entry/stock_entry.json | 4 +- .../stock/doctype/stock_entry/stock_entry.py | 186 +++++--- erpnext/stock/get_item_details.py | 9 +- erpnext/stock/stock_ledger.py | 7 + .../subcontracting_order.js | 18 +- .../subcontracting_order.py | 142 +----- .../test_subcontracting_order.py | 2 +- .../subcontracting_receipt.py | 17 +- .../test_subcontracting_receipt.py | 4 +- 41 files changed, 1047 insertions(+), 459 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 172ac9e0091..306d01dc1ff 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -572,6 +572,10 @@ frappe.ui.form.on("Purchase Invoice", { }, is_subcontracted: function(frm) { + if (frm.doc.is_old_subcontracting_flow) { + erpnext.buying.get_default_bom(frm); + } + frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); }, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5c8ab64300d..62818777563 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -513,6 +513,10 @@ class PurchaseInvoice(BuyingController): # because updating ordered qty in bin depends upon updated ordered qty in PO if self.update_stock == 1: self.update_stock_ledger() + + if self.is_old_subcontracting_flow: + self.set_consumed_qty_in_subcontract_order() + from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit update_serial_nos_after_submit(self, "items") @@ -1416,6 +1420,9 @@ class PurchaseInvoice(BuyingController): self.update_stock_ledger() self.delete_auto_created_batches() + if self.is_old_subcontracting_flow: + self.set_consumed_qty_in_subcontract_order() + self.make_gl_entries_on_cancel() if self.update_stock == 1: diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index dd62886d965..387b2cb3549 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -616,11 +616,13 @@ "search_index": 1 }, { + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", "options": "BOM", - "read_only": 1 + "read_only": 1, + "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow" }, { "default": "0", @@ -872,7 +874,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-11-15 17:04:07.191013", + "modified": "2022-06-15 16:02:15.196835", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -880,5 +882,6 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 6aba373bc64..c635a7fa71a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -7,6 +7,19 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Purchase Order", { setup: function(frm) { + + if (frm.doc.is_old_subcontracting_flow) { + frm.set_query("reserve_warehouse", "supplied_items", function() { + return { + filters: { + "company": frm.doc.company, + "name": ['!=', frm.doc.supplier_warehouse], + "is_group": 0 + } + } + }); + } + frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) @@ -19,7 +32,10 @@ frappe.ui.form.on("Purchase Order", { frm.set_query("fg_item", "items", function() { return { - filters: {'is_sub_contracted_item': 1} + filters: { + 'is_sub_contracted_item': 1, + 'default_bom': ['!=', ''] + } } }); }, @@ -28,6 +44,44 @@ frappe.ui.form.on("Purchase Order", { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, + refresh: function(frm) { + if(frm.doc.is_old_subcontracting_flow) + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function(frm) { + let po_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + po_details.push(d.name) + } + }); + } + + if (po_details && po_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.controllers.subcontracting_controller.get_materials_from_supplier', + freeze: true, + freeze_message: __('Creating Stock Entry'), + args: { + subcontract_order: frm.doc.name, + rm_details: po_details, + order_doctype: cur_frm.doc.doctype + }, + callback: function(r) { + if (r && r.message) { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } + }, + onload: function(frm) { set_schedule_date(frm); if (!frm.doc.transaction_date){ @@ -67,7 +121,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e 'Purchase Receipt': 'Purchase Receipt', 'Purchase Invoice': 'Purchase Invoice', 'Payment Entry': 'Payment', - 'Subcontracting Order': 'Subcontracting Order' + 'Subcontracting Order': 'Subcontracting Order', + 'Stock Entry': 'Material to Supplier' } super.setup(); @@ -138,7 +193,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if(flt(doc.per_received) < 100 && allow_receipt) { cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); if (doc.is_subcontracted) { - cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create')); + if (doc.is_old_subcontracting_flow) { + if (me.has_unsupplied_items()) { + cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer")); + } + } + else { + cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create')); + } } } if(flt(doc.per_billed) < 100) @@ -206,6 +268,143 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e set_schedule_date(this.frm); } + has_unsupplied_items() { + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + } + + make_stock_entry() { + var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; }); + var me = this; + + if(items.length >= 1){ + me.raw_material_data = []; + me.show_dialog = 1; + let title = __('Transfer Material to Supplier'); + let fields = [ + {fieldtype:'Section Break', label: __('Raw Materials')}, + {fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), + fields: [ + { + fieldtype:'Data', + fieldname:'item_code', + label: __('Item'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Data', + fieldname:'rm_item_code', + label: __('Raw Material'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Float', + read_only:1, + fieldname:'qty', + label: __('Quantity'), + read_only:1, + in_list_view:1 + }, + { + fieldtype:'Data', + read_only:1, + fieldname:'warehouse', + label: __('Reserve Warehouse'), + in_list_view:1 + }, + { + fieldtype:'Float', + read_only:1, + fieldname:'rate', + label: __('Rate'), + hidden:1 + }, + { + fieldtype:'Float', + read_only:1, + fieldname:'amount', + label: __('Amount'), + hidden:1 + }, + { + fieldtype:'Link', + read_only:1, + fieldname:'uom', + label: __('UOM'), + hidden:1 + } + ], + data: me.raw_material_data, + get_data: function() { + return me.raw_material_data; + } + } + ] + + me.dialog = new frappe.ui.Dialog({ + title: title, fields: fields + }); + + if (me.frm.doc['supplied_items']) { + me.frm.doc['supplied_items'].forEach((item, index) => { + if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { + me.raw_material_data.push ({ + 'name':item.name, + 'item_code': item.main_item_code, + 'rm_item_code': item.rm_item_code, + 'item_name': item.rm_item_code, + 'qty': item.required_qty - item.supplied_qty, + 'warehouse':item.reserve_warehouse, + 'rate':item.rate, + 'amount':item.amount, + 'stock_uom':item.stock_uom + }); + me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); + } + }) + } + + me.dialog.get_field('sub_con_rm_items').check_all_rows() + + me.dialog.show() + this.dialog.set_primary_action(__('Transfer'), function() { + me.values = me.dialog.get_values(); + if(me.values) { + me.values.sub_con_rm_items.map((row,i) => { + if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { + let row_id = i+1; + frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id])); + } + }) + me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()) + me.dialog.hide() + } + }); + } + + me.dialog.get_close_btn().on('click', () => { + me.dialog.hide(); + }); + + } + + _make_rm_stock_entry(rm_items) { + frappe.call({ + method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry", + args: { + subcontract_order: cur_frm.doc.name, + rm_items: rm_items, + order_doctype: cur_frm.doc.doctype + } + , + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + } + make_inter_company_order(frm) { frappe.model.open_mapped_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order", @@ -444,6 +643,20 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc, } } +if (cur_frm.doc.is_old_subcontracting_flow) { + cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) { + var d = locals[cdt][cdn] + return { + filters: [ + ['BOM', 'item', '=', d.item_code], + ['BOM', 'is_active', '=', '1'], + ['BOM', 'docstatus', '=', '1'], + ['BOM', 'company', '=', doc.company] + ] + } + } +} + function set_schedule_date(frm) { if(frm.doc.schedule_date){ erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "items", "schedule_date"); @@ -451,3 +664,9 @@ function set_schedule_date(frm) { } frappe.provide("erpnext.buying"); + +frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) { + if (frm.doc.is_old_subcontracting_flow) { + erpnext.buying.get_default_bom(frm); + } +}); \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index b622b4f1be8..aa50487d78e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -452,10 +452,6 @@ "options": "Warehouse", "print_hide": 1 }, - { - "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" - }, { "default": "0", "fieldname": "is_subcontracted", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 6cf5837d8bd..6f960a2c65e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -24,6 +24,7 @@ from erpnext.controllers.buying_controller import BuyingController from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.utils import get_bin form_grid_templates = {"items": "templates/form_grid/item_grid.html"} @@ -68,6 +69,11 @@ class PurchaseOrder(BuyingController): self.validate_with_previous_doc() self.validate_for_subcontracting() self.validate_minimum_order_qty() + + if self.is_old_subcontracting_flow: + self.validate_bom_for_subcontracting_items() + self.create_raw_materials_supplied() + self.validate_fg_item_for_subcontracting() self.set_received_qty_for_drop_ship_items() validate_inter_company_party( @@ -191,8 +197,17 @@ class PurchaseOrder(BuyingController): ).format(item_code, qty, itemwise_min_order_qty.get(item_code)) ) + def validate_bom_for_subcontracting_items(self): + for item in self.items: + if not item.bom: + frappe.throw( + _("Row #{0}: BOM is not specified for subcontracting item {0}").format( + item.idx, item.item_code + ) + ) + def validate_fg_item_for_subcontracting(self): - if self.is_subcontracted: + if self.is_subcontracted and not self.is_old_subcontracting_flow: for item in self.items: if not item.fg_item: frappe.throw( @@ -207,6 +222,10 @@ class PurchaseOrder(BuyingController): "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}" ).format(item.idx, item.fg_item, item.item_code) ) + elif 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) + ) if not item.fg_item_qty: frappe.throw( _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format( @@ -305,6 +324,7 @@ class PurchaseOrder(BuyingController): self.set_status(update=True, status=status) self.update_requested_qty() self.update_ordered_qty() + self.update_reserved_qty_for_subcontract() self.notify_update() clear_doctype_notifications(self) @@ -318,6 +338,7 @@ class PurchaseOrder(BuyingController): self.update_requested_qty() self.update_ordered_qty() self.validate_budget() + self.update_reserved_qty_for_subcontract() frappe.get_doc("Authorization Control").validate_approving_authority( self.doctype, self.company, self.base_grand_total @@ -337,6 +358,7 @@ class PurchaseOrder(BuyingController): if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() + self.update_reserved_qty_for_subcontract() self.check_on_hold_or_closed_status() frappe.db.set(self, "status", "Cancelled") @@ -406,6 +428,13 @@ class PurchaseOrder(BuyingController): if item.delivered_by_supplier == 1: item.received_qty = item.qty + def update_reserved_qty_for_subcontract(self): + if self.is_old_subcontracting_flow: + for d in self.supplied_items: + if d.rm_item_code: + stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order") + def update_receiving_percentage(self): total_qty, received_qty = 0.0, 0.0 for item in self.items: @@ -649,4 +678,16 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): target_doc.populate_items_table() + if target_doc.set_warehouse: + for item in target_doc.items: + item.warehouse = target_doc.set_warehouse + else: + source_doc = frappe.get_doc("Purchase Order", source_name) + if source_doc.set_warehouse: + for item in target_doc.items: + item.warehouse = source_doc.set_warehouse + else: + for idx, item in enumerate(target_doc.items): + item.warehouse = source_doc.items[idx].warehouse + 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 0c38c3e8da8..01b55c00d6b 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": ["Subcontracting Order"]}, + {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]}, ], } 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 12eef79dff9..4794104740f 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -574,18 +574,20 @@ "read_only": 1 }, { + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", "options": "BOM", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow" }, { "default": "0", + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "include_exploded_items", "fieldtype": "Check", - "hidden": 1, "label": "Include Exploded Items", "print_hide": 1 }, @@ -849,27 +851,27 @@ "print_hide": 1 }, { - "depends_on": "eval:parent.is_subcontracted", + "depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow", "fieldname": "fg_item", "fieldtype": "Link", "label": "Finished Good Item", - "mandatory_depends_on": "eval:parent.is_subcontracted", + "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow", "options": "Item" }, { "default": "1", - "depends_on": "eval:parent.is_subcontracted", + "depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow", "fieldname": "fg_item_qty", "fieldtype": "Float", "label": "Finished Good Item Qty", - "mandatory_depends_on": "eval:parent.is_subcontracted" + "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-04-07 14:53:16.684010", + "modified": "2022-06-16 06:00:01.624317", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js index 57a41ad56c9..075671f4ec6 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -27,18 +27,16 @@ frappe.query_reports["Subcontract Order Summary"] = { reqd: 1 }, { - label: __("Subcontracting Order"), + label: __("Order Type"), + fieldname: "order_type", + fieldtype: "Select", + options: ["Purchase Order", "Subcontracting Order"], + default: "Subcontracting Order" + }, + { + label: __("Subcontract Order"), fieldname: "name", - fieldtype: "Link", - options: "Subcontracting Order", - get_query: function () { - return { - filters: { - docstatus: 1, - company: frappe.query_report.get_filter_value('company') - } - } - } + fieldtype: "Data" } ] }; \ No newline at end of file diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 3750daa71ea..0213051aeb7 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py @@ -8,7 +8,7 @@ from frappe import _ def execute(filters=None): columns, data = [], [] - columns = get_columns() + columns = get_columns(filters) data = get_data(filters) return columns, data @@ -20,42 +20,45 @@ def get_data(report_filters): if orders: supplied_items = get_supplied_items(orders, report_filters) - sco_details = prepare_subcontracted_data(orders, supplied_items) - get_subcontracted_data(sco_details, data) + order_details = prepare_subcontracted_data(orders, supplied_items) + get_subcontracted_data(order_details, data) return data def get_subcontracted_orders(report_filters): fields = [ - "`tabSubcontracting Order Item`.`parent` as sco_id", - "`tabSubcontracting Order Item`.`item_code`", - "`tabSubcontracting Order Item`.`item_name`", - "`tabSubcontracting Order Item`.`qty`", - "`tabSubcontracting Order Item`.`name`", - "`tabSubcontracting Order Item`.`received_qty`", - "`tabSubcontracting Order`.`status`", + f"`tab{report_filters.order_type} Item`.`parent` as order_id", + f"`tab{report_filters.order_type} Item`.`item_code`", + f"`tab{report_filters.order_type} Item`.`item_name`", + f"`tab{report_filters.order_type} Item`.`qty`", + f"`tab{report_filters.order_type} Item`.`name`", + f"`tab{report_filters.order_type} Item`.`received_qty`", + f"`tab{report_filters.order_type}`.`status`", ] filters = get_filters(report_filters) - return frappe.get_all("Subcontracting Order", fields=fields, filters=filters) or [] + return frappe.get_all(report_filters.order_type, fields=fields, filters=filters) or [] def get_filters(report_filters): filters = [ - ["Subcontracting Order", "docstatus", "=", 1], + [report_filters.order_type, "docstatus", "=", 1], [ - "Subcontracting Order", + report_filters.order_type, "transaction_date", "between", (report_filters.from_date, report_filters.to_date), ], ] + if report_filters.order_type == "Purchase Order": + filters.append(["Purchase Order", "is_old_subcontracting_flow", "=", 1]) + for field in ["name", "company"]: if report_filters.get(field): - filters.append(["Subcontracting Order", field, "=", report_filters.get(field)]) + filters.append([report_filters.order_type, field, "=", report_filters.get(field)]) return filters @@ -70,15 +73,21 @@ def get_supplied_items(orders, report_filters): "rm_item_code", "required_qty", "supplied_qty", + "returned_qty", "total_supplied_qty", "consumed_qty", "reference_name", ] - filters = {"parent": ("in", [d.sco_id for d in orders]), "docstatus": 1} + filters = {"parent": ("in", [d.order_id for d in orders]), "docstatus": 1} supplied_items = {} - for row in frappe.get_all("Subcontracting Order Supplied Item", fields=fields, filters=filters): + supplied_items_table = ( + "Purchase Order Item Supplied" + if report_filters.order_type == "Purchase Order" + else "Subcontracting Order Supplied Item" + ) + for row in frappe.get_all(supplied_items_table, fields=fields, filters=filters): new_key = (row.parent, row.reference_name, row.main_item_code) supplied_items.setdefault(new_key, []).append(row) @@ -87,24 +96,24 @@ def get_supplied_items(orders, report_filters): def prepare_subcontracted_data(orders, supplied_items): - sco_details = {} + order_details = {} for row in orders: - key = (row.sco_id, row.name, row.item_code) - if key not in sco_details: - sco_details.setdefault(key, frappe._dict({"sco_item": row, "supplied_items": []})) + key = (row.order_id, row.name, row.item_code) + if key not in order_details: + order_details.setdefault(key, frappe._dict({"order_item": row, "supplied_items": []})) - details = sco_details[key] + details = order_details[key] if supplied_items.get(key): for supplied_item in supplied_items[key]: details["supplied_items"].append(supplied_item) - return sco_details + return order_details -def get_subcontracted_data(sco_details, data): - for key, details in sco_details.items(): - res = details.sco_item +def get_subcontracted_data(order_details, data): + for key, details in order_details.items(): + res = details.order_item for index, row in enumerate(details.supplied_items): if index != 0: res = {} @@ -113,13 +122,13 @@ def get_subcontracted_data(sco_details, data): data.append(res) -def get_columns(): +def get_columns(filters): return [ { - "label": _("Subcontracting Order"), - "fieldname": "sco_id", + "label": _("Subcontract Order"), + "fieldname": "order_id", "fieldtype": "Link", - "options": "Subcontracting Order", + "options": filters.order_type, "width": 100, }, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80}, @@ -142,4 +151,5 @@ def get_columns(): {"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 110}, {"label": _("Supplied Qty"), "fieldname": "supplied_qty", "fieldtype": "Float", "width": 110}, {"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120}, + {"label": _("Returned Qty"), "fieldname": "returned_qty", "fieldtype": "Float", "width": 110}, ] diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js index fc58b6aaafa..6304a0908d0 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js @@ -4,6 +4,13 @@ frappe.query_reports["Subcontracted Item To Be Received"] = { "filters": [ + { + label: __("Order Type"), + fieldname: "order_type", + fieldtype: "Select", + options: ["Purchase Order", "Subcontracting Order"], + default: "Subcontracting Order" + }, { fieldname: "supplier", label: __("Supplier"), diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json index fdf6cf702df..f40b788fe05 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json @@ -13,7 +13,7 @@ "name": "Subcontracted Item To Be Received", "owner": "Administrator", "prepared_report": 0, - "ref_doctype": "Purchase Order", + "ref_doctype": "Subcontracting Order", "report_name": "Subcontracted Item To Be Received", "report_type": "Script Report", "roles": [ diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py index 30f9dec4d06..135449bb2bd 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py @@ -11,18 +11,18 @@ def execute(filters=None): frappe.msgprint(_("To Date must be greater than From Date")) data = [] - columns = get_columns() + columns = get_columns(filters) get_data(data, filters) return columns, data -def get_columns(): +def get_columns(filters): return [ { - "label": _("Subcontracting Order"), + "label": _("Subcontract Order"), "fieldtype": "Link", - "fieldname": "subcontracting_order", - "options": "Subcontracting Order", + "fieldname": "subcontract_order", + "options": filters.order_type, "width": 150, }, {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150}, @@ -57,14 +57,14 @@ def get_columns(): def get_data(data, filters): - sco = get_sco(filters) - sco_name = [v.name for v in sco] - sub_items = get_subcontracting_order_item_supplied(sco_name) - for item in sub_items: - for order in sco: + orders = get_subcontract_orders(filters) + orders_name = [order.name for order in orders] + subcontracted_items = get_subcontract_order_supplied_item(filters.order_type, orders_name) + for item in subcontracted_items: + for order in orders: if order.name == item.parent and item.received_qty < item.qty: row = { - "subcontracting_order": item.parent, + "subcontract_order": item.parent, "date": order.transaction_date, "supplier": order.supplier, "fg_item_code": item.item_code, @@ -76,21 +76,25 @@ def get_data(data, filters): data.append(row) -def get_sco(filters): +def get_subcontract_orders(filters): record_filters = [ ["supplier", "=", filters.supplier], ["transaction_date", "<=", filters.to_date], ["transaction_date", ">=", filters.from_date], ["docstatus", "=", 1], ] + + if filters.order_type == "Purchase Order": + record_filters.append(["is_old_subcontracting_flow", "=", 1]) + return frappe.get_all( - "Subcontracting Order", filters=record_filters, fields=["name", "transaction_date", "supplier"] + filters.order_type, filters=record_filters, fields=["name", "transaction_date", "supplier"] ) -def get_subcontracting_order_item_supplied(sco): +def get_subcontract_order_supplied_item(order_type, orders): return frappe.get_all( - "Subcontracting Order Item", - filters=[("parent", "IN", sco)], + f"{order_type} Item", + filters=[("parent", "IN", orders)], fields=["parent", "item_code", "item_name", "qty", "received_qty"], ) diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index 80fd657f418..c772c1a1b17 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -50,6 +50,7 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): col, data = execute( filters=frappe._dict( { + "order_type": "Subcontracting Order", "supplier": sco.supplier, "from_date": frappe.utils.get_datetime( frappe.utils.add_to_date(sco.transaction_date, days=-10) @@ -60,7 +61,7 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): ) self.assertEqual(data[0]["pending_qty"], 5) self.assertEqual(data[0]["received_qty"], 5) - self.assertEqual(data[0]["subcontracting_order"], sco.name) + self.assertEqual(data[0]["subcontract_order"], sco.name) self.assertEqual(data[0]["supplier"], sco.supplier) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js index 0853afd6576..b6739fe6632 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js @@ -4,6 +4,13 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = { "filters": [ + { + label: __("Order Type"), + fieldname: "order_type", + fieldtype: "Select", + options: ["Purchase Order", "Subcontracting Order"], + default: "Subcontracting Order" + }, { fieldname: "supplier", label: __("Supplier"), diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json index c7cee5e20b3..f689fbcf247 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json @@ -13,7 +13,7 @@ "name": "Subcontracted Raw Materials To Be Transferred", "owner": "Administrator", "prepared_report": 0, - "ref_doctype": "Purchase Order", + "ref_doctype": "Subcontracting Order", "report_name": "Subcontracted Raw Materials To Be Transferred", "report_type": "Script Report", "roles": [ diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py index a837b24357e..ef28eda62a5 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py @@ -10,19 +10,19 @@ def execute(filters=None): if filters.from_date >= filters.to_date: frappe.msgprint(_("To Date must be greater than From Date")) - columns = get_columns() + columns = get_columns(filters) data = get_data(filters) return columns, data or [] -def get_columns(): +def get_columns(filters): return [ { - "label": _("Purchase Order"), + "label": _("Subcontract Order"), "fieldtype": "Link", - "fieldname": "purchase_order", - "options": "Purchase Order", + "fieldname": "subcontract_order", + "options": filters.order_type, "width": 200, }, {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150}, @@ -46,10 +46,10 @@ def get_columns(): def get_data(filters): - sco_rm_item_details = get_sco_items_to_supply(filters) + order_rm_item_details = get_order_items_to_supply(filters) data = [] - for row in sco_rm_item_details: + for row in order_rm_item_details: transferred_qty = row.get("transferred_qty") or 0 if transferred_qty < row.get("reqd_qty", 0): pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) @@ -59,22 +59,33 @@ def get_data(filters): return data -def get_sco_items_to_supply(filters): +def get_order_items_to_supply(filters): + supplied_items_table = ( + "Purchase Order Item Supplied" + if filters.order_type == "Purchase Order" + else "Subcontracting Order Supplied Item" + ) + + record_filters = [ + [filters.order_type, "per_received", "<", "100"], + [filters.order_type, "supplier", "=", filters.supplier], + [filters.order_type, "transaction_date", "<=", filters.to_date], + [filters.order_type, "transaction_date", ">=", filters.from_date], + [filters.order_type, "docstatus", "=", 1], + ] + + if filters.order_type == "Purchase Order": + record_filters.append([filters.order_type, "is_old_subcontracting_flow", "=", 1]) + return frappe.db.get_all( - "Subcontracting Order", + filters.order_type, fields=[ - "name as subcontracting_order", + "name as subcontract_order", "transaction_date as date", "supplier as supplier", - "`tabSubcontracting Order Supplied Item`.rm_item_code as rm_item_code", - "`tabSubcontracting Order Supplied Item`.required_qty as reqd_qty", - "`tabSubcontracting Order Supplied Item`.supplied_qty as transferred_qty", - ], - filters=[ - ["Subcontracting Order", "per_received", "<", "100"], - ["Subcontracting Order", "supplier", "=", filters.supplier], - ["Subcontracting Order", "transaction_date", "<=", filters.to_date], - ["Subcontracting Order", "transaction_date", ">=", filters.from_date], - ["Subcontracting Order", "docstatus", "=", 1], + f"`tab{supplied_items_table}`.rm_item_code as rm_item_code", + f"`tab{supplied_items_table}`.required_qty as reqd_qty", + f"`tab{supplied_items_table}`.supplied_qty as transferred_qty", ], + filters=record_filters, ) diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index d29791cebf6..160295776b1 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -9,14 +9,12 @@ from frappe.tests.utils import FrappeTestCase from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import ( execute, ) +from erpnext.controllers.subcontracting_controller import make_rm_stock_entry from erpnext.controllers.tests.test_subcontracting_controller import ( get_subcontracting_order, make_service_item, ) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - make_rm_stock_entry, -) class TestSubcontractedItemToBeTransferred(FrappeTestCase): @@ -45,6 +43,7 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase): col, data = execute( filters=frappe._dict( { + "order_type": "Subcontracting Order", "supplier": sco.supplier, "from_date": frappe.utils.get_datetime( frappe.utils.add_to_date(sco.transaction_date, days=-10) @@ -55,12 +54,12 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase): ) sco.reload() - sco_data = [row for row in data if row.get("subcontracting_order") == sco.name] + sco_data = [row for row in data if row.get("subcontract_order") == sco.name] # Alphabetically sort to be certain of order sco_data = sorted(sco_data, key=lambda i: i["rm_item_code"]) self.assertEqual(len(sco_data), 2) - self.assertEqual(sco_data[0]["subcontracting_order"], sco.name) + self.assertEqual(sco_data[0]["subcontract_order"], sco.name) self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item") self.assertEqual(sco_data[0]["p_qty"], 8) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7cb34fccf30..e83e0c2702d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2657,6 +2657,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_ordered_qty() parent.update_ordered_and_reserved_qty() parent.update_receiving_percentage() + if parent.is_old_subcontracting_flow: + parent.update_reserved_qty_for_subcontract() + parent.create_raw_materials_supplied() + parent.save() else: # Sales Order parent.validate_warehouse() parent.update_reserved_qty() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fa091df8683..4db8ccb5b8d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -11,7 +11,7 @@ from erpnext.accounts.doctype.budget.budget import validate_expense_against_budg from erpnext.accounts.party import get_party_details from erpnext.buying.utils import update_last_purchase_rate, validate_for_items from erpnext.controllers.sales_and_purchase_return import get_rate_for_return -from erpnext.controllers.stock_controller import StockController +from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.utils import get_incoming_rate @@ -20,7 +20,10 @@ class QtyMismatchError(ValidationError): pass -class BuyingController(StockController): +class BuyingController(SubcontractingController): + def __setup__(self): + self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] + def get_feed(self): if self.get("supplier_name"): return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total) @@ -51,6 +54,8 @@ class BuyingController(StockController): # sub-contracting self.validate_for_subcontracting() + if self.get("is_old_subcontracting_flow"): + self.create_raw_materials_supplied() self.set_landed_cost_voucher_amount() if self.doctype in ("Purchase Receipt", "Purchase Invoice"): @@ -251,9 +256,18 @@ class BuyingController(StockController): ) qty_in_stock_uom = flt(item.qty * item.conversion_factor) - item.valuation_rate = ( - item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount) - ) / qty_in_stock_uom + if self.get("is_old_subcontracting_flow"): + item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) + item.valuation_rate = ( + item.base_net_amount + + item.item_tax_amount + + item.rm_supp_cost + + flt(item.landed_cost_voucher_amount) + ) / qty_in_stock_uom + else: + item.valuation_rate = ( + item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount) + ) / qty_in_stock_uom else: item.valuation_rate = 0.0 @@ -312,6 +326,19 @@ class BuyingController(StockController): if self.is_subcontracted: if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype)) + + if self.get("is_old_subcontracting_flow"): + for item in self.get("items"): + if item in self.sub_contracted_items and not item.bom: + frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) + + if self.doctype != "Purchase Order": + return + + for row in self.get("supplied_items"): + if not row.reserve_warehouse: + msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" + frappe.throw(_(msg)) else: for item in self.get("items"): if item.get("bom"): @@ -440,7 +467,9 @@ class BuyingController(StockController): sle.update( { "incoming_rate": incoming_rate, - "recalculate_rate": 1 if (self.is_subcontracted and d.fg_item) or d.from_warehouse else 0, + "recalculate_rate": 1 + if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse + else 0, } ) sl_entries.append(sle) @@ -468,6 +497,8 @@ class BuyingController(StockController): ) ) + if self.get("is_old_subcontracting_flow"): + self.make_sl_entries_for_supplier_warehouse(sl_entries) self.make_sl_entries( sl_entries, allow_negative_stock=allow_negative_stock, @@ -494,6 +525,8 @@ class BuyingController(StockController): ) po_obj.update_ordered_qty(po_item_rows) + if self.get("is_old_subcontracting_flow"): + po_obj.update_reserved_qty_for_subcontract() def on_submit(self): if self.get("is_return"): @@ -718,7 +751,10 @@ class BuyingController(StockController): if self.doctype == "Material Request": return - validate_item_type(self, "is_purchase_item", "purchase") + if self.get("is_old_subcontracting_flow"): + validate_item_type(self, "is_sub_contracted_item", "subcontracted") + else: + validate_item_type(self, "is_purchase_item", "purchase") def get_asset_item_details(asset_items): diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 4e0d91147e9..2a2f8f562e7 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -2,6 +2,7 @@ # For license information, please see license.txt import copy +import json from collections import defaultdict import frappe @@ -14,13 +15,40 @@ from erpnext.stock.utils import get_incoming_rate class SubcontractingController(StockController): + def __init__(self, *args, **kwargs): + super(SubcontractingController, self).__init__(*args, **kwargs) + if self.get("is_old_subcontracting_flow"): + self.subcontract_data = frappe._dict( + { + "order_doctype": "Purchase Order", + "order_field": "purchase_order", + "rm_detail_field": "po_detail", + "receipt_supplied_items_field": "Purchase Receipt Item Supplied", + "order_supplied_items_field": "Purchase Order Item Supplied", + } + ) + else: + self.subcontract_data = frappe._dict( + { + "order_doctype": "Subcontracting Order", + "order_field": "subcontracting_order", + "rm_detail_field": "sco_rm_detail", + "receipt_supplied_items_field": "Subcontracting Receipt Supplied Item", + "order_supplied_items_field": "Subcontracting Order Supplied Item", + } + ) + def before_validate(self): - self.remove_empty_rows() - self.set_items_conversion_factor() + if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]: + self.remove_empty_rows() + self.set_items_conversion_factor() def validate(self): - self.validate_items() - self.create_raw_materials_supplied() + if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]: + self.validate_items() + self.create_raw_materials_supplied() + else: + super(SubcontractingController, self).validate() def remove_empty_rows(self): for key in ["service_items", "items", "supplied_items"]: @@ -54,7 +82,10 @@ class SubcontractingController(StockController): def __get_data_before_save(self): item_dict = {} - if self.doctype == "Subcontracting Receipt" and self._doc_before_save: + if ( + self.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"] + and self._doc_before_save + ): for row in self._doc_before_save.get("items"): item_dict[row.name] = (row.item_code, row.qty) @@ -64,7 +95,7 @@ class SubcontractingController(StockController): self.__changed_name = [] self.__reference_name = [] - if self.doctype == "Subcontracting Order" or self.is_new(): + if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new(): self.set(self.raw_material_table, []) return @@ -93,36 +124,38 @@ class SubcontractingController(StockController): self.alternative_item_details = frappe._dict() self.__get_backflush_based_on() - def __get_subcontracting_orders(self): - self.subcontracting_orders = [] + def __get_subcontract_orders(self): + self.subcontract_orders = [] - if self.doctype == "Subcontracting Order": + if self.doctype in ["Purchase Order", "Subcontracting Order"]: return - self.subcontracting_orders = [ - item.subcontracting_order for item in self.items if item.subcontracting_order + self.subcontract_orders = [ + item.get(self.subcontract_data.order_field) + for item in self.items + if item.get(self.subcontract_data.order_field) ] def __get_pending_qty_to_receive(self): - """Get qty to be received against the subcontracting order.""" + """Get qty to be received against the subcontract order.""" self.qty_to_be_received = defaultdict(float) if ( - self.doctype != "Subcontracting Order" + self.doctype != self.subcontract_data.order_doctype and self.backflush_based_on != "BOM" - and self.subcontracting_orders + and self.subcontract_orders ): for row in frappe.get_all( - "Subcontracting Order Item", + f"{self.subcontract_data.order_doctype} Item", fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], - filters={"docstatus": 1, "parent": ("in", self.subcontracting_orders)}, + filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)}, ): self.qty_to_be_received[(row.item_code, row.parent)] += row.qty def __get_transferred_items(self): - fields = ["`tabStock Entry`.`subcontracting_order`"] + fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"] alias_dict = { "item_code": "rm_item_code", "subcontracted_item": "main_item_code", @@ -145,7 +178,7 @@ class SubcontractingController(StockController): "s_warehouse", "t_warehouse", "item_group", - "sco_rm_detail", + self.subcontract_data.rm_detail_field, ] if self.backflush_based_on == "BOM": @@ -157,7 +190,7 @@ class SubcontractingController(StockController): filters = [ ["Stock Entry", "docstatus", "=", 1], ["Stock Entry", "purpose", "=", "Send to Subcontractor"], - ["Stock Entry", "subcontracting_order", "in", self.subcontracting_orders], + ["Stock Entry", self.subcontract_data.order_field, "in", self.subcontract_orders], ] return frappe.get_all("Stock Entry", fields=fields, filters=filters) @@ -168,21 +201,21 @@ class SubcontractingController(StockController): def __get_received_items(self, doctype): fields = [] - self.sco_field = "subcontracting_order" - - for field in ["name", self.sco_field, "parent"]: + for field in ["name", self.subcontract_data.order_field, "parent"]: fields.append(f"`tab{doctype} Item`.`{field}`") filters = [ [doctype, "docstatus", "=", 1], - [f"{doctype} Item", self.sco_field, "in", self.subcontracting_orders], + [f"{doctype} Item", self.subcontract_data.order_field, "in", self.subcontract_orders], ] + if doctype == "Purchase Invoice": + filters.append(["Purchase Invoice", "update_stock", "=", 1]) return frappe.get_all(f"{doctype}", fields=fields, filters=filters) - def __get_consumed_items(self, doctype, scr_items): + def __get_consumed_items(self, doctype, receipt_items): return frappe.get_all( - "Subcontracting Receipt Supplied Item", + self.subcontract_data.receipt_supplied_items_field, fields=[ "serial_no", "rm_item_code", @@ -191,26 +224,26 @@ class SubcontractingController(StockController): "consumed_qty", "main_item_code", ], - filters={"docstatus": 1, "reference_name": ("in", list(scr_items)), "parenttype": doctype}, + filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype}, ) def __update_consumed_materials(self, doctype, return_consumed_items=False): """Deduct the consumed materials from the available materials.""" - scr_items = self.__get_received_items(doctype) - if not scr_items: + receipt_items = self.__get_received_items(doctype) + if not receipt_items: return ([], {}) if return_consumed_items else None - scr_items = { - item.name: item.get(self.get("sco_field") or "subcontracting_order") for item in scr_items + receipt_items = { + item.name: item.get(self.subcontract_data.order_field) for item in receipt_items } - consumed_materials = self.__get_consumed_items(doctype, scr_items.keys()) + consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys()) if return_consumed_items: - return (consumed_materials, scr_items) + return (consumed_materials, receipt_items) for row in consumed_materials: - key = (row.rm_item_code, row.main_item_code, scr_items.get(row.reference_name)) + key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name)) if not self.available_materials.get(key): continue @@ -226,16 +259,16 @@ class SubcontractingController(StockController): def get_available_materials(self): """Get the available raw materials which has been transferred to the supplier. available_materials = { - (item_code, subcontracted_item, subcontracting_order): { + (item_code, subcontracted_item, subcontract_order): { 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details } } """ - if not self.subcontracting_orders: + if not self.subcontract_orders: return for row in self.__get_transferred_items(): - key = (row.rm_item_code, row.main_item_code, row.subcontracting_order) + key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field)) if key not in self.available_materials: self.available_materials.setdefault( @@ -246,14 +279,16 @@ class SubcontractingController(StockController): "serial_no": [], "batch_no": defaultdict(float), "item_details": row, - "sco_rm_details": [], + f"{self.subcontract_data.rm_detail_field}s": [], } ), ) details = self.available_materials[key] details.qty += row.qty - details.sco_rm_details.append(row.sco_rm_detail) + details[f"{self.subcontract_data.rm_detail_field}s"].append( + row.get(self.subcontract_data.rm_detail_field) + ) if row.serial_no: details.serial_no.extend(get_serial_nos(row.serial_no)) @@ -264,7 +299,11 @@ class SubcontractingController(StockController): self.__set_alternative_item_details(row) self.__transferred_items = copy.deepcopy(self.available_materials) - self.__update_consumed_materials("Subcontracting Receipt") + if self.get("is_old_subcontracting_flow"): + for doctype in ["Purchase Receipt", "Purchase Invoice"]: + self.__update_consumed_materials(doctype) + else: + self.__update_consumed_materials("Subcontracting Receipt") def __remove_changed_rows(self): if not self.__changed_name: @@ -317,7 +356,7 @@ class SubcontractingController(StockController): ) def __update_reserve_warehouse(self, row, item): - if self.doctype == "Subcontracting Order": + if self.doctype == self.subcontract_data.order_doctype: row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse def __set_alternative_item(self, bom_item): @@ -325,7 +364,7 @@ class SubcontractingController(StockController): bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) def __set_serial_nos(self, item_row, rm_obj): - key = (rm_obj.rm_item_code, item_row.item_code, item_row.subcontracting_order) + key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) if self.available_materials.get(key) and self.available_materials[key]["serial_no"]: used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)] rm_obj.serial_no = "\n".join(used_serial_nos) @@ -340,7 +379,7 @@ class SubcontractingController(StockController): "consumed_qty": qty, "batch_no": batch_no, "required_qty": qty, - "subcontracting_order": item_row.subcontracting_order, + self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field), } ) @@ -351,7 +390,7 @@ class SubcontractingController(StockController): rm_obj.consumed_qty = consumed_qty def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): - key = (rm_obj.rm_item_code, item_row.item_code, item_row.subcontracting_order) + key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: new_rm_obj = None @@ -397,16 +436,18 @@ class SubcontractingController(StockController): ) rm_obj.rate = get_incoming_rate(args) - if self.doctype == "Subcontracting Order": + if self.doctype == self.subcontract_data.order_doctype: rm_obj.required_qty = qty rm_obj.amount = rm_obj.required_qty * rm_obj.rate else: rm_obj.consumed_qty = 0 - rm_obj.subcontracting_order = item_row.subcontracting_order + setattr( + rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field) + ) self.__set_batch_nos(bom_item, item_row, rm_obj, qty) def __get_qty_based_on_material_transfer(self, item_row, transfer_item): - key = (item_row.item_code, item_row.subcontracting_order) + key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) if self.qty_to_be_received == item_row.qty: return transfer_item.qty @@ -427,13 +468,13 @@ class SubcontractingController(StockController): has_supplied_items = True if self.get(self.raw_material_table) else False for row in self.items: - if self.doctype != "Subcontracting Order" and ( + if self.doctype != self.subcontract_data.order_doctype and ( (self.__changed_name and row.name not in self.__changed_name) or (has_supplied_items and not self.__changed_name) ): continue - if self.doctype == "Subcontracting Order" or self.backflush_based_on == "BOM": + if self.doctype == self.subcontract_data.order_doctype or self.backflush_based_on == "BOM": for bom_item in self.__get_materials_from_bom( row.item_code, row.bom, row.get("include_exploded_items") ): @@ -445,17 +486,22 @@ class SubcontractingController(StockController): elif self.backflush_based_on != "BOM": for key, transfer_item in self.available_materials.items(): - if (key[1], key[2]) == (row.item_code, row.subcontracting_order) and transfer_item.qty > 0: + if (key[1], key[2]) == ( + row.item_code, + row.get(self.subcontract_data.order_field), + ) and transfer_item.qty > 0: qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 transfer_item.qty -= qty self.__add_supplied_item(row, transfer_item.get("item_details"), qty) if self.qty_to_be_received: - self.qty_to_be_received[(row.item_code, row.subcontracting_order)] -= row.qty + self.qty_to_be_received[ + (row.item_code, row.get(self.subcontract_data.order_field)) + ] -= row.qty def __prepare_supplied_items(self): self.initialized_fields() - self.__get_subcontracting_orders() + self.__get_subcontract_orders() self.__get_pending_qty_to_receive() self.get_available_materials() self.__remove_changed_rows() @@ -465,8 +511,10 @@ class SubcontractingController(StockController): if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( "batch_no" ): - link = get_link_to_form("Subcontracting Order", row.subcontracting_order) - msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Subcontracting Order {link}' + link = get_link_to_form( + self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field) + ) + msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {link}' frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) def __validate_serial_no(self, row, key): @@ -476,16 +524,18 @@ class SubcontractingController(StockController): if incorrect_sn: incorrect_sn = "\n".join(incorrect_sn) - link = get_link_to_form("Subcontracting Order", row.subcontracting_order) - msg = f"The Serial Nos {incorrect_sn} has not supplied against the Subcontracting Order {link}" + link = get_link_to_form( + self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field) + ) + msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}" frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) def __validate_supplied_items(self): - if self.doctype != "Subcontracting Receipt": + if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: return for row in self.get(self.raw_material_table): - key = (row.rm_item_code, row.main_item_code, row.subcontracting_order) + key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field)) if not self.__transferred_items or not self.__transferred_items.get(key): return @@ -493,6 +543,9 @@ class SubcontractingController(StockController): self.__validate_serial_no(row, key) def set_materials_for_subcontracted_items(self, raw_material_table): + if self.doctype == "Purchase Invoice" and not self.update_stock: + return + self.raw_material_table = raw_material_table self.__identify_change_in_item_table() self.__prepare_supplied_items() @@ -501,16 +554,16 @@ class SubcontractingController(StockController): def create_raw_materials_supplied(self, raw_material_table="supplied_items"): self.set_materials_for_subcontracted_items(raw_material_table) - if self.doctype == "Subcontracting Receipt": + if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]: for item in self.get("items"): item.rm_supp_cost = 0.0 - def __update_consumed_qty_in_sco(self, itemwise_consumed_qty): + def __update_consumed_qty_in_subcontract_order(self, itemwise_consumed_qty): fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"] - filters = {"docstatus": 1, "parent": ("in", self.subcontracting_orders)} + filters = {"docstatus": 1, "parent": ("in", self.subcontract_orders)} for row in frappe.get_all( - "Subcontracting Order Supplied Item", fields=fields, filters=filters, order_by="idx" + self.subcontract_data.order_supplied_items_field, fields=fields, filters=filters, order_by="idx" ): key = (row.rm_item_code, row.main_item_code, row.parent) consumed_qty = itemwise_consumed_qty.get(key, 0) @@ -520,22 +573,31 @@ class SubcontractingController(StockController): itemwise_consumed_qty[key] -= consumed_qty frappe.db.set_value( - "Subcontracting Order Supplied Item", row.name, "consumed_qty", consumed_qty + self.subcontract_data.order_supplied_items_field, row.name, "consumed_qty", consumed_qty ) - def set_consumed_qty_in_sco(self): - # Update consumed qty back in the subcontracting order - self.__get_subcontracting_orders() - itemwise_consumed_qty = defaultdict(float) - consumed_items, scr_items = self.__update_consumed_materials( - "Subcontracting Receipt", return_consumed_items=True - ) + def set_consumed_qty_in_subcontract_order(self): + # Update consumed qty back in the subcontract order + if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"] or self.get( + "is_old_subcontracting_flow" + ): + self.__get_subcontract_orders() + itemwise_consumed_qty = defaultdict(float) + if self.get("is_old_subcontracting_flow"): + doctypes = ["Purchase Receipt", "Purchase Invoice"] + else: + doctypes = ["Subcontracting Receipt"] - for row in consumed_items: - key = (row.rm_item_code, row.main_item_code, scr_items.get(row.reference_name)) - itemwise_consumed_qty[key] += row.consumed_qty + for doctype in doctypes: + consumed_items, receipt_items = self.__update_consumed_materials( + doctype, return_consumed_items=True + ) - self.__update_consumed_qty_in_sco(itemwise_consumed_qty) + for row in consumed_items: + key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name)) + itemwise_consumed_qty[key] += row.consumed_qty + + self.__update_consumed_qty_in_subcontract_order(itemwise_consumed_qty) def update_ordered_and_reserved_qty(self): sco_map = {} @@ -618,10 +680,30 @@ class SubcontractingController(StockController): via_landed_cost_voucher=via_landed_cost_voucher, ) - def get_supplied_items_cost(self, item_row_id): + def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): supplied_items_cost = 0.0 for item in self.get("supplied_items"): if item.reference_name == item_row_id: + if ( + self.get("is_old_subcontracting_flow") + and reset_outgoing_rate + and frappe.get_cached_value("Item", item.rm_item_code, "is_stock_item") + ): + rate = get_incoming_rate( + { + "item_code": item.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * item.consumed_qty, + "serial_no": item.serial_no, + "batch_no": item.batch_no, + } + ) + + if rate > 0: + item.rate = rate + item.amount = flt(flt(item.consumed_qty) * flt(item.rate), item.precision("amount")) supplied_items_cost += item.amount @@ -631,13 +713,25 @@ class SubcontractingController(StockController): if self.doctype == "Subcontracting Order": self.update_status() elif self.doctype == "Subcontracting Receipt": - self.__get_subcontracting_orders + self.__get_subcontract_orders - if self.subcontracting_orders: - for sco in set(self.subcontracting_orders): + if self.subcontract_orders: + for sco in set(self.subcontract_orders): sco_doc = frappe.get_doc("Subcontracting Order", sco) sco_doc.update_status() + @frappe.whitelist() + def get_current_stock(self): + if self.doctype in ["Purchase Receipt", "Subcontracting Receipt"]: + for item in self.get("supplied_items"): + if self.supplier_warehouse: + actual_qty = frappe.db.get_value( + "Bin", + {"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse}, + "actual_qty", + ) + item.current_stock = flt(actual_qty) or 0 + @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): @@ -650,3 +744,159 @@ class SubcontractingController(StockController): self._sub_contracted_items = [item.name for item in items] return self._sub_contracted_items + + +def get_item_details(items): + item = frappe.qb.DocType("Item") + item_list = ( + frappe.qb.from_(item) + .select(item.item_code, item.description, item.allow_alternative_item) + .where(item.name.isin(items)) + .run(as_dict=True) + ) + + item_details = {} + for item in item_list: + item_details[item.item_code] = item + + return item_details + + +@frappe.whitelist() +def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"): + rm_items_list = rm_items + + if isinstance(rm_items, str): + rm_items_list = json.loads(rm_items) + elif not rm_items: + frappe.throw(_("No Items available for transfer")) + + if rm_items_list: + fg_items = list(set(item["item_code"] for item in rm_items_list)) + else: + frappe.throw(_("No Items selected for transfer")) + + if subcontract_order: + subcontract_order = frappe.get_doc(order_doctype, subcontract_order) + + if fg_items: + items = tuple(set(item["rm_item_code"] for item in rm_items_list)) + item_wh = get_item_details(items) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = "Send to Subcontractor" + if order_doctype == "Purchase Order": + stock_entry.purchase_order = subcontract_order.name + else: + stock_entry.subcontracting_order = subcontract_order.name + stock_entry.supplier = subcontract_order.supplier + stock_entry.supplier_name = subcontract_order.supplier_name + stock_entry.supplier_address = subcontract_order.supplier_address + stock_entry.address_display = subcontract_order.address_display + stock_entry.company = subcontract_order.company + stock_entry.to_warehouse = subcontract_order.supplier_warehouse + stock_entry.set_stock_entry_type() + + if order_doctype == "Purchase Order": + rm_detail_field = "po_detail" + else: + rm_detail_field = "sco_rm_detail" + + for item_code in fg_items: + for rm_item_data in rm_items_list: + if rm_item_data["item_code"] == item_code: + rm_item_code = rm_item_data["rm_item_code"] + items_dict = { + rm_item_code: { + rm_detail_field: rm_item_data.get("name"), + "item_name": rm_item_data["item_name"], + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item_data["qty"], + "from_warehouse": rm_item_data["warehouse"], + "stock_uom": rm_item_data["stock_uom"], + "serial_no": rm_item_data.get("serial_no"), + "batch_no": rm_item_data.get("batch_no"), + "main_item_code": rm_item_data["item_code"], + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + } + } + stock_entry.add_to_stock_entry_detail(items_dict) + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer")) + return subcontract_order.name + + +def add_items_in_ste( + ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_detail", batch_no=None +): + item = ste_doc.append("items", row.item_details) + + rm_detail = list(set(row.get(f"{rm_detail_field}s")).intersection(rm_details)) + item.update( + { + "qty": qty, + "batch_no": batch_no, + "basic_rate": row.item_details["rate"], + rm_detail_field: rm_detail[0] if rm_detail else "", + "s_warehouse": row.item_details["t_warehouse"], + "t_warehouse": row.item_details["s_warehouse"], + "item_code": row.item_details["rm_item_code"], + "subcontracted_item": row.item_details["main_item_code"], + "serial_no": "\n".join(row.serial_no) if row.serial_no else "", + } + ) + + +def make_return_stock_entry_for_subcontract( + available_materials, order_doc, rm_details, order_doctype="Subcontracting Order" +): + ste_doc = frappe.new_doc("Stock Entry") + ste_doc.purpose = "Material Transfer" + + if order_doctype == "Purchase Order": + ste_doc.purchase_order = order_doc.name + rm_detail_field = "po_detail" + else: + ste_doc.subcontracting_order = order_doc.name + rm_detail_field = "sco_rm_detail" + ste_doc.company = order_doc.company + ste_doc.is_return = 1 + + for key, value in available_materials.items(): + if not value.qty: + continue + + if value.batch_no: + for batch_no, qty in value.batch_no.items(): + if qty > 0: + add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field, batch_no) + else: + add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field) + + ste_doc.set_stock_entry_type() + ste_doc.calculate_rate_and_amount() + + return ste_doc + + +@frappe.whitelist() +def get_materials_from_supplier( + subcontract_order, rm_details, order_doctype="Subcontracting Order" +): + if isinstance(rm_details, str): + rm_details = json.loads(rm_details) + + doc = frappe.get_cached_doc(order_doctype, subcontract_order) + doc.initialized_fields() + doc.subcontract_orders = [doc.name] + doc.get_available_materials() + + if not doc.available_materials: + frappe.throw( + _("Materials are already received against the {0} {1}").format(order_doctype, subcontract_order) + ) + + return make_return_stock_entry_for_subcontract( + doc.available_materials, doc, rm_details, order_doctype + ) diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py index 4ef3d649df5..4fab8058b86 100644 --- a/erpnext/controllers/tests/test_subcontracting_controller.py +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -9,13 +9,15 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import cint from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.controllers.subcontracting_controller import ( + get_materials_from_supplier, + make_rm_stock_entry, +) from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - get_materials_from_supplier, - make_rm_stock_entry, make_subcontracting_receipt, ) diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 38a8500ac73..7ad2bec8598 100644 --- a/erpnext/patches/v13_0/add_bin_unique_constraint.py +++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py @@ -64,4 +64,8 @@ def delete_and_patch_duplicate_bins(): bin.update(qty_dict) bin.update_reserved_qty_for_production() bin.update_reserved_qty_for_sub_contracting() + if frappe.db.count( + "Purchase Order", {"status": ["!=", "Completed"], "is_old_subcontracting_flow": 1} + ): + bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order") bin.db_update() diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 91e07716548..09779d89ec1 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -83,9 +83,17 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac this.frm.set_query("item_code", "items", function() { if (me.frm.doc.is_subcontracted) { + var filters = {'supplier': me.frm.doc.supplier}; + if (me.frm.doc.is_old_subcontracting_flow) { + filters["is_sub_contracted_item"] = 1; + } + else { + filters["is_stock_item"] = 0; + } + return{ query: "erpnext.controllers.queries.item_query", - filters:{ 'supplier': me.frm.doc.supplier, 'is_stock_item': 0 } + filters: filters } } else { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index de93c82ef2c..d86ff1c50fe 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -471,7 +471,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe cost_center: item.cost_center, tax_category: me.frm.doc.tax_category, item_tax_template: item.item_tax_template, - child_docname: item.name + child_docname: item.name, + is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow, } }, diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 01710f1e41a..68b3e2e20af 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -486,7 +486,11 @@ erpnext.utils.update_child_items = function(opts) { filters = {"is_sales_item": 1}; } else if (frm.doc.doctype == 'Purchase Order') { if (frm.doc.is_subcontracted) { - filters = {"is_sub_contracted_item": 1}; + if (frm.doc.is_old_subcontracting_flow) { + filters = {"is_sub_contracted_item": 1}; + } else { + 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 448b0496eb9..548df318fac 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -40,23 +40,37 @@ class Bin(Document): self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) self.db_set("projected_qty", self.projected_qty) - def update_reserved_qty_for_sub_contracting(self): + def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): # reserved qty - sco = frappe.qb.DocType("Subcontracting Order") - supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item") + subcontract_order = frappe.qb.DocType(subcontract_doctype) + supplied_item = frappe.qb.DocType( + "Purchase Order Item Supplied" + if subcontract_doctype == "Purchase Order" + else "Subcontracting Order Supplied Item" + ) + + conditions = ( + (supplied_item.rm_item_code == self.item_code) + & (subcontract_order.name == supplied_item.parent) + & (subcontract_order.per_received < 100) + & (supplied_item.reserve_warehouse == self.warehouse) + & ( + ( + (subcontract_order.is_old_subcontracting_flow == 1) + & (subcontract_order.status != "Closed") + & (subcontract_order.docstatus == 1) + ) + if subcontract_doctype == "Purchase Order" + else (subcontract_order.docstatus == 1) + ) + ) reserved_qty_for_sub_contract = ( - frappe.qb.from_(sco) + frappe.qb.from_(subcontract_order) .from_(supplied_item) .select(Sum(Coalesce(supplied_item.required_qty, 0))) - .where( - (supplied_item.rm_item_code == self.item_code) - & (sco.name == supplied_item.parent) - & (sco.docstatus == 1) - & (sco.per_received < 100) - & (supplied_item.reserve_warehouse == self.warehouse) - ) + .where(conditions) ).run()[0][0] or 0.0 se = frappe.qb.DocType("Stock Entry") @@ -69,23 +83,34 @@ class Bin(Document): else: qty_field = se_item.transfer_qty + conditions = ( + (se.docstatus == 1) + & (se.purpose == "Send to Subcontractor") + & ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code)) + & (se.name == se_item.parent) + & (subcontract_order.docstatus == 1) + & (subcontract_order.per_received < 100) + & ( + ( + (Coalesce(se.purchase_order, "") != "") + & (subcontract_order.name == se.purchase_order) + & (subcontract_order.is_old_subcontracting_flow == 1) + & (subcontract_order.status != "Closed") + ) + if subcontract_doctype == "Purchase Order" + else ( + (Coalesce(se.subcontracting_order, "") != "") + & (subcontract_order.name == se.subcontracting_order) + ) + ) + ) + materials_transferred = ( frappe.qb.from_(se) .from_(se_item) - .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.subcontracting_order, "") != "") - & ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code)) - & (se.name == se_item.parent) - & (sco.name == se.subcontracting_order) - & (sco.docstatus == 1) - & (sco.per_received < 100) - ) + .from_(subcontract_order) + .select(Sum(qty_field)) + .where(conditions) ).run()[0][0] or 0.0 if reserved_qty_for_sub_contract > materials_transferred: diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 49530b4bb39..199641803ed 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -5,6 +5,7 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import flt +from erpnext.controllers.subcontracting_controller import make_rm_stock_entry from erpnext.controllers.tests.test_subcontracting_controller import ( get_subcontracting_order, make_service_item, @@ -21,7 +22,6 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - make_rm_stock_entry, make_subcontracting_receipt, ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 74db616b2b6..e6fcb78f129 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -296,6 +296,10 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt frappe.provide("erpnext.buying"); frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) { + if (frm.doc.is_old_subcontracting_flow) { + erpnext.buying.get_default_bom(frm); + } + frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); }); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index ff4e0a13cf6..cef9ddda977 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -123,6 +123,7 @@ class PurchaseReceipt(BuyingController): if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + self.get_current_stock() self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") @@ -234,6 +235,7 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() + self.set_consumed_qty_in_subcontract_order() def check_next_docstatus(self): submit_rv = frappe.db.sql( @@ -269,6 +271,7 @@ class PurchaseReceipt(BuyingController): self.repost_future_sle_and_gle() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.delete_auto_created_batches() + self.set_consumed_qty_in_subcontract_order() def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 625a3037c16..f0de04b1616 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -642,13 +642,15 @@ "print_hide": 1 }, { + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", "no_copy": 1, "options": "BOM", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow" }, { "default": "0", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a838236f2e2..1c514a90eee 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -615,8 +615,15 @@ frappe.ui.form.on('Stock Entry', { if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); }, + purchase_order: (frm) => { + if (frm.doc.purchase_order) { + frm.set_value("subcontracting_order", ""); + } + }, + subcontracting_order: (frm) => { if (frm.doc.subcontracting_order) { + frm.set_value("purchase_order", ""); erpnext.utils.map_current_doc({ method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order', source_name: frm.doc.subcontracting_order, @@ -624,9 +631,6 @@ frappe.ui.form.on('Stock Entry', { freeze: true, }); } - else { - frm.set_value("items", []); - } }, }); @@ -790,6 +794,16 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle return erpnext.queries.item({is_stock_item: 1}); }; + this.frm.set_query("purchase_order", function() { + return { + "filters": { + "docstatus": 1, + "is_old_subcontracting_flow": 1, + "company": me.frm.doc.company + } + }; + }); + this.frm.set_query("subcontracting_order", function() { return { "filters": { @@ -814,7 +828,12 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle } } - this.frm.add_fetch("subcontracting_order", "supplier", "supplier"); + if (me.frm.doc.purchase_order) { + this.frm.add_fetch("purchase_order", "supplier", "supplier"); + } + else { + this.frm.add_fetch("subcontracting_order", "supplier", "supplier"); + } frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' } this.frm.set_query("supplier_address", erpnext.queries.address_query) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 86f1b6a4867..abe98e2933e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -148,11 +148,11 @@ "search_index": 1 }, { + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", "fieldname": "purchase_order", "fieldtype": "Link", "label": "Purchase Order", - "options": "Purchase Order", - "read_only": 1 + "options": "Purchase Order" }, { "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 6599eddd4ac..d3f15e703f4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -62,6 +62,27 @@ form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"} class StockEntry(StockController): + def __init__(self, *args, **kwargs): + super(StockEntry, self).__init__(*args, **kwargs) + if self.purchase_order: + self.subcontract_data = frappe._dict( + { + "order_doctype": "Purchase Order", + "order_field": "purchase_order", + "rm_detail_field": "po_detail", + "order_supplied_items_field": "Purchase Order Item Supplied", + } + ) + else: + self.subcontract_data = frappe._dict( + { + "order_doctype": "Subcontracting Order", + "order_field": "subcontracting_order", + "rm_detail_field": "sco_rm_detail", + "order_supplied_items_field": "Subcontracting Order Supplied Item", + } + ) + def get_feed(self): return self.stock_entry_type @@ -134,8 +155,8 @@ class StockEntry(StockController): update_serial_nos_after_submit(self, "items") self.update_work_order() - self.validate_subcontracting_order() - self.update_subcontracting_order_supplied_items() + self.validate_subcontract_order() + self.update_subcontract_order_supplied_items() self.update_subcontracting_order_status() self.make_gl_entries() @@ -155,7 +176,7 @@ class StockEntry(StockController): self.set_material_request_transfer_status("Completed") def on_cancel(self): - self.update_subcontracting_order_supplied_items() + self.update_subcontract_order_supplied_items() self.update_subcontracting_order_status() if self.work_order and self.purpose == "Material Consumption for Manufacture": @@ -809,8 +830,8 @@ class StockEntry(StockController): serial_nos.append(sn) - def validate_subcontracting_order(self): - """Throw exception if more raw material is transferred against Subcontracting Order than in + def validate_subcontract_order(self): + """Throw exception if more raw material is transferred against Subcontract 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" @@ -818,28 +839,29 @@ 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.subcontracting_order): + if not (self.purpose == "Send to Subcontractor" and self.get(self.subcontract_data.order_field)): return if backflush_raw_materials_based_on == "BOM": - subcontracting_order = frappe.get_doc("Subcontracting Order", self.subcontracting_order) + subcontract_order = frappe.get_doc( + self.subcontract_data.order_doctype, self.get(self.subcontract_data.order_field) + ) 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 subcontracting_order.supplied_items - if d.rm_item_code == item_code - ] + [flt(d.required_qty) for d in subcontract_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( - "Subcontracting Order Item", - {"parent": self.subcontracting_order, "item_code": se_item.subcontracted_item}, + f"{self.subcontract_data.order_doctype} Item", + { + "parent": self.get(self.subcontract_data.order_field), + "item_code": se_item.subcontracted_item, + }, "bom", ) @@ -851,7 +873,7 @@ class StockEntry(StockController): required_qty = sum( [ flt(d.required_qty) - for d in subcontracting_order.supplied_items + for d in subcontract_order.supplied_items if d.rm_item_code == original_item_code ] ) @@ -860,43 +882,57 @@ class StockEntry(StockController): if not required_qty: frappe.throw( - _("Item {0} not found in 'Raw Materials Supplied' table in Subcontracting Order {1}").format( - se_item.item_code, self.subcontracting_order + _("Item {0} not found in 'Raw Materials Supplied' table in {1} {2}").format( + se_item.item_code, + self.subcontract_data.order_doctype, + self.get(self.subcontract_data.order_field), ) ) parent = frappe.qb.DocType("Stock Entry") child = frappe.qb.DocType("Stock Entry Detail") + conditions = ( + (parent.docstatus == 1) + & (child.item_code == se_item.item_code) + & ( + (parent.purchase_order == self.purchase_order) + if self.subcontract_data.order_doctype == "Purchase Order" + else (parent.subcontracting_order == self.subcontracting_order) + ) + ) + total_supplied = ( frappe.qb.from_(parent) .inner_join(child) .on(parent.name == child.parent) .select(Sum(child.transfer_qty)) - .where(parent.docstatus == 1) - .where(parent.subcontracting_order == self.subcontracting_order) - .where(child.item_code == se_item.item_code) + .where(conditions) ).run()[0][0] if flt(total_supplied, precision) > flt(total_allowed, precision): frappe.throw( - _( - "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 + _("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format( + se_item.idx, + se_item.item_code, + total_allowed, + self.subcontract_data.order_doctype, + self.get(self.subcontract_data.order_field), ) ) - elif not se_item.sco_rm_detail: + elif not se_item.get(self.subcontract_data.rm_detail_field): filters = { - "parent": self.subcontracting_order, + "parent": self.get(self.subcontract_data.order_field), "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) + order_rm_detail = frappe.db.get_value( + self.subcontract_data.order_supplied_items_field, filters, "name" + ) + if order_rm_detail: + se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail) elif backflush_raw_materials_based_on == "Material Transferred for Subcontract": for row in self.items: if not row.subcontracted_item: @@ -905,17 +941,19 @@ class StockEntry(StockController): row.idx, frappe.bold(row.item_code) ) ) - elif not row.sco_rm_detail: + elif not row.get(self.subcontract_data.rm_detail_field): filters = { - "parent": self.subcontracting_order, + "parent": self.get(self.subcontract_data.order_field), "docstatus": 1, "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item, } - 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) + order_rm_detail = frappe.db.get_value( + self.subcontract_data.order_supplied_items_field, filters, "name" + ) + if order_rm_detail: + row.db_set(self.subcontract_data.rm_detail_field, order_rm_detail) def validate_bom(self): for d in self.get("items"): @@ -1263,12 +1301,12 @@ class StockEntry(StockController): if ( self.purpose == "Send to Subcontractor" - and self.get("subcontracting_order") + and self.get(self.subcontract_data.order_field) and args.get("item_code") ): subcontract_items = frappe.get_all( - "Subcontracting Order Supplied Item", - {"parent": self.subcontracting_order, "rm_item_code": args.get("item_code")}, + self.subcontract_data.order_supplied_items_field, + {"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")}, "main_item_code", ) @@ -1362,18 +1400,18 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - # Get SCO Supplied Items Details - if self.subcontracting_order and self.purpose == "Send to Subcontractor": - # Get SCO Supplied Items Details - parent = frappe.qb.DocType("Subcontracting Order") - child = frappe.qb.DocType("Subcontracting Order Supplied Item") + # Get Subcontract Order Supplied Items Details + if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor": + # Get Subcontract Order Supplied Items Details + parent = frappe.qb.DocType(self.subcontract_data.order_doctype) + child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field) item_wh = ( frappe.qb.from_(parent) .inner_join(child) .on(parent.name == child.parent) .select(child.rm_item_code, child.reserve_warehouse) - .where(parent.name == self.subcontracting_order) + .where(parent.name == self.get(self.subcontract_data.order_field)) ).run(as_list=True) item_wh = frappe._dict(item_wh) @@ -1381,8 +1419,8 @@ class StockEntry(StockController): for item in item_dict.values(): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse - # Get Reserve Warehouse from SCO - if self.subcontracting_order and self.purpose == "Send to Subcontractor": + # Get Reserve Warehouse from Subcontract Order + if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor": item["from_warehouse"] = item_wh.get(item.item_code) item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" @@ -1519,7 +1557,9 @@ class StockEntry(StockController): fetch_qty_in_stock_uom=False, ) - used_alternative_items = get_used_alternative_items(work_order=self.work_order) + used_alternative_items = get_used_alternative_items( + subcontract_order_field=self.subcontract_data.order_field, work_order=self.work_order + ) for item in item_dict.values(): # if source warehouse presents in BOM set from_warehouse as bom source_warehouse if item["allow_alternative_item"]: @@ -1925,7 +1965,7 @@ class StockEntry(StockController): se_child.is_process_loss = item_row.get("is_process_loss", 0) for field in [ - "sco_rm_detail", + self.subcontract_data.rm_detail_field, "original_item", "expense_account", "description", @@ -1999,33 +2039,37 @@ class StockEntry(StockController): else: frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) - def update_subcontracting_order_supplied_items(self): - if self.subcontracting_order and ( - self.purpose in ["Send to Subcontractor", "Material Transfer"] + def update_subcontract_order_supplied_items(self): + if self.get(self.subcontract_data.order_field) and ( + self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return ): - # Get SCO Supplied Items Details - sco_supplied_items = frappe.db.get_all( - "Subcontracting Order Supplied Item", - filters={"parent": self.subcontracting_order}, + # Get Subcontract Order Supplied Items Details + order_supplied_items = frappe.db.get_all( + self.subcontract_data.order_supplied_items_field, + filters={"parent": self.get(self.subcontract_data.order_field)}, fields=["name", "rm_item_code", "reserve_warehouse"], ) - # Get Items Supplied in Stock Entries against SCO - supplied_items = get_supplied_items(self.subcontracting_order) + # Get Items Supplied in Stock Entries against Subcontract Order + supplied_items = get_supplied_items( + self.get(self.subcontract_data.order_field), + self.subcontract_data.rm_detail_field, + self.subcontract_data.order_field, + ) - for row in sco_supplied_items: + for row in order_supplied_items: key, item = row.name, {} if not supplied_items.get(key): - # no stock transferred against SCO Supplied Items row + # no stock transferred against Subcontract Order Supplied Items row item = {"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0} else: item = supplied_items.get(key) - frappe.db.set_value("Subcontracting Order Supplied Item", row.name, item) + frappe.db.set_value(self.subcontract_data.order_supplied_items_field, row.name, item) # RM Item-Reserve Warehouse Dict - item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in sco_supplied_items} + item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in order_supplied_items} for d in self.get("items"): # Update reserved sub contracted quantity in bin based on Supplied Item Details and @@ -2382,13 +2426,13 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): return operating_cost_per_unit -def get_used_alternative_items(subcontracting_order=None, work_order=None): +def get_used_alternative_items( + subcontract_order=None, subcontract_order_field="subcontracting_order", work_order=None +): cond = "" - if subcontracting_order: - cond = "and ste.purpose = 'Send to Subcontractor' and ste.subcontracting_order = '{0}'".format( - subcontracting_order - ) + if subcontract_order: + cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'" elif work_order: cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( work_order @@ -2524,25 +2568,27 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None): return sample_quantity -def get_supplied_items(subcontracting_order): +def get_supplied_items( + subcontract_order, rm_detail_field="sco_rm_detail", subcontract_order_field="subcontracting_order" +): fields = [ "`tabStock Entry Detail`.`transfer_qty`", "`tabStock Entry`.`is_return`", - "`tabStock Entry Detail`.`sco_rm_detail`", + f"`tabStock Entry Detail`.`{rm_detail_field}`", "`tabStock Entry Detail`.`item_code`", ] filters = [ ["Stock Entry", "docstatus", "=", 1], - ["Stock Entry", "subcontracting_order", "=", subcontracting_order], + ["Stock Entry", subcontract_order_field, "=", subcontract_order], ] supplied_item_details = {} for row in frappe.get_all("Stock Entry", fields=fields, filters=filters): - if not row.sco_rm_detail: + if not row.get(rm_detail_field): continue - key = row.sco_rm_detail + key = row.get(rm_detail_field) 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 e701c14aa98..c23548c2d01 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -240,8 +240,13 @@ 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") and item.is_stock_item: - throw(_("Item {0} must be a Non-Stock Item").format(item.name)) + if args.get("is_subcontracted"): + if args.get("is_old_subcontracting_flow"): + if item.is_sub_contracted_item != 1: + throw(_("Item {0} must be a Sub-contracted Item").format(item.name)) + else: + if 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): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 9293dde77f0..8d82c7316a3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -738,6 +738,13 @@ class update_entries_after(object): "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate ) + # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice + if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): + doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) + doc.update_valuation_rate(reset_outgoing_rate=False) + for d in doc.items + doc.supplied_items: + d.db_update() + def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate): if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index c9e4577cea3..dbd337afd43 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -24,7 +24,8 @@ frappe.ui.form.on('Subcontracting Order', { return { filters: { docstatus: 1, - is_subcontracted: 1 + is_subcontracted: 1, + is_old_subcontracting_flow: 0 } }; }); @@ -115,10 +116,14 @@ frappe.ui.form.on('Subcontracting Order', { if (sco_rm_details && sco_rm_details.length) { frm.add_custom_button(__('Return of Components'), () => { frm.call({ - method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.get_materials_from_supplier', + method: 'erpnext.controllers.subcontracting_controller.get_materials_from_supplier', freeze: true, freeze_message: __('Creating Stock Entry'), - args: { subcontracting_order: frm.doc.name, sco_rm_details: sco_rm_details }, + args: { + subcontract_order: frm.doc.name, + rm_details: sco_rm_details, + order_doctype: cur_frm.doc.doctype + }, callback: function (r) { if (r && r.message) { const doc = frappe.model.sync(r.message); @@ -306,10 +311,11 @@ erpnext.buying.SubcontractingOrderController = class SubcontractingOrderControll make_rm_stock_entry(rm_items) { frappe.call({ - method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_rm_stock_entry', + method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry', args: { - subcontracting_order: cur_frm.doc.name, - rm_items: rm_items + subcontract_order: cur_frm.doc.name, + rm_items: rm_items, + order_doctype: cur_frm.doc.doctype }, callback: (r) => { var doclist = frappe.model.sync(r.message); diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index d12c9e825c4..3655910efb1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -1,8 +1,6 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import json - import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc @@ -42,6 +40,9 @@ class SubcontractingOrder(SubcontractingController): if not po.is_subcontracted: frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting.")) + if po.is_old_subcontracting_flow: + frappe.throw(_("Please select a valid Purchase Order that has Service Items.")) + if po.docstatus != 1: msg = f"Please submit Purchase Order {po.name} before proceeding." frappe.throw(_(msg)) @@ -227,143 +228,6 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None): return target_doc -def get_item_details(items): - item = frappe.qb.DocType("Item") - item_list = ( - frappe.qb.from_(item) - .select(item.item_code, item.description, item.allow_alternative_item) - .where(item.name.isin(items)) - .run(as_dict=True) - ) - - item_details = {} - for item in item_list: - item_details[item.item_code] = item - - return item_details - - -@frappe.whitelist() -def make_rm_stock_entry(subcontracting_order, rm_items): - rm_items_list = rm_items - - if isinstance(rm_items, str): - rm_items_list = json.loads(rm_items) - elif not rm_items: - frappe.throw(_("No Items available for transfer")) - - if rm_items_list: - fg_items = list(set(item["item_code"] for item in rm_items_list)) - else: - frappe.throw(_("No Items selected for transfer")) - - if subcontracting_order: - subcontracting_order = frappe.get_doc("Subcontracting Order", subcontracting_order) - - if fg_items: - items = tuple(set(item["rm_item_code"] for item in rm_items_list)) - item_wh = get_item_details(items) - - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Send to Subcontractor" - stock_entry.subcontracting_order = subcontracting_order.name - stock_entry.supplier = subcontracting_order.supplier - stock_entry.supplier_name = subcontracting_order.supplier_name - stock_entry.supplier_address = subcontracting_order.supplier_address - stock_entry.address_display = subcontracting_order.address_display - stock_entry.company = subcontracting_order.company - stock_entry.to_warehouse = subcontracting_order.supplier_warehouse - stock_entry.set_stock_entry_type() - - for item_code in fg_items: - for rm_item_data in rm_items_list: - if rm_item_data["item_code"] == item_code: - rm_item_code = rm_item_data["rm_item_code"] - items_dict = { - rm_item_code: { - "sco_rm_detail": rm_item_data.get("name"), - "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item_data["qty"], - "from_warehouse": rm_item_data["warehouse"], - "stock_uom": rm_item_data["stock_uom"], - "serial_no": rm_item_data.get("serial_no"), - "batch_no": rm_item_data.get("batch_no"), - "main_item_code": rm_item_data["item_code"], - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), - } - } - stock_entry.add_to_stock_entry_detail(items_dict) - return stock_entry.as_dict() - else: - frappe.throw(_("No Items selected for transfer")) - return subcontracting_order.name - - -def add_items_in_ste(ste_doc, row, qty, sco_rm_details, batch_no=None): - item = ste_doc.append("items", row.item_details) - - sco_rm_detail = list(set(row.sco_rm_details).intersection(sco_rm_details)) - item.update( - { - "qty": qty, - "batch_no": batch_no, - "basic_rate": row.item_details["rate"], - "sco_rm_detail": sco_rm_detail[0] if sco_rm_detail else "", - "s_warehouse": row.item_details["t_warehouse"], - "t_warehouse": row.item_details["s_warehouse"], - "item_code": row.item_details["rm_item_code"], - "subcontracted_item": row.item_details["main_item_code"], - "serial_no": "\n".join(row.serial_no) if row.serial_no else "", - } - ) - - -def make_return_stock_entry_for_subcontract(available_materials, sco_doc, sco_rm_details): - ste_doc = frappe.new_doc("Stock Entry") - ste_doc.purpose = "Material Transfer" - - ste_doc.subcontracting_order = sco_doc.name - ste_doc.company = sco_doc.company - ste_doc.is_return = 1 - - for key, value in available_materials.items(): - if not value.qty: - continue - - if value.batch_no: - for batch_no, qty in value.batch_no.items(): - if qty > 0: - add_items_in_ste(ste_doc, value, value.qty, sco_rm_details, batch_no) - else: - add_items_in_ste(ste_doc, value, value.qty, sco_rm_details) - - ste_doc.set_stock_entry_type() - ste_doc.calculate_rate_and_amount() - - return ste_doc - - -@frappe.whitelist() -def get_materials_from_supplier(subcontracting_order, sco_rm_details): - if isinstance(sco_rm_details, str): - sco_rm_details = json.loads(sco_rm_details) - - doc = frappe.get_cached_doc("Subcontracting Order", subcontracting_order) - doc.initialized_fields() - doc.subcontracting_orders = [doc.name] - doc.get_available_materials() - - if not doc.available_materials: - frappe.throw( - _("Materials are already received against the Subcontracting Order {0}").format( - subcontracting_order - ) - ) - - return make_return_stock_entry_for_subcontract(doc.available_materials, doc, sco_rm_details) - - @frappe.whitelist() def update_subcontracting_order_status(sco): if isinstance(sco, str): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 1454f1aced4..e579834963a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -7,6 +7,7 @@ import frappe from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order +from erpnext.controllers.subcontracting_controller import make_rm_stock_entry from erpnext.controllers.tests.test_subcontracting_controller import ( get_rm_items, get_subcontracting_order, @@ -22,7 +23,6 @@ from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - make_rm_stock_entry, make_subcontracting_receipt, ) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 5ee49d8502c..0c4ec6fb76f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -3,7 +3,7 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, getdate, nowdate +from frappe.utils import cint, getdate, nowdate from erpnext.controllers.subcontracting_controller import SubcontractingController @@ -78,7 +78,7 @@ class SubcontractingReceipt(SubcontractingController): self.update_status_updater_args() self.update_prevdoc_status() self.set_subcontracting_order_status() - self.set_consumed_qty_in_sco() + self.set_consumed_qty_in_subcontract_order() self.update_stock_ledger() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit @@ -97,7 +97,7 @@ class SubcontractingReceipt(SubcontractingController): self.make_gl_entries_on_cancel() self.repost_future_sle_and_gle() self.delete_auto_created_batches() - self.set_consumed_qty_in_sco() + self.set_consumed_qty_in_subcontract_order() self.set_subcontracting_order_status() self.update_status() @@ -162,17 +162,6 @@ class SubcontractingReceipt(SubcontractingController): if not item.expense_account: item.expense_account = expense_account - @frappe.whitelist() - def get_current_stock(self): - for item in self.get("supplied_items"): - if self.supplier_warehouse: - actual_qty = frappe.db.get_value( - "Bin", - {"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse}, - "actual_qty", - ) - item.current_stock = flt(actual_qty) or 0 - def update_status(self, status=None, update_modified=False): if self.docstatus >= 1 and not status: if self.docstatus == 1: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 156a2711fae..763e76882e0 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -119,7 +119,7 @@ class TestSubcontractingReceipt(FrappeTestCase): receive more than the required qty in the SCO. Expected Result: Error Raised for Over Receipt against SCO. """ - from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + from erpnext.controllers.subcontracting_controller import ( make_rm_stock_entry as make_subcontract_transfer_entry, ) from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( @@ -188,8 +188,8 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertRaises(frappe.ValidationError, scr2.submit) def test_subcontracted_scr_for_multi_transfer_batches(self): + from erpnext.controllers.subcontracting_controller import make_rm_stock_entry from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( - make_rm_stock_entry, make_subcontracting_receipt, ) From 8e4458e0e625b77bee2639305b84395198e8b055 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 30 Jun 2022 16:47:43 +0530 Subject: [PATCH 35/41] fix: failing test Removed this test case as the new POs will not have the Supplied Items table. --- .../purchase_order/test_purchase_order.py | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 728f749b5d8..bd7e4e8d865 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -136,43 +136,6 @@ class TestPurchaseOrder(FrappeTestCase): # ordered qty decreases as ordered qty is 0 (deleted row) self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 - def test_supplied_items_validations_on_po_update_after_submit(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100) - item = po.items[0] - - original_supplied_items = {po.name: po.required_qty for po in po.supplied_items} - - # Just update rate - trans_item = [ - { - "item_code": "_Test FG Item", - "rate": 20, - "qty": 5, - "conversion_factor": 1.0, - "docname": item.name, - } - ] - update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) - po.reload() - - new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} - self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys())) - - # Update qty to 2x - trans_item[0]["qty"] *= 2 - update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) - po.reload() - - new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} - self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values())) - - # Set transfer qty and attempt to update qty, shouldn't be allowed - po.supplied_items[0].supplied_qty = 2 - po.supplied_items[0].db_update() - trans_item[0]["qty"] *= 2 - with self.assertRaises(frappe.ValidationError): - update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) - def test_update_child(self): mr = make_material_request(qty=10) po = make_purchase_order(mr.name) From b86710bb9a72c9b63d6e2b32d0bdf2c6601aebd0 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 30 Jun 2022 17:28:42 +0530 Subject: [PATCH 36/41] fix(ui): hide "Update Items" button based on subcontracting conditions --- .../doctype/purchase_order/purchase_order.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2b1e3ce9140..582f23e3114 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -157,14 +157,17 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if(!in_list(["Closed", "Delivered"], doc.status)) { if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { - this.frm.add_custom_button(__('Update Items'), () => { - erpnext.utils.update_child_items({ - frm: this.frm, - child_docname: "items", - child_doctype: "Purchase Order Detail", - cannot_add_row: false, - }) - }); + // Don't add Update Items button if the PO is following the new subcontracting flow. + if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) { + this.frm.add_custom_button(__('Update Items'), () => { + erpnext.utils.update_child_items({ + frm: this.frm, + child_docname: "items", + child_doctype: "Purchase Order Detail", + cannot_add_row: false, + }) + }); + } } if (this.frm.has_perm("submit")) { if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) { From fd162f9b14935f29e0e4fde10063a74329d13e30 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 1 Jul 2022 16:51:19 +0530 Subject: [PATCH 37/41] fix: supplier warehouse in PR --- erpnext/controllers/buying_controller.py | 23 ++++++++----------- .../purchase_receipt/purchase_receipt.js | 4 ++-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4db8ccb5b8d..97843f748e0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -323,22 +323,19 @@ class BuyingController(SubcontractingController): d.margin_rate_or_amount = 0.0 def validate_for_subcontracting(self): - if self.is_subcontracted: + if self.is_subcontracted and self.get("is_old_subcontracting_flow"): if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype)) - if self.get("is_old_subcontracting_flow"): - for item in self.get("items"): - if item in self.sub_contracted_items and not item.bom: - frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - - if self.doctype != "Purchase Order": - return - - for row in self.get("supplied_items"): - if not row.reserve_warehouse: - msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" - frappe.throw(_(msg)) + for item in self.get("items"): + if item in self.sub_contracted_items and not item.bom: + frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) + if self.doctype != "Purchase Order": + return + for row in self.get("supplied_items"): + if not row.reserve_warehouse: + msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" + frappe.throw(_(msg)) else: for item in self.get("items"): if item.get("bom"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index e6fcb78f129..312c166f8b7 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -198,7 +198,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status")) } - this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); + this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_old_subcontracting_flow); } make_purchase_invoice() { @@ -300,7 +300,7 @@ frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) { erpnext.buying.get_default_bom(frm); } - frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); + frm.toggle_reqd("supplier_warehouse", frm.doc.is_old_subcontracting_flow); }); frappe.ui.form.on('Purchase Receipt Item', { From caeaa3f94086c03154e7a9ee369ce6a4c80f030a Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Sat, 2 Jul 2022 06:20:09 +0530 Subject: [PATCH 38/41] fix: multiple SCO against a PO --- erpnext/buying/doctype/purchase_order/purchase_order.py | 9 +++++++++ .../doctype/subcontracting_order/subcontracting_order.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 6f960a2c65e..cd58d25136a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -691,3 +691,12 @@ def get_mapped_subcontracting_order(source_name, target_doc=None): item.warehouse = source_doc.items[idx].warehouse return target_doc + + +@frappe.whitelist() +def is_subcontracting_order_created(po_name) -> bool: + count = frappe.db.count( + "Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]} + ) + + return True if count else False diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 3655910efb1..73ab43401b5 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -6,6 +6,7 @@ from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt +from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty from erpnext.stock.utils import get_bin @@ -36,7 +37,15 @@ class SubcontractingOrder(SubcontractingController): def validate_purchase_order_for_subcontracting(self): if self.purchase_order: + if is_subcontracting_order_created(self.purchase_order): + frappe.throw( + _( + "Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one." + ) + ) + po = frappe.get_doc("Purchase Order", self.purchase_order) + if not po.is_subcontracted: frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting.")) From 687329f5713a31873da575b3ae8b089dd2dead69 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 5 Jul 2022 08:31:31 +0530 Subject: [PATCH 39/41] chore: update fg_item_qty based on qty in PO Item --- .../buying/doctype/purchase_order/purchase_order.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 582f23e3114..b5051eb2862 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -112,6 +112,16 @@ frappe.ui.form.on("Purchase Order Item", { set_schedule_date(frm); } } + }, + + qty: function(frm, cdt, cdn) { + if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) { + var row = locals[cdt][cdn]; + + if (row.qty) { + row.fg_item_qty = row.qty; + } + } } }); From 6f7e67db9d7811eabbcb8259471a31c65f6b349a Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 5 Jul 2022 08:44:35 +0530 Subject: [PATCH 40/41] chore: hide "Duplicate" button in PO --- .../buying/doctype/purchase_order/purchase_order.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index b5051eb2862..fbb42fe2f64 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -45,8 +45,17 @@ frappe.ui.form.on("Purchase Order", { }, refresh: function(frm) { - if(frm.doc.is_old_subcontracting_flow) + if(frm.doc.is_old_subcontracting_flow) { frm.trigger('get_materials_from_supplier'); + + $('a.grey-link').each(function () { + var id = $(this).children(':first-child').attr('data-label'); + if (id == 'Duplicate') { + $(this).remove(); + return false; + } + }); + } }, get_materials_from_supplier: function(frm) { From a7161d387554798185e4e00dfb64700ad94345ad Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Tue, 5 Jul 2022 09:15:28 +0530 Subject: [PATCH 41/41] fix: SCO status on SCR cancel --- .../doctype/subcontracting_order/subcontracting_order.py | 3 ++- .../subcontracting_order/test_subcontracting_order.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 73ab43401b5..71cdc94a3ae 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -192,10 +192,11 @@ class SubcontractingOrder(SubcontractingController): status = "Partial Material Transferred" if total_supplied_qty >= total_required_qty: status = "Material Transferred" + else: + status = "Open" elif self.docstatus == 2: status = "Cancelled" - if status: frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index e579834963a..94bb38e9803 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -96,6 +96,12 @@ class TestSubcontractingOrder(FrappeTestCase): sco.load_from_db() self.assertEqual(sco.status, "Completed") + # Partially Received (scr cancelled) + scr.load_from_db() + scr.cancel() + sco.load_from_db() + self.assertEqual(sco.status, "Partially Received") + def test_make_rm_stock_entry(self): sco = get_subcontracting_order() rm_items = get_rm_items(sco.supplied_items)