From 6d89b2fa28f4f027c9eb6b3189d73fe3904f65f4 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 18 Jun 2022 15:46:59 +0530 Subject: [PATCH] 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, )