diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 7e3597e491..ec861a2787 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -539,7 +539,7 @@ frappe.ui.form.on("Purchase Invoice", { }, add_custom_buttons: function(frm) { - if (frm.doc.per_received < 100) { + if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) { frm.add_custom_button(__('Purchase Receipt'), () => { frm.events.make_purchase_receipt(frm); }, __('Create')); @@ -572,9 +572,10 @@ frappe.ui.form.on("Purchase Invoice", { }, is_subcontracted: function(frm) { - if (frm.doc.is_subcontracted) { + 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.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 9f87c5ab54..534b879e78 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -169,7 +169,8 @@ "column_break_114", "auto_repeat", "update_auto_repeat_reference", - "per_received" + "per_received", + "is_old_subcontracting_flow" ], "fields": [ { @@ -547,7 +548,8 @@ "fieldname": "is_subcontracted", "fieldtype": "Check", "label": "Is Subcontracted", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "fieldname": "items_section", @@ -1365,7 +1367,7 @@ "width": "50px" }, { - "depends_on": "eval:doc.update_stock && doc.is_subcontracted", + "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", "label": "Supplier Warehouse", @@ -1416,13 +1418,21 @@ "label": "Advance Tax", "options": "Advance Tax", "read_only": 1 - } + }, + { + "default": "0", + "fieldname": "is_old_subcontracting_flow", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Old Subcontracting Flow", + "read_only": 1 + } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-11-25 13:31:02.716727", + "modified": "2022-06-15 15:40:58.527065", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 4e0d1c966d..775d2550a9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -502,7 +502,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() - self.set_consumed_qty_in_po() + + 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") @@ -1405,7 +1408,9 @@ class PurchaseInvoice(BuyingController): if self.update_stock == 1: self.update_stock_ledger() self.delete_auto_created_batches() - self.set_consumed_qty_in_po() + + if self.is_old_subcontracting_flow: + self.set_consumed_qty_in_subcontract_order() self.make_gl_entries_on_cancel() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 427e4814e7..e55d3a70af 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -470,37 +470,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.total, expected_values[i][2]) - def test_purchase_invoice_with_subcontracted_item(self): - wrapper = frappe.copy_doc(test_records[0]) - wrapper.get("items")[0].item_code = "_Test FG Item" - wrapper.insert() - wrapper.load_from_db() - - expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]] - for i, item in enumerate(wrapper.get("items")): - self.assertEqual(item.item_code, expected_values[i][0]) - self.assertEqual(item.item_tax_amount, expected_values[i][1]) - self.assertEqual(item.valuation_rate, expected_values[i][2]) - - self.assertEqual(wrapper.base_net_total, 1250) - - # tax amounts - expected_values = [ - ["_Test Account Shipping Charges - _TC", 100, 1350], - ["_Test Account Customs Duty - _TC", 125, 1350], - ["_Test Account Excise Duty - _TC", 140, 1490], - ["_Test Account Education Cess - _TC", 2.8, 1492.8], - ["_Test Account S&H Education Cess - _TC", 1.4, 1494.2], - ["_Test Account CST - _TC", 29.88, 1524.08], - ["_Test Account VAT - _TC", 156.25, 1680.33], - ["_Test Account Discount - _TC", 168.03, 1512.30], - ] - - for i, tax in enumerate(wrapper.get("taxes")): - self.assertEqual(tax.account_head, expected_values[i][0]) - self.assertEqual(tax.tax_amount, expected_values[i][1]) - self.assertEqual(tax.total, expected_values[i][2]) - def test_purchase_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -961,30 +930,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.cancel() self.assertEqual(actual_qty_0, get_qty_after_transaction()) - def test_subcontracting_via_purchase_invoice(self): - from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - update_backflush_based_on("BOM") - make_stock_entry( - item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="_Test Warehouse 1 - _TC", - qty=100, - basic_rate=100, - ) - - pi = make_purchase_invoice( - item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1 - ) - - self.assertEqual(len(pi.get("supplied_items")), 2) - - rm_supp_cost = sum(d.amount for d in pi.get("supplied_items")) - self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) - def test_rejected_serial_no(self): pi = make_purchase_invoice( item_code="_Test Serialized Item With Series", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 1f79d4761e..7fa2fe2a66 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -619,10 +619,13 @@ "search_index": 1 }, { + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", - "options": "BOM" + "options": "BOM", + "read_only": 1, + "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow" }, { "default": "0", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 33dbe3f468..fbb42fe2f6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -8,15 +8,17 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Purchase Order", { setup: function(frm) { - frm.set_query("reserve_warehouse", "supplied_items", function() { - return { - filters: { - "company": frm.doc.company, - "name": ['!=', frm.doc.supplier_warehouse], - "is_group": 0 + 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" }) @@ -28,12 +30,67 @@ frappe.ui.form.on("Purchase Order", { } }); + frm.set_query("fg_item", "items", function() { + return { + filters: { + 'is_sub_contracted_item': 1, + 'default_bom': ['!=', ''] + } + } + }); }, company: function(frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, + refresh: function(frm) { + if(frm.doc.is_old_subcontracting_flow) { + frm.trigger('get_materials_from_supplier'); + + $('a.grey-link').each(function () { + var id = $(this).children(':first-child').attr('data-label'); + if (id == 'Duplicate') { + $(this).remove(); + return false; + } + }); + } + }, + + get_materials_from_supplier: function(frm) { + 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){ @@ -52,39 +109,6 @@ frappe.ui.form.on("Purchase Order", { frm.set_value("tax_withholding_category", frm.supplier_tds); } }, - - refresh: function(frm) { - frm.trigger('get_materials_from_supplier'); - }, - - get_materials_from_supplier: function(frm) { - let po_details = []; - - if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { - frm.doc.supplied_items.forEach(d => { - if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { - po_details.push(d.name) - } - }); - } - - if (po_details && po_details.length) { - frm.add_custom_button(__('Return of Components'), () => { - frm.call({ - method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier', - freeze: true, - freeze_message: __('Creating Stock Entry'), - args: { purchase_order: frm.doc.name, po_details: po_details }, - callback: function(r) { - if (r && r.message) { - const doc = frappe.model.sync(r.message); - frappe.set_route("Form", doc[0].doctype, doc[0].name); - } - } - }); - }, __('Create')); - } - } }); frappe.ui.form.on("Purchase Order Item", { @@ -97,6 +121,16 @@ frappe.ui.form.on("Purchase Order Item", { set_schedule_date(frm); } } + }, + + qty: function(frm, cdt, cdn) { + if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) { + var row = locals[cdt][cdn]; + + if (row.qty) { + row.fg_item_qty = row.qty; + } + } } }); @@ -105,12 +139,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e this.frm.custom_make_buttons = { 'Purchase Receipt': 'Purchase Receipt', 'Purchase Invoice': 'Purchase Invoice', - 'Stock Entry': 'Material to Supplier', 'Payment Entry': 'Payment', + 'Subcontracting Order': 'Subcontracting Order', + 'Stock Entry': 'Material to Supplier' } super.setup(); - } refresh(doc, cdt, cdn) { @@ -142,14 +176,17 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if(!in_list(["Closed", "Delivered"], doc.status)) { if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { - this.frm.add_custom_button(__('Update Items'), () => { - erpnext.utils.update_child_items({ - frm: this.frm, - child_docname: "items", - child_doctype: "Purchase Order Detail", - cannot_add_row: false, - }) - }); + // Don't add Update Items button if the PO is following the new subcontracting flow. + if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) { + this.frm.add_custom_button(__('Update Items'), () => { + erpnext.utils.update_child_items({ + frm: this.frm, + child_docname: "items", + child_doctype: "Purchase Order Detail", + cannot_add_row: false, + }) + }); + } } if (this.frm.has_perm("submit")) { if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) { @@ -177,9 +214,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e if (doc.status != "On Hold") { if(flt(doc.per_received) < 100 && allow_receipt) { cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); - if(doc.is_subcontracted && me.has_unsupplied_items()) { - cur_frm.add_custom_button(__('Material to Supplier'), - function() { me.make_stock_entry(); }, __("Transfer")); + if (doc.is_subcontracted) { + 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) @@ -370,10 +413,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e _make_rm_stock_entry(rm_items) { frappe.call({ - method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry", + method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry", args: { - purchase_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: function(r) { @@ -405,6 +449,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e }) } + make_subcontracting_order() { + frappe.model.open_mapped_doc({ + method: "erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order", + frm: cur_frm, + freeze_message: __("Creating Subcontracting Order ...") + }) + } + add_from_mappers() { var me = this; this.frm.add_custom_button(__('Material Request'), @@ -613,15 +665,17 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc, } } -cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) { - var d = locals[cdt][cdn] - return { - filters: [ - ['BOM', 'item', '=', d.item_code], - ['BOM', 'is_active', '=', '1'], - ['BOM', 'docstatus', '=', '1'], - ['BOM', 'company', '=', doc.company] - ] +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] + ] + } } } @@ -634,7 +688,7 @@ function set_schedule_date(frm) { frappe.provide("erpnext.buying"); frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) { - if (frm.doc.is_subcontracted) { + 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 b365a836cd..aa50487d78 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -16,6 +16,8 @@ "supplier_name", "apply_tds", "tax_withholding_category", + "is_subcontracted", + "supplier_warehouse", "column_break1", "company", "transaction_date", @@ -55,10 +57,7 @@ "price_list_currency", "plc_conversion_rate", "ignore_pricing_rule", - "sec_warehouse", - "is_subcontracted", - "col_break_warehouse", - "supplier_warehouse", + "section_break_45", "before_items_section", "scan_barcode", "items_col_break", @@ -142,7 +141,8 @@ "party_account_currency", "is_internal_supplier", "represents_company", - "inter_company_order_reference" + "inter_company_order_reference", + "is_old_subcontracting_flow" ], "fields": [ { @@ -158,7 +158,8 @@ "hidden": 1, "label": "Title", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "reqd": 1 }, { "fieldname": "naming_series", @@ -443,11 +444,6 @@ "permlevel": 1, "print_hide": 1 }, - { - "fieldname": "sec_warehouse", - "fieldtype": "Section Break", - "label": "Subcontracting" - }, { "description": "Sets 'Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", @@ -456,15 +452,10 @@ "options": "Warehouse", "print_hide": 1 }, - { - "fieldname": "col_break_warehouse", - "fieldtype": "Column Break" - }, { "default": "0", "fieldname": "is_subcontracted", "fieldtype": "Check", - "in_standard_filter": 1, "label": "Is Subcontracted", "print_hide": 1 }, @@ -1142,6 +1133,10 @@ "label": "Tax Withholding Category", "options": "Tax Withholding Category" }, + { + "fieldname": "section_break_45", + "fieldtype": "Section Break" + }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", @@ -1163,13 +1158,21 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "default": "0", + "fieldname": "is_old_subcontracting_flow", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Old Subcontracting Flow", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-04-26 12:16:38.694276", + "modified": "2022-06-15 15:40:58.527065", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 44426ba43d..cd58d25136 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -69,8 +69,12 @@ class PurchaseOrder(BuyingController): self.validate_with_previous_doc() self.validate_for_subcontracting() self.validate_minimum_order_qty() - self.validate_bom_for_subcontracting_items() - self.create_raw_materials_supplied("supplied_items") + + 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( self.doctype, self.supplier, self.company, self.inter_company_order_reference @@ -194,12 +198,38 @@ class PurchaseOrder(BuyingController): ) def validate_bom_for_subcontracting_items(self): - if self.is_subcontracted: + 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 and not self.is_old_subcontracting_flow: for item in self.items: - if not item.bom: + if not item.fg_item: frappe.throw( - _("BOM is not specified for subcontracting item {0} at row {1}").format( - item.item_code, item.idx + _("Row #{0}: Finished Good Item is not specified for service item {1}").format( + item.idx, item.item_code + ) + ) + else: + if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): + frappe.throw( + _( + "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( + item.idx, item.item_code ) ) @@ -294,9 +324,7 @@ class PurchaseOrder(BuyingController): self.set_status(update=True, status=status) self.update_requested_qty() self.update_ordered_qty() - if self.is_subcontracted: - self.update_reserved_qty_for_subcontract() - + self.update_reserved_qty_for_subcontract() self.notify_update() clear_doctype_notifications(self) @@ -310,9 +338,7 @@ class PurchaseOrder(BuyingController): self.update_requested_qty() self.update_ordered_qty() self.validate_budget() - - if self.is_subcontracted: - self.update_reserved_qty_for_subcontract() + self.update_reserved_qty_for_subcontract() frappe.get_doc("Authorization Control").validate_approving_authority( self.doctype, self.company, self.base_grand_total @@ -332,9 +358,7 @@ class PurchaseOrder(BuyingController): if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() - if self.is_subcontracted: - self.update_reserved_qty_for_subcontract() - + self.update_reserved_qty_for_subcontract() self.check_on_hold_or_closed_status() frappe.db.set(self, "status", "Cancelled") @@ -405,10 +429,11 @@ class PurchaseOrder(BuyingController): item.received_qty = item.qty def update_reserved_qty_for_subcontract(self): - for d in self.supplied_items: - if d.rm_item_code: - stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse) - stock_bin.update_reserved_qty_for_sub_contracting() + 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 @@ -587,80 +612,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions return doc -@frappe.whitelist() -def make_rm_stock_entry(purchase_order, rm_items): - rm_items_list = rm_items - - if isinstance(rm_items, str): - rm_items_list = json.loads(rm_items) - elif not rm_items: - frappe.throw(_("No Items available for transfer")) - - if rm_items_list: - fg_items = list(set(d["item_code"] for d in rm_items_list)) - else: - frappe.throw(_("No Items selected for transfer")) - - if purchase_order: - purchase_order = frappe.get_doc("Purchase Order", purchase_order) - - if fg_items: - items = tuple(set(d["rm_item_code"] for d in rm_items_list)) - item_wh = get_item_details(items) - - stock_entry = frappe.new_doc("Stock Entry") - stock_entry.purpose = "Send to Subcontractor" - stock_entry.purchase_order = purchase_order.name - stock_entry.supplier = purchase_order.supplier - stock_entry.supplier_name = purchase_order.supplier_name - stock_entry.supplier_address = purchase_order.supplier_address - stock_entry.address_display = purchase_order.address_display - stock_entry.company = purchase_order.company - stock_entry.to_warehouse = purchase_order.supplier_warehouse - stock_entry.set_stock_entry_type() - - for item_code in fg_items: - for rm_item_data in rm_items_list: - if rm_item_data["item_code"] == item_code: - rm_item_code = rm_item_data["rm_item_code"] - items_dict = { - rm_item_code: { - "po_detail": rm_item_data.get("name"), - "item_name": rm_item_data["item_name"], - "description": item_wh.get(rm_item_code, {}).get("description", ""), - "qty": rm_item_data["qty"], - "from_warehouse": rm_item_data["warehouse"], - "stock_uom": rm_item_data["stock_uom"], - "serial_no": rm_item_data.get("serial_no"), - "batch_no": rm_item_data.get("batch_no"), - "main_item_code": rm_item_data["item_code"], - "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), - } - } - stock_entry.add_to_stock_entry_detail(items_dict) - - stock_entry.set_missing_values() - return stock_entry.as_dict() - else: - frappe.throw(_("No Items selected for transfer")) - return purchase_order.name - - -def get_item_details(items): - item_details = {} - for d in frappe.db.sql( - """select item_code, description, allow_alternative_item from `tabItem` - where name in ({0})""".format( - ", ".join(["%s"] * len(items)) - ), - items, - as_dict=1, - ): - item_details[d.item_code] = d - - return item_details - - def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -691,61 +642,61 @@ def make_inter_company_sales_order(source_name, target_doc=None): @frappe.whitelist() -def get_materials_from_supplier(purchase_order, po_details): - if isinstance(po_details, str): - po_details = json.loads(po_details) - - doc = frappe.get_cached_doc("Purchase Order", purchase_order) - doc.initialized_fields() - doc.purchase_orders = [doc.name] - doc.get_available_materials() - - if not doc.available_materials: - frappe.throw( - _("Materials are already received against the purchase order {0}").format(purchase_order) - ) - - return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details) +def make_subcontracting_order(source_name, target_doc=None): + return get_mapped_subcontracting_order(source_name, target_doc) -def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): - ste_doc = frappe.new_doc("Stock Entry") - ste_doc.purpose = "Material Transfer" - ste_doc.purchase_order = po_doc.name - ste_doc.company = po_doc.company - ste_doc.is_return = 1 +def get_mapped_subcontracting_order(source_name, target_doc=None): - for key, value in available_materials.items(): - if not value.qty: - continue + if target_doc and isinstance(target_doc, str): + target_doc = json.loads(target_doc) + for key in ["service_items", "items", "supplied_items"]: + if key in target_doc: + del target_doc[key] + target_doc = json.dumps(target_doc) - if value.batch_no: - for batch_no, qty in value.batch_no.items(): - if qty > 0: - add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no) - else: - add_items_in_ste(ste_doc, value, value.qty, po_details) - - ste_doc.set_stock_entry_type() - ste_doc.set_missing_values() - - return ste_doc - - -def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None): - item = ste_doc.append("items", row.item_details) - - po_detail = list(set(row.po_details).intersection(po_details)) - item.update( + target_doc = get_mapped_doc( + "Purchase Order", + source_name, { - "qty": qty, - "batch_no": batch_no, - "basic_rate": row.item_details["rate"], - "po_detail": po_detail[0] if po_detail else "", - "s_warehouse": row.item_details["t_warehouse"], - "t_warehouse": row.item_details["s_warehouse"], - "item_code": row.item_details["rm_item_code"], - "subcontracted_item": row.item_details["main_item_code"], - "serial_no": "\n".join(row.serial_no) if row.serial_no else "", - } + "Purchase Order": { + "doctype": "Subcontracting Order", + "field_map": {}, + "field_no_map": ["total_qty", "total", "net_total"], + "validation": { + "docstatus": ["=", 1], + }, + }, + "Purchase Order Item": { + "doctype": "Subcontracting Order Service Item", + "field_map": {}, + "field_no_map": [], + }, + }, + target_doc, ) + + target_doc.populate_items_table() + + 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 + + +@frappe.whitelist() +def is_subcontracting_order_created(po_name) -> bool: + count = frappe.db.count( + "Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]} + ) + + return True if count else False diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index 81f20100c3..01b55c00d6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -22,6 +22,6 @@ def get_data(): "label": _("Reference"), "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], }, - {"label": _("Sub-contracting"), "items": ["Stock Entry"]}, + {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]}, ], } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 5f84de60d0..bd7e4e8d86 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -13,9 +13,6 @@ from erpnext.buying.doctype.purchase_order.purchase_order import ( make_purchase_invoice as make_pi_from_po, ) from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_rm_stock_entry as make_subcontract_transfer_entry, -) from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.stock.doctype.item.test_item import make_item @@ -24,7 +21,6 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_pi_from_pr, ) -from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry class TestPurchaseOrder(FrappeTestCase): @@ -140,43 +136,6 @@ class TestPurchaseOrder(FrappeTestCase): # ordered qty decreases as ordered qty is 0 (deleted row) self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 - def test_supplied_items_validations_on_po_update_after_submit(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100) - item = po.items[0] - - original_supplied_items = {po.name: po.required_qty for po in po.supplied_items} - - # Just update rate - trans_item = [ - { - "item_code": "_Test FG Item", - "rate": 20, - "qty": 5, - "conversion_factor": 1.0, - "docname": item.name, - } - ] - update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) - po.reload() - - new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} - self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys())) - - # Update qty to 2x - trans_item[0]["qty"] *= 2 - update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) - po.reload() - - new_supplied_items = {po.name: po.required_qty for po in po.supplied_items} - self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values())) - - # Set transfer qty and attempt to update qty, shouldn't be allowed - po.supplied_items[0].supplied_qty = 2 - po.supplied_items[0].db_update() - trans_item[0]["qty"] *= 2 - with self.assertRaises(frappe.ValidationError): - update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name) - def test_update_child(self): mr = make_material_request(qty=10) po = make_purchase_order(mr.name) @@ -426,31 +385,6 @@ class TestPurchaseOrder(FrappeTestCase): new_item_with_tax.delete() frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() - def test_update_child_uom_conv_factor_change(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) - - trans_item = json.dumps( - [ - { - "item_code": po.get("items")[0].item_code, - "rate": po.get("items")[0].rate, - "qty": po.get("items")[0].qty, - "uom": "_Test UOM 1", - "conversion_factor": 2, - "docname": po.get("items")[0].name, - } - ] - ) - update_child_qty_rate("Purchase Order", trans_item, po.name) - po.reload() - - total_reqd_qty_after_change = sum( - d.get("required_qty") for d in po.as_dict().get("supplied_items") - ) - - self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) - def test_update_qty(self): po = create_purchase_order() @@ -609,10 +543,6 @@ class TestPurchaseOrder(FrappeTestCase): ) automatically_fetch_payment_terms(enable=0) - def test_subcontracting(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - self.assertEqual(len(po.get("supplied_items")), 2) - def test_warehouse_company_validation(self): from erpnext.stock.utils import InvalidWarehouseCompany @@ -777,379 +707,6 @@ class TestPurchaseOrder(FrappeTestCase): pi.insert() self.assertTrue(pi.get("payment_schedule")) - def test_reserved_qty_subcontract_po(self): - # Make stock available for raw materials - make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=30, - basic_rate=100, - ) - - bin1 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], - as_dict=1, - ) - - # Submit PO - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - - bin2 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], - as_dict=1, - ) - - self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) - self.assertNotEqual(bin1.modified, bin2.modified) - - # Create stock transfer - rm_item = [ - { - "item_code": "_Test FG Item", - "rm_item_code": "_Test Item", - "item_name": "_Test Item", - "qty": 6, - "warehouse": "_Test Warehouse - _TC", - "rate": 100, - "amount": 600, - "stock_uom": "Nos", - } - ] - rm_item_string = json.dumps(rm_item) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.to_warehouse = "_Test Warehouse 1 - _TC" - se.save() - se.submit() - - bin3 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # close PO - po.update_status("Closed") - bin4 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Re-open PO - po.update_status("Submitted") - bin5 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - make_stock_entry( - target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse 1 - _TC", - item_code="_Test Item Home Desktop 100", - qty=40, - basic_rate=100, - ) - - # make Purchase Receipt against PO - pr = make_purchase_receipt(po.name) - pr.supplier_warehouse = "_Test Warehouse 1 - _TC" - pr.save() - pr.submit() - - bin6 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel PR - pr.cancel() - bin7 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Make Purchase Invoice - pi = make_pi_from_po(po.name) - pi.update_stock = 1 - pi.supplier_warehouse = "_Test Warehouse 1 - _TC" - pi.insert() - pi.submit() - bin8 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - # Cancel PR - pi.cancel() - bin9 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) - - # Cancel Stock Entry - se.cancel() - bin10 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) - - # Cancel PO - po.reload() - po.cancel() - bin11 = frappe.db.get_value( - "Bin", - filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, - fieldname="reserved_qty_for_sub_contract", - as_dict=1, - ) - - self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) - - def test_exploded_items_in_subcontracted(self): - item_code = "_Test Subcontracted FG Item 11" - make_subcontracted_item(item_code=item_code) - - po = create_purchase_order( - item_code=item_code, - qty=1, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - include_exploded_items=1, - ) - - name = frappe.db.get_value("BOM", {"item": item_code}, "name") - bom = frappe.get_doc("BOM", name) - - exploded_items = sorted( - [d.item_code for d in bom.exploded_items if not d.get("sourced_by_supplier")] - ) - supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) - self.assertEqual(exploded_items, supplied_items) - - po1 = create_purchase_order( - item_code=item_code, - qty=1, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - include_exploded_items=0, - ) - - supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items]) - bom_items = sorted([d.item_code for d in bom.items if not d.get("sourced_by_supplier")]) - - self.assertEqual(supplied_items1, bom_items) - - def test_backflush_based_on_stock_entry(self): - item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code=item_code) - make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1}) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 5 - po = create_purchase_order( - item_code=item_code, - qty=order_qty, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - ) - - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 1", - qty=10, - basic_rate=100, - ) - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 1", - "item_name": "_Test Item", - "qty": 10, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": "_Test Item Home Desktop 100", - "item_name": "_Test Item Home Desktop 100", - "qty": 20, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": "Test Extra Item 1", - "item_name": "Test Extra Item 1", - "qty": 10, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": "Test Extra Item 2", - "stock_uom": "Nos", - "qty": 10, - "warehouse": "_Test Warehouse - _TC", - "item_name": "Test Extra Item 2", - }, - ] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - pr = make_purchase_receipt(po.name) - - received_qty = 2 - # partial receipt - pr.get("items")[0].qty = received_qty - pr.save() - pr.submit() - - transferred_items = sorted( - [d.item_code for d in se.get("items") if se.purchase_order == po.name] - ) - issued_items = sorted([d.rm_item_code for d in pr.get("supplied_items")]) - - self.assertEqual(transferred_items, issued_items) - self.assertEqual(pr.get("items")[0].rm_supp_cost, 2000) - - transferred_rm_map = frappe._dict() - for item in rm_items: - transferred_rm_map[item.get("rm_item_code")] = item - - update_backflush_based_on("BOM") - - def test_supplied_qty_against_subcontracted_po(self): - item_code = "_Test Subcontracted FG Item 5" - make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1}) - - make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) - - update_backflush_based_on("Material Transferred for Subcontract") - - order_qty = 250 - po = create_purchase_order( - item_code=item_code, - qty=order_qty, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - do_not_save=True, - ) - - # Add same subcontracted items multiple times - po.append( - "items", - { - "item_code": item_code, - "qty": order_qty, - "schedule_date": add_days(nowdate(), 1), - "warehouse": "_Test Warehouse - _TC", - }, - ) - - po.set_missing_values() - po.submit() - - # Material receipt entry for the raw materials which will be send to supplier - make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 4", - qty=500, - basic_rate=100, - ) - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 4", - "item_name": "_Test Item", - "qty": 250, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - "name": po.supplied_items[0].name, - }, - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 4", - "item_name": "_Test Item", - "qty": 250, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - ] - - # Raw Materials transfer entry from stores to supplier's warehouse - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.submit() - - # Test po_detail field has value or not - for item_row in se.items: - self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name) - - po_doc = frappe.get_doc("Purchase Order", po.name) - for row in po_doc.supplied_items: - # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order - self.assertEqual(row.supplied_qty, 250.0) - - update_backflush_based_on("BOM") - def test_advance_payment_entry_unlink_against_purchase_order(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -1248,50 +805,6 @@ def make_pr_against_po(po, received_qty=0): return pr -def make_subcontracted_item(**args): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - args = frappe._dict(args) - - if not frappe.db.exists("Item", args.item_code): - make_item( - args.item_code, - { - "is_stock_item": 1, - "is_sub_contracted_item": 1, - "has_batch_no": args.get("has_batch_no") or 0, - }, - ) - - if not args.raw_materials: - if not frappe.db.exists("Item", "Test Extra Item 1"): - make_item( - "Test Extra Item 1", - { - "is_stock_item": 1, - }, - ) - - if not frappe.db.exists("Item", "Test Extra Item 2"): - make_item( - "Test Extra Item 2", - { - "is_stock_item": 1, - }, - ) - - args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] - - if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): - make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - - -def update_backflush_based_on(based_on): - doc = frappe.get_doc("Buying Settings") - doc.backflush_raw_materials_of_subcontract_based_on = based_on - doc.save() - - def get_same_items(): return [ { diff --git a/erpnext/buying/doctype/purchase_order/test_records.json b/erpnext/buying/doctype/purchase_order/test_records.json index 896050ce43..4df994a68c 100644 --- a/erpnext/buying/doctype/purchase_order/test_records.json +++ b/erpnext/buying/doctype/purchase_order/test_records.json @@ -1,38 +1,4 @@ [ - { - "advance_paid": 0.0, - "buying_price_list": "_Test Price List", - "company": "_Test Company", - "conversion_rate": 1.0, - "currency": "INR", - "doctype": "Purchase Order", - "base_grand_total": 5000.0, - "grand_total": 5000.0, - "is_subcontracted": 1, - "naming_series": "_T-Purchase Order-", - "base_net_total": 5000.0, - "items": [ - { - "base_amount": 5000.0, - "conversion_factor": 1.0, - "description": "_Test FG Item", - "doctype": "Purchase Order Item", - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "parentfield": "items", - "qty": 10.0, - "rate": 500.0, - "schedule_date": "2013-03-01", - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": "_Test Warehouse - _TC" - } - ], - "supplier": "_Test Supplier", - "supplier_name": "_Test Supplier", - "transaction_date": "2013-02-12", - "schedule_date": "2013-02-13" - }, { "advance_paid": 0.0, "buying_price_list": "_Test Price List", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 7994b08ad4..1a9845396f 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -11,6 +11,8 @@ "supplier_part_no", "item_name", "product_bundle", + "fg_item", + "fg_item_qty", "column_break_4", "schedule_date", "expected_delivery_date", @@ -574,16 +576,18 @@ "read_only": 1 }, { - "depends_on": "eval:parent.is_subcontracted", + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "bom", "fieldtype": "Link", "label": "BOM", "options": "BOM", - "print_hide": 1 + "print_hide": 1, + "read_only": 1, + "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow" }, { "default": "0", - "depends_on": "eval:parent.is_subcontracted", + "depends_on": "eval:parent.is_old_subcontracting_flow", "fieldname": "include_exploded_items", "fieldtype": "Check", "label": "Include Exploded Items", @@ -848,6 +852,22 @@ "label": "Sales Order Packed Item", "no_copy": 1, "print_hide": 1 + }, + { + "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 && !parent.is_old_subcontracting_flow", + "options": "Item" + }, + { + "default": "1", + "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 && !parent.is_old_subcontracting_flow" } ], "idx": 1, 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 6889322fb9..075671f4ec 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js @@ -14,32 +14,29 @@ frappe.query_reports["Subcontract Order Summary"] = { }, { label: __("From Date"), - fieldname:"from_date", + fieldname: "from_date", fieldtype: "Date", default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { label: __("To Date"), - fieldname:"to_date", + fieldname: "to_date", fieldtype: "Date", default: frappe.datetime.get_today(), reqd: 1 }, { - label: __("Purchase Order"), + label: __("Order Type"), + fieldname: "order_type", + fieldtype: "Select", + options: ["Purchase Order", "Subcontracting Order"], + default: "Subcontracting Order" + }, + { + label: __("Subcontract Order"), fieldname: "name", - fieldtype: "Link", - options: "Purchase Order", - get_query: function() { - return { - filters: { - docstatus: 1, - is_subcontracted: 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.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json index 526a8d8ad0..7861e49ccf 100644 --- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json +++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json @@ -15,7 +15,7 @@ "name": "Subcontract Order Summary", "owner": "Administrator", "prepared_report": 0, - "ref_doctype": "Purchase Order", + "ref_doctype": "Subcontracting Order", "report_name": "Subcontract Order Summary", "report_type": "Script Report", "roles": [ diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py index 3d66637576..0213051aeb 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,43 +20,45 @@ def get_data(report_filters): if orders: supplied_items = get_supplied_items(orders, report_filters) - po_details = prepare_subcontracted_data(orders, supplied_items) - get_subcontracted_data(po_details, data) + order_details = prepare_subcontracted_data(orders, supplied_items) + get_subcontracted_data(order_details, data) return data def get_subcontracted_orders(report_filters): fields = [ - "`tabPurchase Order Item`.`parent` as po_id", - "`tabPurchase Order Item`.`item_code`", - "`tabPurchase Order Item`.`item_name`", - "`tabPurchase Order Item`.`qty`", - "`tabPurchase Order Item`.`name`", - "`tabPurchase Order Item`.`received_qty`", - "`tabPurchase Order`.`status`", + 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("Purchase 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 = [ - ["Purchase Order", "docstatus", "=", 1], - ["Purchase Order", "is_subcontracted", "=", 1], + [report_filters.order_type, "docstatus", "=", 1], [ - "Purchase 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(["Purchase Order", field, "=", report_filters.get(field)]) + filters.append([report_filters.order_type, field, "=", report_filters.get(field)]) return filters @@ -77,10 +79,15 @@ def get_supplied_items(orders, report_filters): "reference_name", ] - filters = {"parent": ("in", [d.po_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("Purchase Order Item Supplied", 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) @@ -89,24 +96,24 @@ def get_supplied_items(orders, report_filters): def prepare_subcontracted_data(orders, supplied_items): - po_details = {} + order_details = {} for row in orders: - key = (row.po_id, row.name, row.item_code) - if key not in po_details: - po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []})) + key = (row.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 = po_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 po_details + return order_details -def get_subcontracted_data(po_details, data): - for key, details in po_details.items(): - res = details.po_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 = {} @@ -115,13 +122,13 @@ def get_subcontracted_data(po_details, data): data.append(res) -def get_columns(): +def get_columns(filters): return [ { - "label": _("Purchase Order"), - "fieldname": "po_id", + "label": _("Subcontract Order"), + "fieldname": "order_id", "fieldtype": "Link", - "options": "Purchase Order", + "options": filters.order_type, "width": 100, }, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80}, 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 fc58b6aaaf..6304a0908d 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 fdf6cf702d..f40b788fe0 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 2e90de66ef..135449bb2b 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": _("Purchase Order"), + "label": _("Subcontract Order"), "fieldtype": "Link", - "fieldname": "purchase_order", - "options": "Purchase 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): - po = get_po(filters) - po_name = [v.name for v in po] - sub_items = get_purchase_order_item_supplied(po_name) - for item in sub_items: - for order in po: + 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 = { - "purchase_order": item.parent, + "subcontract_order": item.parent, "date": order.transaction_date, "supplier": order.supplier, "fg_item_code": item.item_code, @@ -76,22 +76,25 @@ def get_data(data, filters): data.append(row) -def get_po(filters): +def get_subcontract_orders(filters): record_filters = [ - ["is_subcontracted", "=", 1], ["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( - "Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"] + filters.order_type, filters=record_filters, fields=["name", "transaction_date", "supplier"] ) -def get_purchase_order_item_supplied(po): +def get_subcontract_order_supplied_item(order_type, orders): return frappe.get_all( - "Purchase Order Item", - filters=[("parent", "IN", po)], + 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 57f8741b5b..c772c1a1b1 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -7,18 +7,35 @@ import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_to_be_received import ( execute, ) +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, +) from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_subcontracting_receipt, +) class TestSubcontractedItemToBeReceived(FrappeTestCase): def test_pending_and_received_qty(self): - po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) - transfer_param = [] + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 500, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC" + ) make_stock_entry( item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 ) @@ -28,28 +45,28 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase): qty=100, basic_rate=100, ) - make_purchase_receipt_against_po(po.name) - po.reload() + make_subcontracting_receipt_against_sco(sco.name) + sco.reload() col, data = execute( filters=frappe._dict( { - "supplier": po.supplier, + "order_type": "Subcontracting Order", + "supplier": sco.supplier, "from_date": frappe.utils.get_datetime( - frappe.utils.add_to_date(po.transaction_date, days=-10) + frappe.utils.add_to_date(sco.transaction_date, days=-10) ), - "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)), + "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)), } ) ) self.assertEqual(data[0]["pending_qty"], 5) self.assertEqual(data[0]["received_qty"], 5) - self.assertEqual(data[0]["purchase_order"], po.name) - self.assertEqual(data[0]["supplier"], po.supplier) + self.assertEqual(data[0]["subcontract_order"], sco.name) + self.assertEqual(data[0]["supplier"], sco.supplier) -def make_purchase_receipt_against_po(po, quantity=5): - pr = make_purchase_receipt(po) - pr.items[0].qty = quantity - pr.supplier_warehouse = "_Test Warehouse 1 - _TC" - pr.insert() - pr.submit() +def make_subcontracting_receipt_against_sco(sco, quantity=5): + scr = make_subcontracting_receipt(sco) + scr.items[0].qty = quantity + scr.insert() + scr.submit() 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 0853afd657..b6739fe663 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 c7cee5e20b..f689fbcf24 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 6b8a3b140a..ef28eda62a 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): - po_rm_item_details = get_po_items_to_supply(filters) + order_rm_item_details = get_order_items_to_supply(filters) data = [] - for row in po_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,23 +59,33 @@ def get_data(filters): return data -def get_po_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( - "Purchase Order", + filters.order_type, fields=[ - "name as purchase_order", + "name as subcontract_order", "transaction_date as date", "supplier as supplier", - "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code", - "`tabPurchase Order Item Supplied`.required_qty as reqd_qty", - "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty", - ], - filters=[ - ["Purchase Order", "per_received", "<", "100"], - ["Purchase Order", "is_subcontracted", "=", 1], - ["Purchase Order", "supplier", "=", filters.supplier], - ["Purchase Order", "transaction_date", "<=", filters.to_date], - ["Purchase Order", "transaction_date", ">=", filters.from_date], - ["Purchase Order", "docstatus", "=", 1], + 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 2791a26db7..160295776b 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -3,24 +3,34 @@ # Compiled at: 2019-05-06 10:24:35 # Decompiled by https://python-decompiler.com -import json - import frappe from frappe.tests.utils import FrappeTestCase -from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import ( execute, ) +from erpnext.controllers.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 class TestSubcontractedItemToBeTransferred(FrappeTestCase): def test_pending_and_transferred_qty(self): - po = create_purchase_order( - item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" - ) + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 500, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) # Material Receipt of RMs make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100) @@ -28,50 +38,48 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase): item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100 ) - se = transfer_subcontracted_raw_materials(po) + transfer_subcontracted_raw_materials(sco) col, data = execute( filters=frappe._dict( { - "supplier": po.supplier, + "order_type": "Subcontracting Order", + "supplier": sco.supplier, "from_date": frappe.utils.get_datetime( - frappe.utils.add_to_date(po.transaction_date, days=-10) + frappe.utils.add_to_date(sco.transaction_date, days=-10) ), - "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)), + "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)), } ) ) - po.reload() + sco.reload() - po_data = [row for row in data if row.get("purchase_order") == po.name] + sco_data = [row for row in data if row.get("subcontract_order") == sco.name] # Alphabetically sort to be certain of order - po_data = sorted(po_data, key=lambda i: i["rm_item_code"]) + sco_data = sorted(sco_data, key=lambda i: i["rm_item_code"]) - self.assertEqual(len(po_data), 2) - self.assertEqual(po_data[0]["purchase_order"], po.name) + self.assertEqual(len(sco_data), 2) + self.assertEqual(sco_data[0]["subcontract_order"], sco.name) - self.assertEqual(po_data[0]["rm_item_code"], "_Test Item") - self.assertEqual(po_data[0]["p_qty"], 8) - self.assertEqual(po_data[0]["transferred_qty"], 2) + self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item") + self.assertEqual(sco_data[0]["p_qty"], 8) + self.assertEqual(sco_data[0]["transferred_qty"], 2) - self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100") - self.assertEqual(po_data[1]["p_qty"], 19) - self.assertEqual(po_data[1]["transferred_qty"], 1) - - se.cancel() - po.cancel() + self.assertEqual(sco_data[1]["rm_item_code"], "_Test Item Home Desktop 100") + self.assertEqual(sco_data[1]["p_qty"], 19) + self.assertEqual(sco_data[1]["transferred_qty"], 1) -def transfer_subcontracted_raw_materials(po): - # Order of supplied items fetched in PO is flaky +def transfer_subcontracted_raw_materials(sco): + # Order of supplied items fetched in SCO is flaky transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1} - item_1 = po.supplied_items[0].rm_item_code - item_2 = po.supplied_items[1].rm_item_code + item_1 = sco.supplied_items[0].rm_item_code + item_2 = sco.supplied_items[1].rm_item_code - rm_item = [ + rm_items = [ { - "name": po.supplied_items[0].name, + "name": sco.supplied_items[0].name, "item_code": item_1, "rm_item_code": item_1, "item_name": item_1, @@ -82,7 +90,7 @@ def transfer_subcontracted_raw_materials(po): "stock_uom": "Nos", }, { - "name": po.supplied_items[1].name, + "name": sco.supplied_items[1].name, "item_code": item_2, "rm_item_code": item_2, "item_name": item_2, @@ -93,8 +101,7 @@ def transfer_subcontracted_raw_materials(po): "stock_uom": "Nos", }, ] - rm_item_string = json.dumps(rm_item) - se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) se.from_warehouse = "_Test Warehouse - _TC" se.to_warehouse = "_Test Warehouse - _TC" se.stock_entry_type = "Send to Subcontractor" diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ceac815bf4..5528b4dc37 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2709,10 +2709,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_subcontracted: + if parent.is_old_subcontracting_flow: if should_update_supplied_items(parent): parent.update_reserved_qty_for_subcontract() - parent.create_raw_materials_supplied("supplied_items") + parent.create_raw_materials_supplied() parent.save() else: # Sales Order parent.validate_warehouse() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index f28de3b064..036733c0c3 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -11,8 +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 import Subcontracting +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 @@ -21,7 +20,7 @@ class QtyMismatchError(ValidationError): pass -class BuyingController(StockController, Subcontracting): +class BuyingController(SubcontractingController): def __setup__(self): self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] @@ -55,7 +54,8 @@ class BuyingController(StockController, Subcontracting): # sub-contracting self.validate_for_subcontracting() - self.create_raw_materials_supplied("supplied_items") + 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"): @@ -256,13 +256,18 @@ class BuyingController(StockController, Subcontracting): ) qty_in_stock_uom = flt(item.qty * item.conversion_factor) - item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) - item.valuation_rate = ( - item.base_net_amount - + item.item_tax_amount - + item.rm_supp_cost - + flt(item.landed_cost_voucher_amount) - ) / qty_in_stock_uom + 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 @@ -300,7 +305,7 @@ class BuyingController(StockController, Subcontracting): raise_error_if_no_rate=False, ) - rate = flt(outgoing_rate * d.conversion_factor, d.precision("rate")) + rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) else: rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate") @@ -317,76 +322,25 @@ class BuyingController(StockController, Subcontracting): d.discount_amount = 0.0 d.margin_rate_or_amount = 0.0 - def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True): - supplied_items_cost = 0.0 - for d in self.get("supplied_items"): - if d.reference_name == item_row_id: - if reset_outgoing_rate and frappe.get_cached_value("Item", d.rm_item_code, "is_stock_item"): - rate = get_incoming_rate( - { - "item_code": d.rm_item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * d.consumed_qty, - "serial_no": d.serial_no, - "batch_no": d.batch_no, - } - ) - - if rate > 0: - d.rate = rate - - d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount")) - supplied_items_cost += flt(d.amount) - - return supplied_items_cost - def validate_for_subcontracting(self): - if self.is_subcontracted: + if self.is_subcontracted and self.get("is_old_subcontracting_flow"): if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype)) for item in self.get("items"): if item in self.sub_contracted_items and not item.bom: frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) - if self.doctype != "Purchase Order": return - for row in self.get("supplied_items"): if not row.reserve_warehouse: msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" frappe.throw(_(msg)) else: for item in self.get("items"): - if item.bom: + if item.get("bom"): item.bom = None - def create_raw_materials_supplied(self, raw_material_table): - if self.is_subcontracted: - self.set_materials_for_subcontracted_items(raw_material_table) - - elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: - for item in self.get("items"): - item.rm_supp_cost = 0.0 - - if not self.is_subcontracted and self.get("supplied_items"): - self.set("supplied_items", []) - - @property - def sub_contracted_items(self): - if not hasattr(self, "_sub_contracted_items"): - self._sub_contracted_items = [] - item_codes = list(set(item.item_code for item in self.get("items"))) - if item_codes: - items = frappe.get_all( - "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1} - ) - self._sub_contracted_items = [item.name for item in items] - - return self._sub_contracted_items - def set_qty_as_per_stock_uom(self): for d in self.get("items"): if d.meta.get_field("stock_qty"): @@ -510,7 +464,9 @@ class BuyingController(StockController, Subcontracting): sle.update( { "incoming_rate": incoming_rate, - "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0, + "recalculate_rate": 1 + if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse + else 0, } ) sl_entries.append(sle) @@ -538,7 +494,8 @@ class BuyingController(StockController, Subcontracting): ) ) - self.make_sl_entries_for_supplier_warehouse(sl_entries) + 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, @@ -565,26 +522,9 @@ class BuyingController(StockController, Subcontracting): ) po_obj.update_ordered_qty(po_item_rows) - if self.is_subcontracted: + if self.get("is_old_subcontracting_flow"): po_obj.update_reserved_qty_for_subcontract() - def make_sl_entries_for_supplier_warehouse(self, sl_entries): - if hasattr(self, "supplied_items"): - for d in self.get("supplied_items"): - # negative quantity is passed, as raw material qty has to be decreased - # when PR is submitted and it has to be increased when PR is cancelled - sl_entries.append( - self.get_sl_entries( - d, - { - "item_code": d.rm_item_code, - "warehouse": self.supplier_warehouse, - "actual_qty": -1 * flt(d.consumed_qty), - "dependant_sle_voucher_detail_no": d.reference_name, - }, - ) - ) - def on_submit(self): if self.get("is_return"): return @@ -808,7 +748,7 @@ class BuyingController(StockController, Subcontracting): if self.doctype == "Material Request": return - if hasattr(self, "is_subcontracted") and self.is_subcontracted: + 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") diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index d24ac3f2cf..04a0dfa3d4 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -77,7 +77,7 @@ def validate_returned_items(doc): if doc.doctype != "Purchase Invoice": select_fields += ",serial_no, batch_no" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: select_fields += ",rejected_qty, received_qty" for d in frappe.db.sql( @@ -161,7 +161,7 @@ def validate_returned_items(doc): def validate_quantity(doc, args, ref, valid_items, already_returned_items): fields = ["stock_qty"] - if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]: + if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]: fields.extend(["received_qty", "rejected_qty"]) already_returned_data = already_returned_items.get(args.item_code) or {} @@ -224,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row): if ref_item_row.get("rate", 0) > item_dict["rate"]: item_dict["rate"] = ref_item_row.get("rate", 0) - if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt"]: + if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: item_dict["received_qty"] += ref_item_row.received_qty item_dict["rejected_qty"] += ref_item_row.rejected_qty @@ -239,7 +239,7 @@ def get_ref_item_dict(valid_items, ref_item_row): def get_already_returned_items(doc): column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty" - if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]: + if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]: column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty, sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" @@ -281,17 +281,21 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype): child_doctype = doctype + " Item" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) - if doctype in ("Purchase Receipt", "Purchase Invoice"): + if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): party_type = "supplier" else: party_type = "customer" fields = [ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), - "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype), ] - if doctype in ("Purchase Receipt", "Purchase Invoice"): + if doctype != "Subcontracting Receipt": + fields += [ + "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype), + ] + + if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): fields += [ "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), @@ -342,7 +346,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): # look for Print Heading "Debit Note" doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note")) - for tax in doc.get("taxes"): + for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": tax.tax_amount = -1 * tax.tax_amount @@ -381,8 +385,11 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): for d in doc.get("packed_items"): d.qty = d.qty * -1 - doc.discount_amount = -1 * source.discount_amount - doc.run_method("calculate_taxes_and_totals") + if doc.get("discount_amount"): + doc.discount_amount = -1 * source.discount_amount + + if doctype != "Subcontracting Receipt": + doc.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): target_doc.qty = -1 * source_doc.qty @@ -393,7 +400,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): if serial_nos: target_doc.serial_no = "\n".join(serial_nos) - if doctype == "Purchase Receipt": + if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype ) @@ -405,15 +412,24 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): ) target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) - target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)) - target_doc.received_stock_qty = -1 * flt( - source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) - ) + if hasattr(target_doc, "stock_qty"): + target_doc.stock_qty = -1 * flt( + source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0) + ) + target_doc.received_stock_qty = -1 * flt( + source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) + ) - target_doc.purchase_order = source_doc.purchase_order - target_doc.purchase_order_item = source_doc.purchase_order_item - target_doc.rejected_warehouse = source_doc.rejected_warehouse - target_doc.purchase_receipt_item = source_doc.name + if doctype == "Subcontracting Receipt": + target_doc.subcontracting_order = source_doc.subcontracting_order + target_doc.subcontracting_order_item = source_doc.subcontracting_order_item + target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.subcontracting_receipt_item = source_doc.name + else: + target_doc.purchase_order = source_doc.purchase_order + target_doc.purchase_order_item = source_doc.purchase_order_item + target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.purchase_receipt_item = source_doc.name elif doctype == "Purchase Invoice": returned_qty_map = get_returned_qty_map_for_row( @@ -525,7 +541,7 @@ def get_rate_for_return( item_row, ) - if voucher_type in ("Purchase Receipt", "Purchase Invoice"): + if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): select_field = "incoming_rate" else: select_field = "abs(stock_value_difference / actual_qty)" @@ -560,6 +576,7 @@ def get_return_against_item_fields(voucher_type): "Purchase Invoice": "purchase_invoice_item", "Delivery Note": "dn_detail", "Sales Invoice": "sales_invoice_item", + "Subcontracting Receipt": "subcontracting_receipt_item", } return return_against_item_fields[voucher_type] diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py deleted file mode 100644 index 4bce06ff9b..0000000000 --- a/erpnext/controllers/subcontracting.py +++ /dev/null @@ -1,469 +0,0 @@ -import copy -from collections import defaultdict - -import frappe -from frappe import _ -from frappe.utils import cint, flt, get_link_to_form - -from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos - - -class Subcontracting: - 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() - self.__validate_supplied_items() - - def __prepare_supplied_items(self): - self.initialized_fields() - self.__get_purchase_orders() - self.__get_pending_qty_to_receive() - self.get_available_materials() - self.__remove_changed_rows() - self.__set_supplied_items() - - def initialized_fields(self): - self.available_materials = frappe._dict() - self.__transferred_items = frappe._dict() - self.alternative_item_details = frappe._dict() - self.__get_backflush_based_on() - - def __get_backflush_based_on(self): - self.backflush_based_on = frappe.db.get_single_value( - "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" - ) - - def __get_purchase_orders(self): - self.purchase_orders = [] - - if self.doctype == "Purchase Order": - return - - self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order] - - def __identify_change_in_item_table(self): - self.__changed_name = [] - self.__reference_name = [] - - if self.doctype == "Purchase Order" or self.is_new(): - self.set(self.raw_material_table, []) - return - - item_dict = self.__get_data_before_save() - if not item_dict: - return True - - for n_row in self.items: - self.__reference_name.append(n_row.name) - if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]: - self.__changed_name.append(n_row.name) - - if item_dict.get(n_row.name): - del item_dict[n_row.name] - - self.__changed_name.extend(item_dict.keys()) - - def __get_data_before_save(self): - item_dict = {} - if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self._doc_before_save: - for row in self._doc_before_save.get("items"): - item_dict[row.name] = (row.item_code, row.qty) - - return item_dict - - def get_available_materials(self): - """Get the available raw materials which has been transferred to the supplier. - available_materials = { - (item_code, subcontracted_item, purchase_order): { - 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details - } - } - """ - if not self.purchase_orders: - return - - for row in self.__get_transferred_items(): - key = (row.rm_item_code, row.main_item_code, row.purchase_order) - - if key not in self.available_materials: - self.available_materials.setdefault( - key, - frappe._dict( - { - "qty": 0, - "serial_no": [], - "batch_no": defaultdict(float), - "item_details": row, - "po_details": [], - } - ), - ) - - details = self.available_materials[key] - details.qty += row.qty - details.po_details.append(row.po_detail) - - if row.serial_no: - details.serial_no.extend(get_serial_nos(row.serial_no)) - - if row.batch_no: - details.batch_no[row.batch_no] += row.qty - - self.__set_alternative_item_details(row) - - self.__transferred_items = copy.deepcopy(self.available_materials) - for doctype in ["Purchase Receipt", "Purchase Invoice"]: - self.__update_consumed_materials(doctype) - - def __update_consumed_materials(self, doctype, return_consumed_items=False): - """Deduct the consumed materials from the available materials.""" - - pr_items = self.__get_received_items(doctype) - if not pr_items: - return ([], {}) if return_consumed_items else None - - pr_items = {d.name: d.get(self.get("po_field") or "purchase_order") for d in pr_items} - consumed_materials = self.__get_consumed_items(doctype, pr_items.keys()) - - if return_consumed_items: - return (consumed_materials, pr_items) - - for row in consumed_materials: - key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) - if not self.available_materials.get(key): - continue - - self.available_materials[key]["qty"] -= row.consumed_qty - if row.serial_no: - self.available_materials[key]["serial_no"] = list( - set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) - ) - - if row.batch_no: - self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty - - def __get_transferred_items(self): - fields = ["`tabStock Entry`.`purchase_order`"] - alias_dict = { - "item_code": "rm_item_code", - "subcontracted_item": "main_item_code", - "basic_rate": "rate", - } - - child_table_fields = [ - "item_code", - "item_name", - "description", - "qty", - "basic_rate", - "amount", - "serial_no", - "uom", - "subcontracted_item", - "stock_uom", - "batch_no", - "conversion_factor", - "s_warehouse", - "t_warehouse", - "item_group", - "po_detail", - ] - - if self.backflush_based_on == "BOM": - child_table_fields.append("original_item") - - for field in child_table_fields: - fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}") - - filters = [ - ["Stock Entry", "docstatus", "=", 1], - ["Stock Entry", "purpose", "=", "Send to Subcontractor"], - ["Stock Entry", "purchase_order", "in", self.purchase_orders], - ] - - return frappe.get_all("Stock Entry", fields=fields, filters=filters) - - def __get_received_items(self, doctype): - fields = [] - self.po_field = "purchase_order" - - for field in ["name", self.po_field, "parent"]: - fields.append(f"`tab{doctype} Item`.`{field}`") - - filters = [ - [doctype, "docstatus", "=", 1], - [f"{doctype} Item", self.po_field, "in", self.purchase_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, pr_items): - return frappe.get_all( - "Purchase Receipt Item Supplied", - fields=[ - "serial_no", - "rm_item_code", - "reference_name", - "batch_no", - "consumed_qty", - "main_item_code", - ], - filters={"docstatus": 1, "reference_name": ("in", list(pr_items)), "parenttype": doctype}, - ) - - def __set_alternative_item_details(self, row): - if row.get("original_item"): - self.alternative_item_details[row.get("original_item")] = row - - def __get_pending_qty_to_receive(self): - """Get qty to be received against the purchase order.""" - - self.qty_to_be_received = defaultdict(float) - - if ( - self.doctype != "Purchase Order" and self.backflush_based_on != "BOM" and self.purchase_orders - ): - for row in frappe.get_all( - "Purchase Order Item", - fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], - filters={"docstatus": 1, "parent": ("in", self.purchase_orders)}, - ): - - self.qty_to_be_received[(row.item_code, row.parent)] += row.qty - - def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): - doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" - fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] - - alias_dict = { - "item_code": "rm_item_code", - "name": "bom_detail_no", - "source_warehouse": "reserve_warehouse", - } - for field in [ - "item_code", - "name", - "rate", - "stock_uom", - "source_warehouse", - "description", - "item_name", - "stock_uom", - ]: - fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}") - - filters = [ - [doctype, "parent", "=", bom_no], - [doctype, "docstatus", "=", 1], - ["BOM", "item", "=", item_code], - [doctype, "sourced_by_supplier", "=", 0], - ] - - return ( - frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] - ) - - def __remove_changed_rows(self): - if not self.__changed_name: - return - - i = 1 - self.set(self.raw_material_table, []) - for d in self._doc_before_save.supplied_items: - if d.reference_name in self.__changed_name: - continue - - if d.reference_name not in self.__reference_name: - continue - - d.idx = i - self.append("supplied_items", d) - - i += 1 - - def __set_supplied_items(self): - self.bom_items = {} - - has_supplied_items = True if self.get(self.raw_material_table) else False - for row in self.items: - if self.doctype != "Purchase Order" and ( - (self.__changed_name and row.name not in self.__changed_name) - or (has_supplied_items and not self.__changed_name) - ): - continue - - if self.doctype == "Purchase Order" or self.backflush_based_on == "BOM": - for bom_item in self.__get_materials_from_bom( - row.item_code, row.bom, row.get("include_exploded_items") - ): - qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor - bom_item.main_item_code = row.item_code - self.__update_reserve_warehouse(bom_item, row) - self.__set_alternative_item(bom_item) - self.__add_supplied_item(row, bom_item, qty) - - elif self.backflush_based_on != "BOM": - for key, transfer_item in self.available_materials.items(): - if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0: - qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0 - transfer_item.qty -= qty - self.__add_supplied_item(row, transfer_item.get("item_details"), qty) - - if self.qty_to_be_received: - self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty - - def __update_reserve_warehouse(self, row, item): - if self.doctype == "Purchase Order": - row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse - - def __get_qty_based_on_material_transfer(self, item_row, transfer_item): - key = (item_row.item_code, item_row.purchase_order) - - if self.qty_to_be_received == item_row.qty: - return transfer_item.qty - - if self.qty_to_be_received: - qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) - transfer_item.item_details.required_qty = transfer_item.qty - - if transfer_item.serial_no or frappe.get_cached_value( - "UOM", transfer_item.item_details.stock_uom, "must_be_whole_number" - ): - return frappe.utils.ceil(qty) - - return qty - - def __set_alternative_item(self, bom_item): - if self.alternative_item_details.get(bom_item.rm_item_code): - bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) - - def __add_supplied_item(self, item_row, bom_item, qty): - bom_item.conversion_factor = item_row.conversion_factor - rm_obj = self.append(self.raw_material_table, bom_item) - rm_obj.reference_name = item_row.name - - if self.doctype == "Purchase Order": - rm_obj.required_qty = qty - else: - rm_obj.consumed_qty = 0 - rm_obj.purchase_order = item_row.purchase_order - self.__set_batch_nos(bom_item, item_row, rm_obj, 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.purchase_order) - - if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: - new_rm_obj = None - for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): - if batch_qty >= qty: - self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) - self.available_materials[key]["batch_no"][batch_no] -= qty - return - - elif qty > 0 and batch_qty > 0: - qty -= batch_qty - new_rm_obj = self.append(self.raw_material_table, bom_item) - new_rm_obj.reference_name = item_row.name - self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) - self.available_materials[key]["batch_no"][batch_no] = 0 - - if abs(qty) > 0 and not new_rm_obj: - self.__set_consumed_qty(rm_obj, qty) - else: - self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) - self.__set_serial_nos(item_row, rm_obj) - - def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): - rm_obj.required_qty = required_qty - rm_obj.consumed_qty = consumed_qty - - def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): - rm_obj.update( - { - "consumed_qty": qty, - "batch_no": batch_no, - "required_qty": qty, - "purchase_order": item_row.purchase_order, - } - ) - - self.__set_serial_nos(item_row, rm_obj) - - def __set_serial_nos(self, item_row, rm_obj): - key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order) - if self.available_materials.get(key) and self.available_materials[key]["serial_no"]: - used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)] - rm_obj.serial_no = "\n".join(used_serial_nos) - - # Removed the used serial nos from the list - for sn in used_serial_nos: - self.available_materials[key]["serial_no"].remove(sn) - - def set_consumed_qty_in_po(self): - # Update consumed qty back in the purchase order - if not self.is_subcontracted: - return - - self.__get_purchase_orders() - itemwise_consumed_qty = defaultdict(float) - for doctype in ["Purchase Receipt", "Purchase Invoice"]: - consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True) - - for row in consumed_items: - key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name)) - itemwise_consumed_qty[key] += row.consumed_qty - - self.__update_consumed_qty_in_po(itemwise_consumed_qty) - - def __update_consumed_qty_in_po(self, itemwise_consumed_qty): - fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"] - filters = {"docstatus": 1, "parent": ("in", self.purchase_orders)} - - for row in frappe.get_all( - "Purchase Order Item Supplied", fields=fields, filters=filters, order_by="idx" - ): - key = (row.rm_item_code, row.main_item_code, row.parent) - consumed_qty = itemwise_consumed_qty.get(key, 0) - - if row.supplied_qty < consumed_qty: - consumed_qty = row.supplied_qty - - itemwise_consumed_qty[key] -= consumed_qty - frappe.db.set_value("Purchase Order Item Supplied", row.name, "consumed_qty", consumed_qty) - - def __validate_supplied_items(self): - if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]: - return - - for row in self.get(self.raw_material_table): - key = (row.rm_item_code, row.main_item_code, row.purchase_order) - if not self.__transferred_items or not self.__transferred_items.get(key): - return - - self.__validate_batch_no(row, key) - self.__validate_serial_no(row, key) - - def __validate_batch_no(self, row, key): - if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( - "batch_no" - ): - link = get_link_to_form("Purchase Order", row.purchase_order) - msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}' - frappe.throw(_(msg), title=_("Incorrect Batch Consumed")) - - def __validate_serial_no(self, row, key): - if row.get("serial_no"): - serial_nos = get_serial_nos(row.get("serial_no")) - incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no")) - - if incorrect_sn: - incorrect_sn = "\n".join(incorrect_sn) - link = get_link_to_form("Purchase Order", row.purchase_order) - msg = f"The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}" - frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed")) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py new file mode 100644 index 0000000000..2a2f8f562e --- /dev/null +++ b/erpnext/controllers/subcontracting_controller.py @@ -0,0 +1,902 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import copy +import json +from collections import defaultdict + +import frappe +from frappe import _ +from frappe.utils import cint, cstr, flt, get_link_to_form + +from erpnext.controllers.stock_controller import StockController +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos +from erpnext.stock.utils import get_incoming_rate + + +class SubcontractingController(StockController): + def __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): + if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]: + self.remove_empty_rows() + self.set_items_conversion_factor() + + def validate(self): + 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"]: + if self.get(key): + idx = 1 + for item in self.get(key)[:]: + if not (item.get("item_code") or item.get("main_item_code")): + self.get(key).remove(item) + else: + item.idx = idx + idx += 1 + + def set_items_conversion_factor(self): + for item in self.get("items"): + if not item.conversion_factor: + item.conversion_factor = 1 + + def validate_items(self): + for item in self.items: + if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"): + msg = f"Item {item.item_name} must be a subcontracted item." + frappe.throw(_(msg)) + if item.bom: + bom = frappe.get_doc("BOM", item.bom) + if not bom.is_active: + msg = f"Please select an active BOM for Item {item.item_name}." + frappe.throw(_(msg)) + if bom.item != item.item_code: + msg = f"Please select an valid BOM for Item {item.item_name}." + frappe.throw(_(msg)) + + def __get_data_before_save(self): + item_dict = {} + if ( + self.doctype 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) + + return item_dict + + def __identify_change_in_item_table(self): + self.__changed_name = [] + self.__reference_name = [] + + if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new(): + self.set(self.raw_material_table, []) + return + + item_dict = self.__get_data_before_save() + if not item_dict: + return True + + for row in self.items: + self.__reference_name.append(row.name) + if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]: + self.__changed_name.append(row.name) + + if item_dict.get(row.name): + del item_dict[row.name] + + self.__changed_name.extend(item_dict.keys()) + + def __get_backflush_based_on(self): + self.backflush_based_on = frappe.db.get_single_value( + "Buying Settings", "backflush_raw_materials_of_subcontract_based_on" + ) + + def initialized_fields(self): + self.available_materials = frappe._dict() + self.__transferred_items = frappe._dict() + self.alternative_item_details = frappe._dict() + self.__get_backflush_based_on() + + def __get_subcontract_orders(self): + self.subcontract_orders = [] + + if self.doctype in ["Purchase Order", "Subcontracting Order"]: + return + + 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 subcontract order.""" + + self.qty_to_be_received = defaultdict(float) + + if ( + self.doctype != self.subcontract_data.order_doctype + and self.backflush_based_on != "BOM" + and self.subcontract_orders + ): + for row in frappe.get_all( + f"{self.subcontract_data.order_doctype} Item", + fields=["item_code", "(qty - received_qty) as qty", "parent", "name"], + 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 = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"] + alias_dict = { + "item_code": "rm_item_code", + "subcontracted_item": "main_item_code", + "basic_rate": "rate", + } + + child_table_fields = [ + "item_code", + "item_name", + "description", + "qty", + "basic_rate", + "amount", + "serial_no", + "uom", + "subcontracted_item", + "stock_uom", + "batch_no", + "conversion_factor", + "s_warehouse", + "t_warehouse", + "item_group", + self.subcontract_data.rm_detail_field, + ] + + if self.backflush_based_on == "BOM": + child_table_fields.append("original_item") + + for field in child_table_fields: + fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}") + + filters = [ + ["Stock Entry", "docstatus", "=", 1], + ["Stock Entry", "purpose", "=", "Send to Subcontractor"], + ["Stock Entry", self.subcontract_data.order_field, "in", self.subcontract_orders], + ] + + return frappe.get_all("Stock Entry", fields=fields, filters=filters) + + def __set_alternative_item_details(self, row): + if row.get("original_item"): + self.alternative_item_details[row.get("original_item")] = row + + def __get_received_items(self, doctype): + fields = [] + 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.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, receipt_items): + return frappe.get_all( + self.subcontract_data.receipt_supplied_items_field, + fields=[ + "serial_no", + "rm_item_code", + "reference_name", + "batch_no", + "consumed_qty", + "main_item_code", + ], + 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.""" + + receipt_items = self.__get_received_items(doctype) + if not receipt_items: + return ([], {}) if return_consumed_items else None + + receipt_items = { + item.name: item.get(self.subcontract_data.order_field) for item in receipt_items + } + consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys()) + + if return_consumed_items: + return (consumed_materials, receipt_items) + + for row in consumed_materials: + key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name)) + if not self.available_materials.get(key): + continue + + self.available_materials[key]["qty"] -= row.consumed_qty + if row.serial_no: + self.available_materials[key]["serial_no"] = list( + set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no)) + ) + + if row.batch_no: + self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty + + def get_available_materials(self): + """Get the available raw materials which has been transferred to the supplier. + available_materials = { + (item_code, subcontracted_item, subcontract_order): { + 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details + } + } + """ + if not self.subcontract_orders: + return + + for row in self.__get_transferred_items(): + 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( + key, + frappe._dict( + { + "qty": 0, + "serial_no": [], + "batch_no": defaultdict(float), + "item_details": row, + f"{self.subcontract_data.rm_detail_field}s": [], + } + ), + ) + + details = self.available_materials[key] + details.qty += row.qty + 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)) + + if row.batch_no: + details.batch_no[row.batch_no] += row.qty + + self.__set_alternative_item_details(row) + + self.__transferred_items = copy.deepcopy(self.available_materials) + 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: + return + + i = 1 + self.set(self.raw_material_table, []) + for item in self._doc_before_save.supplied_items: + if item.reference_name in self.__changed_name: + continue + + if item.reference_name not in self.__reference_name: + continue + + item.idx = i + self.append("supplied_items", item) + + i += 1 + + def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0): + doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" + fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"] + + alias_dict = { + "item_code": "rm_item_code", + "name": "bom_detail_no", + "source_warehouse": "reserve_warehouse", + } + for field in [ + "item_code", + "name", + "rate", + "stock_uom", + "source_warehouse", + "description", + "item_name", + "stock_uom", + ]: + fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}") + + filters = [ + [doctype, "parent", "=", bom_no], + [doctype, "docstatus", "=", 1], + ["BOM", "item", "=", item_code], + [doctype, "sourced_by_supplier", "=", 0], + ] + + return ( + frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or [] + ) + + def __update_reserve_warehouse(self, row, item): + if self.doctype == self.subcontract_data.order_doctype: + row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse + + def __set_alternative_item(self, bom_item): + if self.alternative_item_details.get(bom_item.rm_item_code): + bom_item.update(self.alternative_item_details[bom_item.rm_item_code]) + + def __set_serial_nos(self, item_row, rm_obj): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.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) + + # Removed the used serial nos from the list + for sn in used_serial_nos: + self.available_materials[key]["serial_no"].remove(sn) + + def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty): + rm_obj.update( + { + "consumed_qty": qty, + "batch_no": batch_no, + "required_qty": qty, + self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field), + } + ) + + self.__set_serial_nos(item_row, rm_obj) + + def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0): + rm_obj.required_qty = required_qty + rm_obj.consumed_qty = consumed_qty + + def __set_batch_nos(self, bom_item, item_row, rm_obj, qty): + key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field)) + + if self.available_materials.get(key) and self.available_materials[key]["batch_no"]: + new_rm_obj = None + for batch_no, batch_qty in self.available_materials[key]["batch_no"].items(): + if batch_qty >= qty: + self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty) + self.available_materials[key]["batch_no"][batch_no] -= qty + return + + elif qty > 0 and batch_qty > 0: + qty -= batch_qty + new_rm_obj = self.append(self.raw_material_table, bom_item) + new_rm_obj.reference_name = item_row.name + self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty) + self.available_materials[key]["batch_no"][batch_no] = 0 + + if abs(qty) > 0 and not new_rm_obj: + self.__set_consumed_qty(rm_obj, qty) + else: + self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty) + self.__set_serial_nos(item_row, rm_obj) + + def __add_supplied_item(self, item_row, bom_item, qty): + bom_item.conversion_factor = item_row.conversion_factor + rm_obj = self.append(self.raw_material_table, bom_item) + rm_obj.reference_name = item_row.name + + if self.doctype == "Subcontracting Receipt": + args = frappe._dict( + { + "item_code": rm_obj.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(rm_obj.consumed_qty), + "serial_no": rm_obj.serial_no, + "batch_no": rm_obj.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": 1, + } + ) + rm_obj.rate = get_incoming_rate(args) + + if self.doctype == 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 + 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.get(self.subcontract_data.order_field)) + + if self.qty_to_be_received == item_row.qty: + return transfer_item.qty + + if self.qty_to_be_received: + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + transfer_item.item_details.required_qty = transfer_item.qty + + if transfer_item.serial_no or frappe.get_cached_value( + "UOM", transfer_item.item_details.stock_uom, "must_be_whole_number" + ): + return frappe.utils.ceil(qty) + + return qty + + def __set_supplied_items(self): + self.bom_items = {} + + has_supplied_items = True if self.get(self.raw_material_table) else False + for row in self.items: + if self.doctype != 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 == 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") + ): + qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor + bom_item.main_item_code = row.item_code + self.__update_reserve_warehouse(bom_item, row) + self.__set_alternative_item(bom_item) + self.__add_supplied_item(row, bom_item, qty) + + elif self.backflush_based_on != "BOM": + for key, transfer_item in self.available_materials.items(): + if (key[1], key[2]) == ( + row.item_code, + row.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.get(self.subcontract_data.order_field)) + ] -= row.qty + + def __prepare_supplied_items(self): + self.initialized_fields() + self.__get_subcontract_orders() + self.__get_pending_qty_to_receive() + self.get_available_materials() + self.__remove_changed_rows() + self.__set_supplied_items() + + def __validate_batch_no(self, row, key): + if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get( + "batch_no" + ): + link = get_link_to_form( + 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): + if row.get("serial_no"): + serial_nos = get_serial_nos(row.get("serial_no")) + incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no")) + + if incorrect_sn: + incorrect_sn = "\n".join(incorrect_sn) + link = get_link_to_form( + 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 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.get(self.subcontract_data.order_field)) + if not self.__transferred_items or not self.__transferred_items.get(key): + return + + self.__validate_batch_no(row, key) + self.__validate_serial_no(row, key) + + def set_materials_for_subcontracted_items(self, raw_material_table): + 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() + self.__validate_supplied_items() + + def create_raw_materials_supplied(self, raw_material_table="supplied_items"): + self.set_materials_for_subcontracted_items(raw_material_table) + + if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]: + for item in self.get("items"): + item.rm_supp_cost = 0.0 + + 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.subcontract_orders)} + + for row in frappe.get_all( + 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) + + if row.supplied_qty < consumed_qty: + consumed_qty = row.supplied_qty + + itemwise_consumed_qty[key] -= consumed_qty + frappe.db.set_value( + self.subcontract_data.order_supplied_items_field, row.name, "consumed_qty", consumed_qty + ) + + 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 doctype in doctypes: + consumed_items, receipt_items = self.__update_consumed_materials( + doctype, return_consumed_items=True + ) + + 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 = {} + for item in self.get("items"): + if self.doctype == "Subcontracting Receipt" and item.subcontracting_order: + sco_map.setdefault(item.subcontracting_order, []).append(item.subcontracting_order_item) + + for sco, sco_item_rows in sco_map.items(): + if sco and sco_item_rows: + sco_doc = frappe.get_doc("Subcontracting Order", sco) + + if sco_doc.status in ["Closed", "Cancelled"]: + frappe.throw( + _("{0} {1} is cancelled or closed").format(_("Subcontracting Order"), sco), + frappe.InvalidStatusError, + ) + + sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows) + sco_doc.update_reserved_qty_for_subcontracting() + + def make_sl_entries_for_supplier_warehouse(self, sl_entries): + if hasattr(self, "supplied_items"): + for item in self.get("supplied_items"): + # negative quantity is passed, as raw material qty has to be decreased + # when SCR is submitted and it has to be increased when SCR is cancelled + sl_entries.append( + self.get_sl_entries( + item, + { + "item_code": item.rm_item_code, + "warehouse": self.supplier_warehouse, + "actual_qty": -1 * flt(item.consumed_qty), + "dependant_sle_voucher_detail_no": item.reference_name, + }, + ) + ) + + def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False): + self.update_ordered_and_reserved_qty() + + sl_entries = [] + stock_items = self.get_stock_items() + + for item in self.get("items"): + if item.item_code in stock_items and item.warehouse: + scr_qty = flt(item.qty) * flt(item.conversion_factor) + + if scr_qty: + sle = self.get_sl_entries( + item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()} + ) + rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9 + incoming_rate = flt(item.rate, rate_db_precision) + sle.update( + { + "incoming_rate": incoming_rate, + "recalculate_rate": 1, + } + ) + sl_entries.append(sle) + + if flt(item.rejected_qty) != 0: + sl_entries.append( + self.get_sl_entries( + item, + { + "warehouse": item.rejected_warehouse, + "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor), + "serial_no": cstr(item.rejected_serial_no).strip(), + "incoming_rate": 0.0, + "recalculate_rate": 1, + }, + ) + ) + + self.make_sl_entries_for_supplier_warehouse(sl_entries) + self.make_sl_entries( + sl_entries, + allow_negative_stock=allow_negative_stock, + via_landed_cost_voucher=via_landed_cost_voucher, + ) + + def get_supplied_items_cost(self, item_row_id, 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 + + return supplied_items_cost + + def set_subcontracting_order_status(self): + if self.doctype == "Subcontracting Order": + self.update_status() + elif self.doctype == "Subcontracting Receipt": + self.__get_subcontract_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"): + self._sub_contracted_items = [] + item_codes = list(set(item.item_code for item in self.get("items"))) + if item_codes: + items = frappe.get_all( + "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1} + ) + self._sub_contracted_items = [item.name for item in items] + + return self._sub_contracted_items + + +def 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 new file mode 100644 index 0000000000..4fab8058b8 --- /dev/null +++ b/erpnext/controllers/tests/test_subcontracting_controller.py @@ -0,0 +1,1077 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import copy +from collections import defaultdict + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import cint + +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.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 ( + make_subcontracting_receipt, +) + + +class TestSubcontractingController(FrappeTestCase): + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def test_remove_empty_rows(self): + sco = get_subcontracting_order() + len_before = len(sco.service_items) + sco.service_items[0].item_code = None + sco.remove_empty_rows() + self.assertEqual((len_before - 1), len(sco.service_items)) + + def test_create_raw_materials_supplied(self): + sco = get_subcontracting_order() + sco.supplied_items = None + sco.create_raw_materials_supplied() + self.assertIsNotNone(sco.supplied_items) + + def test_sco_with_bom(self): + """ + - Set backflush based on BOM. + - Create SCO for the item Subcontracted Item SA1 and add same item two times. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Create SCR against the SCO and check serial nos and batch no. + """ + + set_backflush_based_on("BOM") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + for key, value in get_supplied_items(scr).items(): + transferred_detais = itemwise_details.get(key) + + for field in ["qty", "serial_no", "batch_no"]: + if value.get(field): + transfer, consumed = (transferred_detais.get(field), value.get(field)) + if field == "serial_no": + transfer, consumed = (sorted(transfer), sorted(consumed)) + + self.assertEqual(transfer, consumed) + + def test_sco_with_material_transfer(self): + """ + - Set backflush based on Material Transfer. + - Create SCO for the item Subcontracted Item SA1 and Subcontracted Item SA5. + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5. + - Create partial SCR against the SCO and check serial nos and batch no. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 5", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA5", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items.append( + { + "main_item_code": "Subcontracted Item SA5", + "item_code": "Subcontracted SRM Item 4", + "qty": 6, + } + ) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.remove(scr1.items[1]) + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + transferred_detais = itemwise_details.get(key) + + for field in ["qty", "serial_no", "batch_no"]: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.save() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + transferred_detais = itemwise_details.get(key) + + for field in ["qty", "serial_no", "batch_no"]: + if value.get(field): + self.assertEqual(value.get(field), transferred_detais.get(field)) + + def test_subcontracting_with_same_components_different_fg(self): + """ + - Set backflush based on Material Transfer. + - Create SCO for the item Subcontracted Item SA2 and Subcontracted Item SA3. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of components for the item Subcontracted Item SA2. + - Create partial SCR against the SCO and check serial nos. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 3", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA3", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] += 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 3 + scr1.remove(scr1.items[1]) + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 4) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4])) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.items[0].qty = 2 + scr2.remove(scr2.items[1]) + scr2.save() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 2) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6])) + + scr3 = make_subcontracting_receipt(sco.name) + scr3.save() + scr3.submit() + + for key, value in get_supplied_items(scr3).items(): + transferred_detais = itemwise_details.get(key) + + self.assertEqual(value.qty, 6) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12])) + + def test_return_non_consumed_materials(self): + """ + - Set backflush based on Material Transfer. + - Create SCO for item Subcontracted Item SA2. + - Transfer the components from Stores to Supplier warehouse with serial nos. + - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2. + - Create SCR for full qty against the SCO and change the qty of raw material. + - After that return the non consumed material back to the store from supplier's warehouse. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] += 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + scr1.supplied_items[0].consumed_qty = 5 + scr1.supplied_items[0].serial_no = "\n".join( + sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5]) + ) + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + transferred_detais = itemwise_details.get(key) + self.assertEqual(value.qty, 5) + self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5])) + + sco.load_from_db() + self.assertEqual(sco.supplied_items[0].consumed_qty, 5) + doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) + self.assertEqual(doc.items[0].qty, 1) + self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC") + self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC") + self.assertEqual( + get_serial_nos(doc.items[0].serial_no), + itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6], + ) + + def test_item_with_batch_based_on_bom(self): + """ + - Set backflush based on BOM. + - Create SCO for item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches. + - Create the 3 SCR against the SCO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the SCR. + """ + + set_backflush_based_on("BOM") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 1", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 2", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 1.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 2 + add_second_row_in_scr(scr1) + scr1.flags.ignore_mandatory = True + scr1.save() + scr1.set_missing_values() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + self.assertEqual(value.qty, 4) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.items[0].qty = 2 + add_second_row_in_scr(scr2) + scr2.flags.ignore_mandatory = True + scr2.save() + scr2.set_missing_values() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + self.assertEqual(value.qty, 4) + + scr3 = make_subcontracting_receipt(sco.name) + scr3.items[0].qty = 2 + scr3.flags.ignore_mandatory = True + scr3.save() + scr3.set_missing_values() + scr3.submit() + + for key, value in get_supplied_items(scr3).items(): + self.assertEqual(value.qty, 2) + + def test_item_with_batch_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for item Subcontracted Item SA4 (has batch no). + - Transfer the components from Stores to Supplier warehouse with batch no and serial nos. + - Transfer the components in multiple batches with extra 2 qty for the batched item. + - Create the 3 SCR against the SCO and split Subcontracted Items into two batches. + - Keep the qty as 2 for Subcontracted Item in the SCR. + - In the first SCR the batched raw materials will be consumed 2 extra qty. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 1", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 2", + "qty": 10.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + { + "main_item_code": "Subcontracted Item SA4", + "item_code": "Subcontracted SRM Item 3", + "qty": 3.0, + "rate": 100.0, + "stock_uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + }, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 2 + add_second_row_in_scr(scr1) + scr1.flags.ignore_mandatory = True + scr1.save() + scr1.set_missing_values() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + qty = 4 if key != "Subcontracted SRM Item 3" else 6 + self.assertEqual(value.qty, qty) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.items[0].qty = 2 + add_second_row_in_scr(scr2) + scr2.flags.ignore_mandatory = True + scr2.save() + scr2.set_missing_values() + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + self.assertEqual(value.qty, 4) + + scr3 = make_subcontracting_receipt(sco.name) + scr3.items[0].qty = 2 + scr3.flags.ignore_mandatory = True + scr3.save() + scr3.set_missing_values() + scr3.submit() + + for key, value in get_supplied_items(scr3).items(): + self.assertEqual(value.qty, 1) + + def test_partial_transfer_serial_no_components_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for the item Subcontracted Item SA2. + - Transfer the partial components from Stores to Supplier warehouse with serial nos. + - Create partial SCR against the SCO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with serial nos. + - Create SCR for remaining qty against the SCO and change the qty manually. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] = 5 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 5 + scr1.flags.ignore_mandatory = True + scr1.save() + scr1.set_missing_values() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3])) + + scr1.load_from_db() + scr1.supplied_items[0].consumed_qty = 5 + scr1.supplied_items[0].serial_no = "\n".join( + itemwise_details[scr1.supplied_items[0].rm_item_code]["serial_no"] + ) + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr2 = make_subcontracting_receipt(sco.name) + scr2.submit() + + for key, value in get_supplied_items(scr2).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(sorted(value.serial_no), sorted(details.serial_no)) + + def test_incorrect_serial_no_components_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for the item Subcontracted Item SA2. + - Transfer the serialized componenets to the supplier. + - Create SCR and change the serial no which is not transferred. + - System should throw the error and not allowed to save the SCR. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.save() + scr1.supplied_items[0].serial_no = "ABCD" + self.assertRaises(frappe.ValidationError, scr1.save) + scr1.delete() + + def test_partial_transfer_batch_based_on_material_transfer(self): + """ + - Set backflush based on Material Transferred for Subcontract. + - Create SCO for the item Subcontracted Item SA6. + - Transfer the partial components from Stores to Supplier warehouse with batch. + - Create partial SCR against the SCO and change the qty manually. + - Transfer the remaining components from Stores to Supplier warehouse with batch. + - Create SCR for remaining qty against the SCO and change the qty manually. + """ + + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 6", + "qty": 10, + "rate": 100, + "fg_item": "Subcontracted Item SA6", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] = 5 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.items[0].qty = 5 + scr1.save() + + transferred_batch_no = "" + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, 3) + transferred_batch_no = details.batch_no + self.assertEqual(value.batch_no, details.batch_no) + + scr1.load_from_db() + scr1.supplied_items[0].consumed_qty = 5 + scr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0] + scr1.save() + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + itemwise_details = make_stock_in_entry(rm_items=rm_items) + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name + + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + scr1 = make_subcontracting_receipt(sco.name) + scr1.submit() + + for key, value in get_supplied_items(scr1).items(): + details = itemwise_details.get(key) + self.assertEqual(value.qty, details.qty) + self.assertEqual(value.batch_no, details.batch_no) + + def test_sco_supplied_qty(self): + """ + Check if 'Supplied Qty' in SCO's Supplied Items table is reset on submit/cancel. + """ + set_backflush_based_on("Material Transferred for Subcontract") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA1", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 5", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA5", + "fg_item_qty": 6, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + rm_items = [ + {"item_code": "Subcontracted SRM Item 1", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 2", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 3", "qty": 5, "main_item_code": "Subcontracted Item SA1"}, + {"item_code": "Subcontracted SRM Item 5", "qty": 6, "main_item_code": "Subcontracted Item SA5"}, + {"item_code": "Subcontracted SRM Item 4", "qty": 6, "main_item_code": "Subcontracted Item SA5"}, + ] + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + for item in rm_items: + item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name + + se = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + + sco.reload() + for item in sco.get("supplied_items"): + self.assertIn(item.supplied_qty, [5.0, 6.0]) + + se.cancel() + sco.reload() + for item in sco.get("supplied_items"): + self.assertEqual(item.supplied_qty, 0.0) + + +def add_second_row_in_scr(scr): + item_dict = {} + for column in [ + "item_code", + "item_name", + "qty", + "uom", + "warehouse", + "stock_uom", + "subcontracting_order", + "subcontracting_order_finished_good_item", + "conversion_factor", + "rate", + "expense_account", + "sco_rm_detail", + ]: + item_dict[column] = scr.items[0].get(column) + + scr.append("items", item_dict) + + +def get_supplied_items(scr_doc): + supplied_items = {} + for row in scr_doc.get("supplied_items"): + if row.rm_item_code not in supplied_items: + supplied_items.setdefault( + row.rm_item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) + + details = supplied_items[row.rm_item_code] + update_item_details(row, details) + + return supplied_items + + +def make_stock_in_entry(**args): + args = frappe._dict(args) + + items = {} + for row in args.rm_items: + row = frappe._dict(row) + + doc = make_stock_entry( + target=row.warehouse or "_Test Warehouse - _TC", + item_code=row.item_code, + qty=row.qty or 1, + basic_rate=row.rate or 100, + ) + + if row.item_code not in items: + items.setdefault( + row.item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)}) + ) + + child_row = doc.items[0] + details = items[child_row.item_code] + update_item_details(child_row, details) + + return items + + +def update_item_details(child_row, details): + details.qty += ( + child_row.get("qty") + if child_row.doctype == "Stock Entry Detail" + else child_row.get("consumed_qty") + ) + + if child_row.serial_no: + details.serial_no.extend(get_serial_nos(child_row.serial_no)) + + if child_row.batch_no: + details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty") + + +def make_stock_transfer_entry(**args): + args = frappe._dict(args) + + items = [] + for row in args.rm_items: + row = frappe._dict(row) + + item = { + "item_code": row.main_item_code or args.main_item_code, + "rm_item_code": row.item_code, + "qty": row.qty or 1, + "item_name": row.item_code, + "rate": row.rate or 100, + "stock_uom": row.stock_uom or "Nos", + "warehouse": row.warehuose or "_Test Warehouse - _TC", + } + + item_details = args.itemwise_details.get(row.item_code) + + if item_details and item_details.serial_no: + serial_nos = item_details.serial_no[0 : cint(row.qty)] + item["serial_no"] = "\n".join(serial_nos) + item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos)) + + if item_details and item_details.batch_no: + for batch_no, batch_qty in item_details.batch_no.items(): + if batch_qty >= row.qty: + item["batch_no"] = batch_no + item_details.batch_no[batch_no] -= row.qty + break + + items.append(item) + + ste_dict = make_rm_stock_entry(args.sco_no, items) + doc = frappe.get_doc(ste_dict) + doc.insert() + doc.submit() + + return doc + + +def make_subcontracted_items(): + sub_contracted_items = { + "Subcontracted Item SA1": {}, + "Subcontracted Item SA2": {}, + "Subcontracted Item SA3": {}, + "Subcontracted Item SA4": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "SBAT.####", + }, + "Subcontracted Item SA5": {}, + "Subcontracted Item SA6": {}, + "Subcontracted Item SA7": {}, + } + + for item, properties in sub_contracted_items.items(): + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1, "is_sub_contracted_item": 1}) + make_item(item, properties) + + +def make_raw_materials(): + raw_materials = { + "Subcontracted SRM Item 1": {}, + "Subcontracted SRM Item 2": {"has_serial_no": 1, "serial_no_series": "SRI.####"}, + "Subcontracted SRM Item 3": { + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT.####", + }, + "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"}, + } + + for item, properties in raw_materials.items(): + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 1}) + make_item(item, properties) + + +def make_service_item(item, properties={}): + if not frappe.db.exists("Item", item): + properties.update({"is_stock_item": 0}) + make_item(item, properties) + + +def make_service_items(): + service_items = { + "Subcontracted Service Item 1": {}, + "Subcontracted Service Item 2": {}, + "Subcontracted Service Item 3": {}, + "Subcontracted Service Item 4": {}, + "Subcontracted Service Item 5": {}, + "Subcontracted Service Item 6": {}, + "Subcontracted Service Item 7": {}, + } + + for item, properties in service_items.items(): + make_service_item(item, properties) + + +def make_bom_for_subcontracted_items(): + boms = { + "Subcontracted Item SA1": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA2": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA3": ["Subcontracted SRM Item 2"], + "Subcontracted Item SA4": [ + "Subcontracted SRM Item 1", + "Subcontracted SRM Item 2", + "Subcontracted SRM Item 3", + ], + "Subcontracted Item SA5": ["Subcontracted SRM Item 5"], + "Subcontracted Item SA6": ["Subcontracted SRM Item 3"], + "Subcontracted Item SA7": ["Subcontracted SRM Item 1"], + } + + for item_code, raw_materials in boms.items(): + if not frappe.db.exists("BOM", {"item": item_code}): + make_bom(item=item_code, raw_materials=raw_materials, rate=100) + + +def set_backflush_based_on(based_on): + frappe.db.set_value( + "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on + ) + + +def get_subcontracting_order(**args): + from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import ( + create_subcontracting_order, + ) + + args = frappe._dict(args) + + if args.get("po_name"): + po = frappe.get_doc("Purchase Order", args.get("po_name")) + + if po.is_subcontracted: + return create_subcontracting_order(po_name=po.name, **args) + + if not args.service_items: + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 7", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA7", + "fg_item_qty": 10, + }, + ] + else: + service_items = args.service_items + + po = create_purchase_order( + rm_items=service_items, + is_subcontracted=1, + supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC", + ) + + return create_subcontracting_order(po_name=po.name, **args) + + +def get_rm_items(supplied_items): + rm_items = [] + + for item in supplied_items: + rm_items.append( + { + "main_item_code": item.main_item_code, + "item_code": item.rm_item_code, + "qty": item.required_qty, + "rate": item.rate, + "stock_uom": item.stock_uom, + "warehouse": item.reserve_warehouse, + } + ) + + return rm_items + + +def make_subcontracted_item(**args): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + args = frappe._dict(args) + + if not frappe.db.exists("Item", args.item_code): + make_item( + args.item_code, + { + "is_stock_item": 1, + "is_sub_contracted_item": 1, + "has_batch_no": args.get("has_batch_no") or 0, + }, + ) + + if not args.raw_materials: + if not frappe.db.exists("Item", "Test Extra Item 1"): + make_item( + "Test Extra Item 1", + { + "is_stock_item": 1, + }, + ) + + if not frappe.db.exists("Item", "Test Extra Item 2"): + make_item( + "Test Extra Item 2", + { + "is_stock_item": 1, + }, + ) + + args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] + + if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): + make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 860512c91d..a190cc7430 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -9,7 +9,7 @@ import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.controllers.tests.test_subcontracting_controller import set_backflush_based_on from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( update_cost_in_all_boms_in_test, @@ -18,7 +18,6 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.test_subcontracting import set_backflush_based_on test_records = frappe.get_test_records("BOM") test_dependencies = ["Item", "Quality Inspection Template"] @@ -256,12 +255,29 @@ class TestBOM(FrappeTestCase): bom.submit() # test that sourced_by_supplier rate is zero even after updating cost self.assertEqual(bom.items[2].rate, 0) - # test in Purchase Order sourced_by_supplier is not added to Supplied Item - po = create_purchase_order( - item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" + + from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, + ) + + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": 1, + }, + ] + # test in Subcontracting Order sourced_by_supplier is not added to Supplied Item + sco = get_subcontracting_order( + service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC" ) bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1]) - supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) + supplied_items = sorted([d.rm_item_code for d in sco.supplied_items]) self.assertEqual(bom_items, supplied_items) def test_bom_tree_representation(self): diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a73b9bcc69..70ccb78278 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -508,7 +508,7 @@ class ProductionPlan(Document): po.is_subcontracted = 1 for row in po_list: po_data = { - "item_code": row.production_item, + "fg_item": row.production_item, "warehouse": row.fg_warehouse, "production_plan_sub_assembly_item": row.name, "bom": row.bom_no, @@ -518,9 +518,6 @@ class ProductionPlan(Document): for field in [ "schedule_date", "qty", - "uom", - "stock_uom", - "item_name", "description", "production_plan_item", ]: diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py index a86c7a47c3..5083b7369d 100644 --- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py +++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py @@ -36,7 +36,7 @@ def get_data(filters): "total_time_in_mins", ] - for field in ["work_order", "workstation", "operation", "company"]: + for field in ["work_order", "workstation", "operation", "status", "company"]: if filters.get(field): query_filters[field] = ("in", filters.get(field)) diff --git a/erpnext/modules.txt b/erpnext/modules.txt index 869166b939..01ebed7ddd 100644 --- a/erpnext/modules.txt +++ b/erpnext/modules.txt @@ -21,3 +21,4 @@ Payroll Telephony Bulk Transaction E-commerce +Subcontracting \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index aef80bca68..9eaecbdc68 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -343,6 +343,7 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note execute:frappe.delete_doc("DocType", "Naming Series") erpnext.patches.v13_0.set_payroll_entry_status erpnext.patches.v13_0.job_card_status_on_hold +erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.crm_ux_cleanup erpnext.patches.v14_0.remove_india_localisation diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py index 38a8500ac7..7ad2bec859 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/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index f6427ca55a..75a5477be8 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -15,6 +15,8 @@ def execute(): ("accounts", "sales_invoice_item"), ("accounts", "purchase_invoice_item"), ("buying", "purchase_receipt_item_supplied"), + ("subcontracting", "subcontracting_receipt_item"), + ("subcontracting", "subcontracting_receipt_supplied_item"), ] for module, doctype in doctypes_to_reload: diff --git a/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py new file mode 100644 index 0000000000..607ef69538 --- /dev/null +++ b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py @@ -0,0 +1,12 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + + +def execute(): + for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: + tab = frappe.qb.DocType(doctype).as_("tab") + frappe.qb.update(tab).set(tab.is_old_subcontracting_flow, 1).where( + tab.is_subcontracted == 1 + ).run() diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a5b7699040..09779d89ec 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_sub_contracted_item': 1 } + filters: filters } } else { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 62b98ec5bf..6b618b2435 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -470,7 +470,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 096175a68d..62abb74c34 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/selling/doctype/quotation/regional/india.js b/erpnext/selling/doctype/quotation/regional/india.js deleted file mode 100644 index 955083565b..0000000000 --- a/erpnext/selling/doctype/quotation/regional/india.js +++ /dev/null @@ -1,3 +0,0 @@ -{% include "erpnext/regional/india/taxes.js" %} - -erpnext.setup_auto_gst_taxation('Quotation'); diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 9ffd6dfddc..bffa829e16 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -10,8 +10,9 @@ from frappe import _ from frappe.cache_manager import clear_defaults_cache from frappe.contacts.address_and_contact import load_address_and_contact from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.utils import cint, formatdate, get_timestamp, today -from frappe.utils.nestedset import NestedSet +from frappe.utils.nestedset import NestedSet, rebuild_tree from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges @@ -150,9 +151,7 @@ class Company(NestedSet): self.create_default_tax_template() if not frappe.db.get_value("Department", {"company": self.name}): - from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures - - install_post_company_fixtures(frappe._dict({"company_name": self.name})) + self.create_default_departments() if not frappe.local.flags.ignore_chart_of_accounts: self.set_default_accounts() @@ -224,6 +223,104 @@ class Company(NestedSet): ), ) + def create_default_departments(self): + records = [ + # Department + { + "doctype": "Department", + "department_name": _("All Departments"), + "is_group": 1, + "parent_department": "", + "__condition": lambda: not frappe.db.exists("Department", _("All Departments")), + }, + { + "doctype": "Department", + "department_name": _("Accounts"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Marketing"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Sales"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Purchase"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Operations"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Production"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Dispatch"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Customer Service"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Human Resources"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Management"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Quality Management"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Research & Development"), + "parent_department": _("All Departments"), + "company": self.name, + }, + { + "doctype": "Department", + "department_name": _("Legal"), + "parent_department": _("All Departments"), + "company": self.name, + }, + ] + + # Make root department with NSM updation + make_records(records[:1]) + + frappe.local.flags.ignore_update_nsm = True + make_records(records) + frappe.local.flags.ignore_update_nsm = False + rebuild_tree("Department", "parent_department") + def validate_coa_input(self): if self.create_chart_of_accounts_based_on == "Existing Company": self.chart_of_accounts = None diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 4235e1f7db..0d329ba787 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -12,7 +12,6 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import ( ) from frappe.desk.page.setup_wizard.setup_wizard import make_records from frappe.utils import cstr, getdate -from frappe.utils.nestedset import rebuild_tree from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.regional.address_template.setup import set_up_address_templates @@ -656,104 +655,6 @@ def install_company(args): make_records(records) -def install_post_company_fixtures(args=None): - records = [ - # Department - { - "doctype": "Department", - "department_name": _("All Departments"), - "is_group": 1, - "parent_department": "", - }, - { - "doctype": "Department", - "department_name": _("Accounts"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Marketing"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Sales"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Purchase"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Operations"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Production"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Dispatch"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Customer Service"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Human Resources"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Management"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Quality Management"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Research & Development"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - { - "doctype": "Department", - "department_name": _("Legal"), - "parent_department": _("All Departments"), - "company": args.company_name, - }, - ] - - # Make root department with NSM updation - make_records(records[:1]) - - frappe.local.flags.ignore_update_nsm = True - make_records(records[1:]) - frappe.local.flags.ignore_update_nsm = False - rebuild_tree("Department", "parent_department") - - def install_defaults(args=None): records = [ # Price Lists diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index bc235d976b..548df318fa 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -40,25 +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 - po = frappe.qb.DocType("Purchase Order") - supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") + 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_(po) + 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) - & (po.name == supplied_item.parent) - & (po.docstatus == 1) - & (po.is_subcontracted) - & (po.status != "Closed") - & (po.per_received < 100) - & (supplied_item.reserve_warehouse == self.warehouse) - ) + .where(conditions) ).run()[0][0] or 0.0 se = frappe.qb.DocType("Stock Entry") @@ -71,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_(po) + .from_(subcontract_order) .select(Sum(qty_field)) - .where( - (se.docstatus == 1) - & (se.purpose == "Send to Subcontractor") - & (Coalesce(se.purchase_order, "") != "") - & ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code)) - & (se.name == se_item.parent) - & (po.name == se.purchase_order) - & (po.docstatus == 1) - & (po.is_subcontracted == 1) - & (po.status != "Closed") - & (po.per_received < 100) - ) + .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 b8f4803c26..199641803e 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -1,17 +1,16 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import json - import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import flt -from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_purchase_receipt, - make_rm_stock_entry, +from erpnext.controllers.subcontracting_controller import make_rm_stock_entry +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_subcontracting_order, + make_service_item, + set_backflush_based_on, ) -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry @@ -22,6 +21,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_subcontracting_receipt, +) class TestItemAlternative(FrappeTestCase): @@ -30,9 +32,7 @@ class TestItemAlternative(FrappeTestCase): make_items() def test_alternative_item_for_subcontract_rm(self): - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) + set_backflush_based_on("BOM") create_stock_reconciliation( item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 @@ -42,15 +42,22 @@ class TestItemAlternative(FrappeTestCase): ) supplier_warehouse = "Test Supplier Warehouse - _TC" - po = create_purchase_order( - item="Test Finished Goods - A", - is_subcontracted=1, - qty=5, - rate=3000, - supplier_warehouse=supplier_warehouse, - ) - rm_item = [ + make_service_item("Subcontracted Service Item 1") + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 5, + "rate": 3000, + "fg_item": "Test Finished Goods - A", + "fg_item_qty": 5, + }, + ] + sco = get_subcontracting_order( + service_items=service_items, supplier_warehouse=supplier_warehouse + ) + rm_items = [ { "item_code": "Test Finished Goods - A", "rm_item_code": "Test FG A RW 1", @@ -73,14 +80,13 @@ class TestItemAlternative(FrappeTestCase): }, ] - rm_item_string = json.dumps(rm_item) reserved_qty_for_sub_contract = frappe.db.get_value( "Bin", {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, "reserved_qty_for_sub_contract", ) - se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) + se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) se.to_warehouse = supplier_warehouse se.insert() @@ -104,22 +110,17 @@ class TestItemAlternative(FrappeTestCase): after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) ) - pr = make_purchase_receipt(po.name) - pr.save() + scr = make_subcontracting_receipt(sco.name) + scr.save() - pr = frappe.get_doc("Purchase Receipt", pr.name) + scr = frappe.get_doc("Subcontracting Receipt", scr.name) status = False - for d in pr.supplied_items: - if d.rm_item_code == "Alternate Item For A RW 1": + for item in scr.supplied_items: + if item.rm_item_code == "Alternate Item For A RW 1": status = True self.assertEqual(status, True) - frappe.db.set_value( - "Buying Settings", - None, - "backflush_raw_materials_of_subcontract_based_on", - "Material Transferred for Subcontract", - ) + set_backflush_based_on("Material Transferred for Subcontract") def test_alternative_item_for_production_rm(self): create_stock_reconciliation( diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 754404b068..312c166f8b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -198,7 +198,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status")) } - this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted); + this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_old_subcontracting_flow); } make_purchase_invoice() { @@ -296,10 +296,11 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt frappe.provide("erpnext.buying"); frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) { - if (frm.doc.is_subcontracted) { + if (frm.doc.is_old_subcontracting_flow) { erpnext.buying.get_default_bom(frm); } - frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); + + frm.toggle_reqd("supplier_warehouse", frm.doc.is_old_subcontracting_flow); }); frappe.ui.form.on('Purchase Receipt Item', { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 923ceb36cd..a70415dfc3 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -133,7 +133,8 @@ "transporter_name", "column_break5", "lr_no", - "lr_date" + "lr_date", + "is_old_subcontracting_flow" ], "fields": [ { @@ -442,7 +443,8 @@ "label": "Is Subcontracted", "oldfieldname": "is_subcontracted", "oldfieldtype": "Select", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "depends_on": "eval:doc.is_subcontracted", @@ -1142,13 +1144,21 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_old_subcontracting_flow", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Old Subcontracting Flow", + "read_only": 1 } ], "icon": "fa fa-truck", "idx": 261, "is_submittable": 1, "links": [], - "modified": "2022-05-27 15:59:18.550583", + "modified": "2022-06-15 15:43:40.664382", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 704e1fc2f9..84da3cc41d 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,7 +235,7 @@ class PurchaseReceipt(BuyingController): self.make_gl_entries() self.repost_future_sle_and_gle() - self.set_consumed_qty_in_po() + self.set_consumed_qty_in_subcontract_order() def check_next_docstatus(self): submit_rv = frappe.db.sql( @@ -270,18 +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_po() - - @frappe.whitelist() - def get_current_stock(self): - for d in self.get("supplied_items"): - if self.supplier_warehouse: - bin = frappe.db.sql( - "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", - (d.rm_item_code, self.supplier_warehouse), - as_dict=1, - ) - d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0 + 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/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index be4f27465e..d0d115d96a 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2,10 +2,6 @@ # License: GNU General Public License v3. See license.txt -import json -import unittest -from collections import defaultdict - import frappe from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today @@ -311,142 +307,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) - def test_subcontracting(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) - - make_stock_entry( - item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100 - ) - make_stock_entry( - item_code="_Test Item Home Desktop 100", - qty=100, - target="_Test Warehouse 1 - _TC", - basic_rate=100, - ) - pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted=1) - self.assertEqual(len(pr.get("supplied_items")), 2) - - rm_supp_cost = sum(d.amount for d in pr.get("supplied_items")) - self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) - - pr.cancel() - - def test_subcontracting_gle_fg_item_rate_zero(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) - - se1 = make_stock_entry( - item_code="_Test Item", - target="Work In Progress - TCP1", - qty=100, - basic_rate=100, - company="_Test Company with perpetual inventory", - ) - - se2 = make_stock_entry( - item_code="_Test Item Home Desktop 100", - target="Work In Progress - TCP1", - qty=100, - basic_rate=100, - company="_Test Company with perpetual inventory", - ) - - pr = make_purchase_receipt( - item_code="_Test FG Item", - qty=10, - rate=0, - is_subcontracted=1, - company="_Test Company with perpetual inventory", - warehouse="Stores - TCP1", - supplier_warehouse="Work In Progress - TCP1", - ) - - gl_entries = get_gl_entries("Purchase Receipt", pr.name) - - self.assertFalse(gl_entries) - - pr.cancel() - se1.cancel() - se2.cancel() - - def test_subcontracting_over_receipt(self): - """ - Behaviour: Raise multiple PRs against one PO that in total - receive more than the required qty in the PO. - Expected Result: Error Raised for Over Receipt against PO. - """ - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt - from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_rm_stock_entry as make_subcontract_transfer_entry, - ) - from erpnext.buying.doctype.purchase_order.test_purchase_order import ( - create_purchase_order, - make_subcontracted_item, - update_backflush_based_on, - ) - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - update_backflush_based_on("Material Transferred for Subcontract") - item_code = "_Test Subcontracted FG Item 1" - make_subcontracted_item(item_code=item_code) - - po = create_purchase_order( - item_code=item_code, - qty=1, - include_exploded_items=0, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - ) - - # stock raw materials in a warehouse before transfer - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100 - ) - make_stock_entry( - target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100 - ) - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": po.supplied_items[0].rm_item_code, - "item_name": "_Test FG Item", - "qty": po.supplied_items[0].required_qty, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - { - "item_code": item_code, - "rm_item_code": po.supplied_items[1].rm_item_code, - "item_name": "Test Extra Item 1", - "qty": po.supplied_items[1].required_qty, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - }, - ] - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.to_warehouse = "_Test Warehouse 1 - _TC" - se.save() - se.submit() - - pr1 = make_purchase_receipt(po.name) - pr2 = make_purchase_receipt(po.name) - - pr1.submit() - self.assertRaises(frappe.ValidationError, pr2.submit) - frappe.db.rollback() - def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr_row_1_serial_no = pr.get("items")[0].serial_no @@ -1133,103 +993,6 @@ class TestPurchaseReceipt(FrappeTestCase): pr.cancel() pr1.cancel() - def test_subcontracted_pr_for_multi_transfer_batches(self): - from erpnext.buying.doctype.purchase_order.purchase_order import ( - make_purchase_receipt, - make_rm_stock_entry, - ) - from erpnext.buying.doctype.purchase_order.test_purchase_order import ( - create_purchase_order, - update_backflush_based_on, - ) - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - - update_backflush_based_on("Material Transferred for Subcontract") - item_code = "_Test Subcontracted FG Item 3" - - make_item( - "Sub Contracted Raw Material 3", - {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1}, - ) - - create_subcontracted_item( - item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"] - ) - - order_qty = 500 - po = create_purchase_order( - item_code=item_code, - qty=order_qty, - is_subcontracted=1, - supplier_warehouse="_Test Warehouse 1 - _TC", - ) - - ste1 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 3", - qty=300, - basic_rate=100, - ) - ste2 = make_stock_entry( - target="_Test Warehouse - _TC", - item_code="Sub Contracted Raw Material 3", - qty=200, - basic_rate=100, - ) - - transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200} - - rm_items = [ - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 3", - "item_name": "_Test Item", - "qty": 300, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - "name": po.supplied_items[0].name, - }, - { - "item_code": item_code, - "rm_item_code": "Sub Contracted Raw Material 3", - "item_name": "_Test Item", - "qty": 200, - "warehouse": "_Test Warehouse - _TC", - "stock_uom": "Nos", - "name": po.supplied_items[0].name, - }, - ] - - rm_item_string = json.dumps(rm_items) - se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string)) - self.assertEqual(len(se.items), 2) - se.items[0].batch_no = ste1.items[0].batch_no - se.items[1].batch_no = ste2.items[0].batch_no - se.submit() - - supplied_qty = frappe.db.get_value( - "Purchase Order Item Supplied", - {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, - "supplied_qty", - ) - - self.assertEqual(supplied_qty, 500.00) - - pr = make_purchase_receipt(po.name) - pr.save() - self.assertEqual(len(pr.supplied_items), 2) - - for row in pr.supplied_items: - self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty) - - update_backflush_based_on("BOM") - - pr.delete() - se.cancel() - ste2.cancel() - ste1.cancel() - po.cancel() - def test_po_to_pi_and_po_to_pr_worflow_full(self): """Test following behaviour: - Create PO @@ -1568,43 +1331,5 @@ def make_purchase_receipt(**args): return pr -def create_subcontracted_item(**args): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - args = frappe._dict(args) - - if not frappe.db.exists("Item", args.item_code): - make_item( - args.item_code, - { - "is_stock_item": 1, - "is_sub_contracted_item": 1, - "has_batch_no": args.get("has_batch_no") or 0, - }, - ) - - if not args.raw_materials: - if not frappe.db.exists("Item", "Test Extra Item 1"): - make_item( - "Test Extra Item 1", - { - "is_stock_item": 1, - }, - ) - - if not frappe.db.exists("Item", "Test Extra Item 2"): - make_item( - "Test Extra Item 2", - { - "is_stock_item": 1, - }, - ) - - args.raw_materials = ["_Test FG Item", "Test Extra Item 1"] - - if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"): - make_bom(item=args.item_code, raw_materials=args.get("raw_materials")) - - test_dependencies = ["BOM", "Item Price", "Location"] test_records = frappe.get_test_records("Purchase Receipt") diff --git a/erpnext/stock/doctype/purchase_receipt/test_records.json b/erpnext/stock/doctype/purchase_receipt/test_records.json index 990ad12b30..e7ea9af6b9 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_records.json +++ b/erpnext/stock/doctype/purchase_receipt/test_records.json @@ -83,37 +83,5 @@ } ], "supplier": "_Test Supplier" - }, - - { - "buying_price_list": "_Test Price List", - "company": "_Test Company", - "conversion_rate": 1.0, - "currency": "INR", - "doctype": "Purchase Receipt", - "base_grand_total": 5000.0, - "is_subcontracted": 1, - "base_net_total": 5000.0, - "items": [ - { - "base_amount": 5000.0, - "conversion_factor": 1.0, - "description": "_Test FG Item", - "doctype": "Purchase Receipt Item", - "item_code": "_Test FG Item", - "item_name": "_Test FG Item", - "parentfield": "items", - "qty": 10.0, - "rate": 500.0, - "received_qty": 10.0, - "rejected_qty": 0.0, - "stock_uom": "_Test UOM", - "uom": "_Test UOM", - "warehouse": "_Test Warehouse - _TC", - "cost_center": "Main - _TC" - } - ], - "supplier": "_Test Supplier", - "supplier_warehouse": "_Test Warehouse - _TC" } ] \ No newline at end of file diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index b45d66391c..c97dbee911 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -645,12 +645,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 + "print_hide": 1, + "read_only": 1, + "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow" }, { "default": "0", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 7101190b6b..6042ed4ac5 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -687,7 +687,10 @@ def update_serial_nos_after_submit(controller, parentfield): update_rejected_serial_nos = ( True - if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty) + if ( + controller.doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt") + and d.rejected_qty + ) else False ) accepted_serial_nos_updated = False @@ -700,7 +703,11 @@ def update_serial_nos_after_submit(controller, parentfield): qty = d.stock_qty else: warehouse = d.warehouse - qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty + qty = ( + d.qty + if controller.doctype in ["Stock Reconciliation", "Subcontracting Receipt"] + else d.stock_qty + ) for sle in stock_ledger_entries: if sle.voucher_detail_no == d.name: if ( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 540ad186e1..1c514a90ee 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -613,7 +613,25 @@ frappe.ui.form.on('Stock Entry', { apply_putaway_rule: function (frm) { if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); - } + }, + + 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, + target_doc: frm, + freeze: true, + }); + } + }, }); frappe.ui.form.on('Stock Entry Detail', { @@ -780,7 +798,16 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle return { "filters": { "docstatus": 1, - "is_subcontracted": 1, + "is_old_subcontracting_flow": 1, + "company": me.frm.doc.company + } + }; + }); + + this.frm.set_query("subcontracting_order", function() { + return { + "filters": { + "docstatus": 1, "company": me.frm.doc.company } }; @@ -801,7 +828,12 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle } } - this.frm.add_fetch("purchase_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 f56e059f81..abe98e2933 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -15,6 +15,7 @@ "add_to_transit", "work_order", "purchase_order", + "subcontracting_order", "delivery_note_no", "sales_invoice_no", "pick_list", @@ -147,12 +148,19 @@ "search_index": 1 }, { - "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", - "fieldname": "purchase_order", - "fieldtype": "Link", - "label": "Purchase Order", - "options": "Purchase Order" + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Purchase Order", + "options": "Purchase Order" }, + { + "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", + "fieldname": "subcontracting_order", + "fieldtype": "Link", + "label": "Subcontracting Order", + "options": "Subcontracting Order" + }, { "depends_on": "eval:doc.purpose==\"Sales Return\"", "fieldname": "delivery_note_no", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 46a1e70b05..9c49408289 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,9 @@ class StockEntry(StockController): update_serial_nos_after_submit(self, "items") self.update_work_order() - self.validate_purchase_order() - self.update_purchase_order_supplied_items() + self.validate_subcontract_order() + self.update_subcontract_order_supplied_items() + self.update_subcontracting_order_status() self.make_gl_entries() @@ -154,7 +176,8 @@ class StockEntry(StockController): self.set_material_request_transfer_status("Completed") def on_cancel(self): - self.update_purchase_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": self.validate_work_order_status() @@ -792,8 +815,8 @@ class StockEntry(StockController): serial_nos.append(sn) - def validate_purchase_order(self): - """Throw exception if more raw material is transferred against Purchase Order than in + def validate_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" @@ -801,24 +824,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.purchase_order): + if not (self.purpose == "Send to Subcontractor" and self.get(self.subcontract_data.order_field)): return if backflush_raw_materials_based_on == "BOM": - purchase_order = frappe.get_doc("Purchase Order", self.purchase_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 purchase_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( - "Purchase Order Item", - {"parent": self.purchase_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", ) @@ -830,7 +858,7 @@ class StockEntry(StockController): required_qty = sum( [ flt(d.required_qty) - for d in purchase_order.supplied_items + for d in subcontract_order.supplied_items if d.rm_item_code == original_item_code ] ) @@ -839,26 +867,57 @@ class StockEntry(StockController): if not required_qty: frappe.throw( - _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format( - se_item.item_code, self.purchase_order + _("Item {0} not found in 'Raw Materials Supplied' table in {1} {2}").format( + se_item.item_code, + self.subcontract_data.order_doctype, + self.get(self.subcontract_data.order_field), ) ) - total_supplied = frappe.db.sql( - """select sum(transfer_qty) - from `tabStock Entry Detail`, `tabStock Entry` - where `tabStock Entry`.purchase_order = %s - and `tabStock Entry`.docstatus = 1 - and `tabStock Entry Detail`.item_code = %s - and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", - (self.purchase_order, se_item.item_code), - )[0][0] + + 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(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 Purchase Order {3}").format( - se_item.idx, se_item.item_code, total_allowed, self.purchase_order + _("Row {0}# Item {1} cannot be transferred more than {2} against {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.get(self.subcontract_data.rm_detail_field): + filters = { + "parent": self.get(self.subcontract_data.order_field), + "docstatus": 1, + "rm_item_code": se_item.item_code, + "main_item_code": se_item.subcontracted_item, + } + + 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: @@ -867,17 +926,19 @@ class StockEntry(StockController): row.idx, frappe.bold(row.item_code) ) ) - elif not row.po_detail: + elif not row.get(self.subcontract_data.rm_detail_field): filters = { - "parent": self.purchase_order, + "parent": self.get(self.subcontract_data.order_field), "docstatus": 1, "rm_item_code": row.item_code, "main_item_code": row.subcontracted_item, } - po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") - if po_detail: - row.db_set("po_detail", po_detail) + 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"): @@ -1224,11 +1285,13 @@ class StockEntry(StockController): args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]) if ( - self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code") + self.purpose == "Send to Subcontractor" + and self.get(self.subcontract_data.order_field) + and args.get("item_code") ): subcontract_items = frappe.get_all( - "Purchase Order Item Supplied", - {"parent": self.purchase_order, "rm_item_code": args.get("item_code")}, + 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", ) @@ -1322,27 +1385,27 @@ class StockEntry(StockController): item_dict = self.get_bom_raw_materials(self.fg_completed_qty) - # Get PO Supplied Items Details - if self.purchase_order and self.purpose == "Send to Subcontractor": - # Get PO Supplied Items Details - item_wh = frappe._dict( - frappe.db.sql( - """ - SELECT - rm_item_code, reserve_warehouse - FROM - `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup - WHERE - po.name = poitemsup.parent and po.name = %s """, - self.purchase_order, - ) - ) + # Get 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.get(self.subcontract_data.order_field)) + ).run(as_list=True) + + item_wh = frappe._dict(item_wh) for item in item_dict.values(): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): item["from_warehouse"] = self.pro_doc.wip_warehouse - # Get Reserve Warehouse from PO - if self.purchase_order and self.purpose == "Send to Subcontractor": + # Get Reserve Warehouse from 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 "" @@ -1478,7 +1541,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"]: @@ -1844,7 +1909,7 @@ class StockEntry(StockController): se_child.is_process_loss = item_row.get("is_process_loss", 0) for field in [ - "po_detail", + self.subcontract_data.rm_detail_field, "original_item", "expense_account", "description", @@ -1918,33 +1983,37 @@ class StockEntry(StockController): else: frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) - def update_purchase_order_supplied_items(self): - if self.purchase_order and ( + def update_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 PO Supplied Items Details - po_supplied_items = frappe.db.get_all( - "Purchase Order Item Supplied", - filters={"parent": self.purchase_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 PO - supplied_items = get_supplied_items(self.purchase_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 po_supplied_items: + for row in order_supplied_items: key, item = row.name, {} if not supplied_items.get(key): - # no stock transferred against PO 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("Purchase Order Item Supplied", 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 po_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 @@ -2145,6 +2214,14 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + def update_subcontracting_order_status(self): + if self.subcontracting_order and self.purpose == "Send to Subcontractor": + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + update_subcontracting_order_status, + ) + + update_subcontracting_order_status(self.subcontracting_order) + def set_missing_values(self): "Updates rate and availability of all the items of mapped doc." self.set_transfer_qty() @@ -2293,13 +2370,13 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): return operating_cost_per_unit -def get_used_alternative_items(purchase_order=None, work_order=None): +def get_used_alternative_items( + subcontract_order=None, subcontract_order_field="subcontracting_order", work_order=None +): cond = "" - if purchase_order: - cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format( - purchase_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 @@ -2352,7 +2429,6 @@ def get_valuation_rate_for_finished_good_entry(work_order): @frappe.whitelist() def get_uom_details(item_code, uom, qty): """Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}` - :param args: dict with `item_code`, `uom` and `qty`""" conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") @@ -2436,25 +2512,27 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None): return sample_quantity -def get_supplied_items(purchase_order): +def get_supplied_items( + 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`.`po_detail`", + f"`tabStock Entry Detail`.`{rm_detail_field}`", "`tabStock Entry Detail`.`item_code`", ] filters = [ ["Stock Entry", "docstatus", "=", 1], - ["Stock Entry", "purchase_order", "=", purchase_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.po_detail: + if not row.get(rm_detail_field): continue - key = row.po_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}) @@ -2474,6 +2552,39 @@ def get_supplied_items(purchase_order): return supplied_item_details +@frappe.whitelist() +def get_items_from_subcontracting_order(source_name, target_doc=None): + sco = frappe.get_doc("Subcontracting Order", source_name) + + if sco.docstatus == 1: + if target_doc and isinstance(target_doc, str): + target_doc = frappe.get_doc(json.loads(target_doc)) + + if target_doc.items: + target_doc.items = [] + + warehouses = {} + for item in sco.items: + warehouses[item.name] = item.warehouse + + for item in sco.supplied_items: + target_doc.append( + "items", + { + "s_warehouse": warehouses.get(item.reference_name), + "t_warehouse": sco.supplier_warehouse, + "item_code": item.rm_item_code, + "qty": item.required_qty, + "transfer_qty": item.required_qty, + "uom": item.stock_uom, + "stock_uom": item.stock_uom, + "conversion_factor": 1, + }, + ) + + return target_doc + + def get_available_materials(work_order) -> dict: data = get_stock_entry_data(work_order) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6f4c910c7f..a2f9978670 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import flt, nowdate, nowtime +from frappe.utils import add_days, flt, nowdate, nowtime from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -1457,6 +1457,138 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(se.items[0].item_name, item.item_name) self.assertEqual(se.items[0].stock_uom, item.stock_uom) + def test_reposting_for_depedent_warehouse(self): + from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + # Inward at WH1 warehouse (Component) + # 1st Repack (Component (WH1) - Subcomponent (WH2)) + # 2nd Repack (Subcomponent (WH2) - FG Item (WH3)) + # Material Transfer of FG Item -> WH 3 -> WH2 -> Wh1 (Two transfer entries) + # Backdated transction which should update valuation rate in repack as well trasfer entries + + for item_code in ["FG Item 1", "Sub Component 1", "Component 1"]: + create_item(item_code) + + for warehouse in ["WH 1", "WH 2", "WH 3"]: + create_warehouse(warehouse) + + make_stock_entry( + item_code="Component 1", + rate=100, + purpose="Material Receipt", + qty=10, + to_warehouse="WH 1 - _TC", + posting_date=add_days(nowdate(), -10), + ) + + repack1 = make_stock_entry( + item_code="Component 1", + purpose="Repack", + do_not_save=True, + qty=10, + from_warehouse="WH 1 - _TC", + posting_date=add_days(nowdate(), -9), + ) + + repack1.append( + "items", + { + "item_code": "Sub Component 1", + "qty": 10, + "t_warehouse": "WH 2 - _TC", + "transfer_qty": 10, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1.0, + }, + ) + + repack1.save() + repack1.submit() + + self.assertEqual(repack1.items[1].basic_rate, 100) + self.assertEqual(repack1.items[1].amount, 1000) + + repack2 = make_stock_entry( + item_code="Sub Component 1", + purpose="Repack", + do_not_save=True, + qty=10, + from_warehouse="WH 2 - _TC", + posting_date=add_days(nowdate(), -8), + ) + + repack2.append( + "items", + { + "item_code": "FG Item 1", + "qty": 10, + "t_warehouse": "WH 3 - _TC", + "transfer_qty": 10, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1.0, + }, + ) + + repack2.save() + repack2.submit() + + self.assertEqual(repack2.items[1].basic_rate, 100) + self.assertEqual(repack2.items[1].amount, 1000) + + transfer1 = make_stock_entry( + item_code="FG Item 1", + purpose="Material Transfer", + qty=10, + from_warehouse="WH 3 - _TC", + to_warehouse="WH 2 - _TC", + posting_date=add_days(nowdate(), -7), + ) + + self.assertEqual(transfer1.items[0].basic_rate, 100) + self.assertEqual(transfer1.items[0].amount, 1000) + + transfer2 = make_stock_entry( + item_code="FG Item 1", + purpose="Material Transfer", + qty=10, + from_warehouse="WH 2 - _TC", + to_warehouse="WH 1 - _TC", + posting_date=add_days(nowdate(), -6), + ) + + self.assertEqual(transfer2.items[0].basic_rate, 100) + self.assertEqual(transfer2.items[0].amount, 1000) + + # Backdated transaction + receipt2 = make_stock_entry( + item_code="Component 1", + rate=200, + purpose="Material Receipt", + qty=10, + to_warehouse="WH 1 - _TC", + posting_date=add_days(nowdate(), -15), + ) + + self.assertEqual(receipt2.items[0].basic_rate, 200) + self.assertEqual(receipt2.items[0].amount, 2000) + + repost_name = frappe.db.get_value( + "Repost Item Valuation", {"voucher_no": receipt2.name, "docstatus": 1}, "name" + ) + + doc = frappe.get_doc("Repost Item Valuation", repost_name) + repost_sl_entries(doc) + + for obj in [repack1, repack2, transfer1, transfer2]: + obj.load_from_db() + + index = 1 if obj.purpose == "Repack" else 0 + self.assertEqual(obj.items[index].basic_rate, 200) + self.assertEqual(obj.items[index].basic_amount, 2000) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index d758c8a0ea..5fe11a2aa7 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -68,6 +68,7 @@ "against_stock_entry", "ste_detail", "po_detail", + "sco_rm_detail", "putaway_rule", "column_break_51", "reference_purchase_receipt", @@ -496,6 +497,15 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "sco_rm_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "SCO Supplied Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { "default": "0", "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index f669e90308..1410da56a3 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -409,61 +409,6 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): lcv.cancel() pr.cancel() - def test_sub_contracted_item_costing(self): - from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom - - company = "_Test Company" - rm_item_code = "_Test Item for Reposting" - subcontracted_item = "_Test Subcontracted Item for Reposting" - - frappe.db.set_value( - "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM" - ) - make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR") - - # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100 - pr = make_purchase_receipt( - company=company, - posting_date="2020-04-10", - warehouse="Stores - _TC", - item_code=rm_item_code, - qty=10, - rate=100, - ) - - # Purchase Receipt for subcontracted item - pr1 = make_purchase_receipt( - company=company, - posting_date="2020-04-20", - warehouse="Finished Goods - _TC", - supplier_warehouse="Stores - _TC", - item_code=subcontracted_item, - qty=10, - rate=20, - is_subcontracted=1, - ) - - self.assertEqual(pr1.items[0].valuation_rate, 120) - - # Update raw material's valuation via LCV, Additional cost = 50 - lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - - pr1.reload() - self.assertEqual(pr1.items[0].valuation_rate, 125) - - # check outgoing_rate for DN after reposting - incoming_rate = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item}, - "incoming_rate", - ) - self.assertEqual(incoming_rate, 125) - - # cleanup data - pr1.cancel() - lcv.cancel() - pr.cancel() - def test_back_dated_entry_not_allowed(self): # Back dated stock transactions are only allowed to stock managers frappe.db.set_value( diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 38ad662b6a..e83182f411 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -238,8 +238,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_sub_contracted_item != 1: - throw(_("Item {0} must be a Sub-contracted 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 ba2d3c1512..b1842e7fc5 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -250,16 +250,11 @@ def repost_future_sle( data.sle_changed = False i += 1 - if doc and i % 2 == 0: + if doc: update_args_in_repost_item_valuation( doc, i, args, distinct_item_warehouses, affected_transactions ) - if doc and args: - update_args_in_repost_item_valuation( - doc, i, args, distinct_item_warehouses, affected_transactions - ) - def validate_item_warehouse(args): for field in ["item_code", "warehouse", "posting_date", "posting_time"]: @@ -501,7 +496,8 @@ class update_entries_after(object): elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: return entries_to_fix else: - return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + self.append_future_sle_for_dependant(dependant_sle, entries_to_fix) + return entries_to_fix def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) @@ -520,14 +516,11 @@ class update_entries_after(object): def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix): self.initialize_previous_data(dependant_sle) - - args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict( - {"item_code": self.item_code, "warehouse": dependant_sle.warehouse} + self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict( + {"sle": dependant_sle} ) - future_sle_for_dependant = list(self.get_sle_after_datetime(args)) - entries_to_fix.extend(future_sle_for_dependant) - return sorted(entries_to_fix, key=lambda k: k["timestamp"]) + self.new_items_found = True def process_sle(self, sle): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -637,6 +630,7 @@ class update_entries_after(object): "Purchase Invoice", "Delivery Note", "Sales Invoice", + "Subcontracting Receipt", ): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"): from erpnext.controllers.sales_and_purchase_return import ( @@ -653,6 +647,8 @@ class update_entries_after(object): else: if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): rate_field = "valuation_rate" + elif sle.voucher_type == "Subcontracting Receipt": + rate_field = "rate" else: rate_field = "incoming_rate" @@ -666,6 +662,8 @@ class update_entries_after(object): else: if sle.voucher_type in ("Delivery Note", "Sales Invoice"): ref_doctype = "Packed Item" + elif sle == "Subcontracting Receipt": + ref_doctype = "Subcontracting Receipt Supplied Item" else: ref_doctype = "Purchase Receipt Item Supplied" @@ -691,6 +689,8 @@ class update_entries_after(object): self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate) elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): self.update_rate_on_purchase_receipt(sle, outgoing_rate) + elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt": + self.update_rate_on_subcontracting_receipt(sle, outgoing_rate) def update_rate_on_stock_entry(self, sle, outgoing_rate): frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) @@ -739,6 +739,14 @@ class update_entries_after(object): for d in doc.items + doc.supplied_items: d.db_update() + def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate): + if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no): + frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate) + else: + frappe.db.set_value( + "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate + ) + def get_serialized_values(self, sle): incoming_rate = flt(sle.incoming_rate) actual_qty = flt(sle.actual_qty) diff --git a/erpnext/subcontracting/__init__.py b/erpnext/subcontracting/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/__init__.py b/erpnext/subcontracting/doctype/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/subcontracting_order/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js new file mode 100644 index 0000000000..dbd337afd4 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -0,0 +1,328 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide('erpnext.buying'); + +frappe.ui.form.on('Subcontracting Order', { + setup: (frm) => { + frm.get_field("items").grid.cannot_add_rows = true; + frm.get_field("items").grid.only_sortable(); + + frm.set_indicator_formatter('item_code', + (doc) => (doc.qty <= doc.received_qty) ? 'green' : 'orange'); + + frm.set_query('supplier_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('purchase_order', () => { + return { + filters: { + docstatus: 1, + is_subcontracted: 1, + is_old_subcontracting_flow: 0 + } + }; + }); + + frm.set_query('set_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + + frm.set_query('expense_account', 'items', () => ({ + query: 'erpnext.controllers.queries.get_expense_account', + filters: { + company: frm.doc.company + } + })); + + frm.set_query('bom', 'items', (doc, cdt, cdn) => { + let d = locals[cdt][cdn]; + return { + filters: { + item: d.item_code, + is_active: 1, + docstatus: 1, + company: frm.doc.company + } + }; + }); + + frm.set_query('set_reserve_warehouse', () => { + return { + filters: { + company: frm.doc.company, + name: ['!=', frm.doc.supplier_warehouse], + is_group: 0 + } + }; + }); + }, + + onload: (frm) => { + if (!frm.doc.transaction_date) { + frm.set_value('transaction_date', frappe.datetime.get_today()); + } + }, + + purchase_order: (frm) => { + frm.set_value('service_items', null); + frm.set_value('items', null); + frm.set_value('supplied_items', null); + + if (frm.doc.purchase_order) { + erpnext.utils.map_current_doc({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order', + source_name: frm.doc.purchase_order, + target_doc: frm, + freeze: true, + freeze_message: __('Mapping Subcontracting Order ...'), + }); + } + }, + + refresh: function (frm) { + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function (frm) { + let sco_rm_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100)) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + sco_rm_details.push(d.name); + } + }); + } + + if (sco_rm_details && sco_rm_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.controllers.subcontracting_controller.get_materials_from_supplier', + freeze: true, + freeze_message: __('Creating Stock Entry'), + 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); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } + } +}); + +erpnext.buying.SubcontractingOrderController = class SubcontractingOrderController { + setup() { + this.frm.custom_make_buttons = { + 'Subcontracting Receipt': 'Subcontracting Receipt', + 'Stock Entry': 'Material to Supplier', + }; + } + + refresh(doc) { + var me = this; + + if (doc.docstatus == 1) { + if (doc.status != 'Completed') { + if (flt(doc.per_received) < 100) { + cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create')); + if (me.has_unsupplied_items()) { + cur_frm.add_custom_button(__('Material to Supplier'), + () => { + me.make_stock_entry(); + }, __('Transfer')); + } + } + cur_frm.page.set_inner_btn_group_as_primary(__('Create')); + } + } + } + + items_add(doc, cdt, cdn) { + if (doc.set_warehouse) { + var row = frappe.get_doc(cdt, cdn); + row.warehouse = doc.set_warehouse; + } + } + + set_warehouse(doc) { + this.set_warehouse_in_children(doc.items, "warehouse", doc.set_warehouse); + } + + set_reserve_warehouse(doc) { + this.set_warehouse_in_children(doc.supplied_items, "reserve_warehouse", doc.set_reserve_warehouse); + } + + set_warehouse_in_children(child_table, warehouse_field, warehouse) { + let transaction_controller = new erpnext.TransactionController(); + transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); + } + + make_stock_entry() { + var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false); + var me = this; + + if (items.length >= 1) { + me.raw_material_data = []; + me.show_dialog = 1; + let title = __('Transfer Material to Supplier'); + let fields = [ + { fieldtype: 'Section Break', label: __('Raw Materials') }, + { + fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), + fields: [ + { + fieldtype: 'Data', + fieldname: 'item_code', + label: __('Item'), + read_only: 1, + in_list_view: 1 + }, + { + fieldtype: 'Data', + fieldname: 'rm_item_code', + label: __('Raw Material'), + read_only: 1, + in_list_view: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'qty', + label: __('Quantity'), + in_list_view: 1 + }, + { + fieldtype: 'Data', + read_only: 1, + fieldname: 'warehouse', + label: __('Reserve Warehouse'), + in_list_view: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'rate', + label: __('Rate'), + hidden: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'amount', + label: __('Amount'), + hidden: 1 + }, + { + fieldtype: 'Link', + read_only: 1, + fieldname: 'uom', + label: __('UOM'), + hidden: 1 + } + ], + data: me.raw_material_data, + get_data: () => me.raw_material_data + } + ]; + + me.dialog = new frappe.ui.Dialog({ + title: title, fields: fields + }); + + if (me.frm.doc['supplied_items']) { + me.frm.doc['supplied_items'].forEach((item) => { + if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { + me.raw_material_data.push({ + 'name': item.name, + 'item_code': item.main_item_code, + 'rm_item_code': item.rm_item_code, + 'item_name': item.rm_item_code, + 'qty': item.required_qty - item.supplied_qty, + 'warehouse': item.reserve_warehouse, + 'rate': item.rate, + 'amount': item.amount, + 'stock_uom': item.stock_uom + }); + me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); + } + }); + } + + me.dialog.get_field('sub_con_rm_items').check_all_rows(); + + me.dialog.show(); + this.dialog.set_primary_action(__('Transfer'), () => { + me.values = me.dialog.get_values(); + if (me.values) { + me.values.sub_con_rm_items.map((row, i) => { + if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { + let row_id = i + 1; + frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id])); + } + }); + me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()); + me.dialog.hide(); + } + }); + } + + me.dialog.get_close_btn().on('click', () => { + me.dialog.hide(); + }); + } + + has_unsupplied_items() { + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + } + + make_subcontracting_receipt() { + frappe.model.open_mapped_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', + frm: cur_frm, + freeze_message: __('Creating Subcontracting Receipt ...') + }); + } + + make_rm_stock_entry(rm_items) { + frappe.call({ + method: 'erpnext.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: (r) => { + var doclist = frappe.model.sync(r.message); + frappe.set_route('Form', doclist[0].doctype, doclist[0].name); + } + }); + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.buying.SubcontractingOrderController({ frm: cur_frm })); \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json new file mode 100644 index 0000000000..c6e76c76d7 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -0,0 +1,485 @@ +{ + "actions": [], + "allow_auto_repeat": 1, + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2022-04-01 22:39:17.662819", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "purchase_order", + "supplier", + "supplier_name", + "supplier_warehouse", + "column_break_7", + "company", + "transaction_date", + "schedule_date", + "amended_from", + "address_and_contact_section", + "supplier_address", + "address_display", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "column_break_19", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "section_break_24", + "column_break_25", + "set_warehouse", + "items", + "section_break_32", + "total_qty", + "column_break_29", + "total", + "service_items_section", + "service_items", + "raw_materials_supplied_section", + "set_reserve_warehouse", + "supplied_items", + "additional_costs_section", + "distribute_additional_costs_based_on", + "additional_costs", + "total_additional_costs", + "order_status_section", + "status", + "column_break_39", + "per_received", + "printing_settings_section", + "select_print_heading", + "column_break_43", + "letter_head" + ], + "fields": [ + { + "allow_on_submit": 1, + "default": "{supplier_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "SC-ORD-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "purchase_order", + "fieldtype": "Link", + "label": "Subcontracting Purchase Order", + "options": "Purchase Order", + "reqd": 1 + }, + { + "bold": 1, + "fieldname": "supplier", + "fieldtype": "Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Supplier", + "options": "Supplier", + "print_hide": 1, + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fetch_from": "supplier.supplier_name", + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Supplier Name", + "read_only": 1, + "reqd": 1 + }, + { + "depends_on": "supplier", + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "label": "Supplier Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "default": "Today", + "fetch_from": "purchase_order.transaction_date", + "fetch_if_empty": 1, + "fieldname": "transaction_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1, + "search_index": 1 + }, + { + "allow_on_submit": 1, + "fetch_from": "purchase_order.schedule_date", + "fetch_if_empty": 1, + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Subcontracting Order", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "address_and_contact_section", + "fieldtype": "Section Break", + "label": "Address and Contact" + }, + { + "fetch_from": "supplier.supplier_primary_address", + "fetch_if_empty": 1, + "fieldname": "supplier_address", + "fieldtype": "Link", + "label": "Supplier Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Small Text", + "label": "Supplier Address Details", + "read_only": 1 + }, + { + "fetch_from": "supplier.supplier_primary_contact", + "fetch_if_empty": 1, + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Supplier Contact", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "contact_display", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Contact Name", + "read_only": 1 + }, + { + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "label": "Contact Mobile No", + "read_only": 1 + }, + { + "fieldname": "contact_email", + "fieldtype": "Small Text", + "label": "Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Company Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Small Text", + "label": "Shipping Address Details", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "billing_address", + "fieldtype": "Link", + "label": "Company Billing Address", + "options": "Address" + }, + { + "fieldname": "billing_address_display", + "fieldtype": "Small Text", + "label": "Billing Address Details", + "read_only": 1 + }, + { + "fieldname": "section_break_24", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "depends_on": "purchase_order", + "description": "Sets 'Warehouse' in each row of the Items table.", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "label": "Set Target Warehouse", + "options": "Warehouse", + "print_hide": 1 + }, + { + "allow_bulk_edit": 1, + "depends_on": "purchase_order", + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Subcontracting Order Item", + "reqd": 1 + }, + { + "fieldname": "section_break_32", + "fieldtype": "Section Break" + }, + { + "depends_on": "purchase_order", + "fieldname": "total_qty", + "fieldtype": "Float", + "label": "Total Quantity", + "read_only": 1 + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "depends_on": "purchase_order", + "fieldname": "total", + "fieldtype": "Currency", + "label": "Total", + "options": "currency", + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "purchase_order", + "fieldname": "service_items_section", + "fieldtype": "Section Break", + "label": "Service Items" + }, + { + "fieldname": "service_items", + "fieldtype": "Table", + "label": "Service Items", + "options": "Subcontracting Order Service Item", + "read_only": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "supplied_items", + "depends_on": "supplied_items", + "fieldname": "raw_materials_supplied_section", + "fieldtype": "Section Break", + "label": "Raw Materials Supplied" + }, + { + "depends_on": "supplied_items", + "description": "Sets 'Reserve Warehouse' in each row of the Supplied Items table.", + "fieldname": "set_reserve_warehouse", + "fieldtype": "Link", + "label": "Set Reserve Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "supplied_items", + "fieldtype": "Table", + "label": "Supplied Items", + "no_copy": 1, + "options": "Subcontracting Order Supplied Item", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "total_additional_costs", + "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)", + "fieldname": "additional_costs_section", + "fieldtype": "Section Break", + "label": "Additional Costs" + }, + { + "fieldname": "additional_costs", + "fieldtype": "Table", + "label": "Additional Costs", + "options": "Landed Cost Taxes and Charges" + }, + { + "fieldname": "total_additional_costs", + "fieldtype": "Currency", + "label": "Total Additional Costs", + "print_hide_if_no_value": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "order_status_section", + "fieldtype": "Section Break", + "label": "Order Status" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled", + "print_hide": 1, + "read_only": 1, + "reqd": 1, + "search_index": 1 + }, + { + "fieldname": "column_break_39", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "per_received", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "% Received", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "printing_settings_section", + "fieldtype": "Section Break", + "label": "Printing Settings", + "print_hide": 1, + "print_width": "50%", + "width": "50%" + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "column_break_43", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "default": "Qty", + "fieldname": "distribute_additional_costs_based_on", + "fieldtype": "Select", + "label": "Distribute Additional Costs Based On ", + "options": "Qty\nAmount" + } + ], + "icon": "fa fa-file-text", + "is_submittable": 1, + "links": [], + "modified": "2022-04-11 21:02:44.097841", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "read": 1, + "report": 1, + "role": "Stock User" + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "role": "Purchase Manager", + "write": 1 + } + ], + "search_fields": "status, transaction_date, supplier", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "timeline_field": "supplier", + "title_field": "supplier_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py new file mode 100644 index 0000000000..71cdc94a3a --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -0,0 +1,246 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt + +from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created +from erpnext.controllers.subcontracting_controller import SubcontractingController +from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.utils import get_bin + + +class SubcontractingOrder(SubcontractingController): + def before_validate(self): + super(SubcontractingOrder, self).before_validate() + + def validate(self): + super(SubcontractingOrder, self).validate() + self.validate_purchase_order_for_subcontracting() + self.validate_items() + self.validate_service_items() + self.validate_supplied_items() + self.set_missing_values() + self.reset_default_field_value("set_warehouse", "items", "warehouse") + + def on_submit(self): + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() + self.update_status() + + def on_cancel(self): + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() + self.update_status() + + def validate_purchase_order_for_subcontracting(self): + if self.purchase_order: + if is_subcontracting_order_created(self.purchase_order): + frappe.throw( + _( + "Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one." + ) + ) + + po = frappe.get_doc("Purchase Order", self.purchase_order) + + if not po.is_subcontracted: + frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting.")) + + 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)) + + if po.per_received == 100: + msg = f"Cannot create more Subcontracting Orders against the Purchase Order {po.name}." + frappe.throw(_(msg)) + else: + self.service_items = self.items = self.supplied_items = None + frappe.throw(_("Please select a Subcontracting Purchase Order.")) + + def validate_service_items(self): + for item in self.service_items: + if frappe.get_value("Item", item.item_code, "is_stock_item"): + msg = f"Service Item {item.item_name} must be a non-stock item." + frappe.throw(_(msg)) + + def validate_supplied_items(self): + if self.supplier_warehouse: + for item in self.supplied_items: + if self.supplier_warehouse == item.reserve_warehouse: + msg = f"Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {item.main_item_code}." + frappe.throw(_(msg)) + + def set_missing_values(self): + self.set_missing_values_in_additional_costs() + self.set_missing_values_in_service_items() + self.set_missing_values_in_supplied_items() + self.set_missing_values_in_items() + + def set_missing_values_in_additional_costs(self): + if self.get("additional_costs"): + self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) + + if self.total_additional_costs: + if self.distribute_additional_costs_based_on == "Amount": + total_amt = sum(flt(item.amount) for item in self.get("items")) + for item in self.items: + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty + else: + total_qty = sum(flt(item.qty) for item in self.get("items")) + additional_cost_per_qty = self.total_additional_costs / total_qty + for item in self.items: + item.additional_cost_per_qty = additional_cost_per_qty + else: + self.total_additional_costs = 0 + + def set_missing_values_in_service_items(self): + for idx, item in enumerate(self.get("service_items")): + self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty + + def set_missing_values_in_supplied_items(self): + for item in self.get("items"): + bom = frappe.get_doc("BOM", item.bom) + rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) + item.rm_cost_per_qty = rm_cost / flt(bom.quantity) + + def set_missing_values_in_items(self): + total_qty = total = 0 + for item in self.items: + item.rate = ( + item.rm_cost_per_qty + item.service_cost_per_qty + (item.additional_cost_per_qty or 0) + ) + item.amount = item.qty * item.rate + total_qty += flt(item.qty) + total += flt(item.amount) + else: + self.total_qty = total_qty + self.total = total + + def update_ordered_qty_for_subcontracting(self, sco_item_rows=None): + item_wh_list = [] + for item in self.get("items"): + if ( + (not sco_item_rows or item.name in sco_item_rows) + and [item.item_code, item.warehouse] not in item_wh_list + and frappe.get_cached_value("Item", item.item_code, "is_stock_item") + and item.warehouse + ): + item_wh_list.append([item.item_code, item.warehouse]) + for item_code, warehouse in item_wh_list: + update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) + + def update_reserved_qty_for_subcontracting(self): + for item in self.supplied_items: + if item.rm_item_code: + stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() + + def populate_items_table(self): + items = [] + + for si in self.service_items: + if si.fg_item: + item = frappe.get_doc("Item", si.fg_item) + bom = frappe.db.get_value("BOM", {"item": item.item_code, "is_active": 1, "is_default": 1}) + + items.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "schedule_date": self.schedule_date, + "description": item.description, + "qty": si.fg_item_qty, + "stock_uom": item.stock_uom, + "bom": bom, + }, + ) + else: + frappe.throw( + _("Please select Finished Good Item for Service Item {0}").format( + si.item_name or si.item_code + ) + ) + else: + for item in items: + self.append("items", item) + else: + self.set_missing_values() + + def update_status(self, status=None, update_modified=False): + if self.docstatus >= 1 and not status: + if self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif self.per_received >= 100: + status = "Completed" + elif self.per_received > 0 and self.per_received < 100: + status = "Partially Received" + else: + total_required_qty = total_supplied_qty = 0 + for item in self.supplied_items: + total_required_qty += item.required_qty + total_supplied_qty += item.supplied_qty or 0 + if total_supplied_qty: + status = "Partial Material Transferred" + if total_supplied_qty >= total_required_qty: + status = "Material Transferred" + else: + status = "Open" + elif self.docstatus == 2: + status = "Cancelled" + + frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified) + + +@frappe.whitelist() +def make_subcontracting_receipt(source_name, target_doc=None): + return get_mapped_subcontracting_receipt(source_name, target_doc) + + +def get_mapped_subcontracting_receipt(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.received_qty) + target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) + + target_doc = get_mapped_doc( + "Subcontracting Order", + source_name, + { + "Subcontracting Order": { + "doctype": "Subcontracting Receipt", + "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "validation": { + "docstatus": ["=", 1], + }, + }, + "Subcontracting Order Item": { + "doctype": "Subcontracting Receipt Item", + "field_map": { + "name": "subcontracting_order_item", + "parent": "subcontracting_order", + "bom": "bom", + }, + "postprocess": update_item, + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + }, + }, + target_doc, + ) + + return target_doc + + +@frappe.whitelist() +def update_subcontracting_order_status(sco): + if isinstance(sco, str): + sco = frappe.get_doc("Subcontracting Order", sco) + + sco.update_status() diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py new file mode 100644 index 0000000000..f17d8cd961 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py @@ -0,0 +1,8 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "subcontracting_order", + "transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}], + } diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js new file mode 100644 index 0000000000..650419cf74 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -0,0 +1,16 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.listview_settings['Subcontracting Order'] = { + get_indicator: function (doc) { + const status_colors = { + "Draft": "grey", + "Open": "orange", + "Partially Received": "yellow", + "Completed": "green", + "Partial Material Transferred": "purple", + "Material Transferred": "blue", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py new file mode 100644 index 0000000000..94bb38e980 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -0,0 +1,536 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import copy + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order +from erpnext.controllers.subcontracting_controller import make_rm_stock_entry +from erpnext.controllers.tests.test_subcontracting_controller import ( + get_rm_items, + get_subcontracting_order, + make_bom_for_subcontracted_items, + make_raw_materials, + make_service_items, + make_stock_in_entry, + make_stock_transfer_entry, + make_subcontracted_item, + make_subcontracted_items, + set_backflush_based_on, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + make_subcontracting_receipt, +) + + +class TestSubcontractingOrder(FrappeTestCase): + def setUp(self): + make_subcontracted_items() + make_raw_materials() + make_service_items() + make_bom_for_subcontracted_items() + + def test_populate_items_table(self): + sco = get_subcontracting_order() + sco.items = None + sco.populate_items_table() + self.assertEqual(len(sco.service_items), len(sco.items)) + + def test_set_missing_values(self): + sco = get_subcontracting_order() + before = {sco.total_qty, sco.total, sco.total_additional_costs} + sco.total_qty = sco.total = sco.total_additional_costs = 0 + sco.set_missing_values() + after = {sco.total_qty, sco.total, sco.total_additional_costs} + self.assertSetEqual(before, after) + + def test_update_status(self): + # Draft + sco = get_subcontracting_order(do_not_submit=1) + self.assertEqual(sco.status, "Draft") + + # Open + sco.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Open") + + # Partial Material Transferred + rm_items = get_rm_items(sco.supplied_items) + rm_items[0]["qty"] -= 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + sco.load_from_db() + self.assertEqual(sco.status, "Partial Material Transferred") + + # Material Transferred + rm_items[0]["qty"] = 1 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + sco.load_from_db() + self.assertEqual(sco.status, "Material Transferred") + + # Partially Received + scr = make_subcontracting_receipt(sco.name) + scr.items[0].qty -= 1 + scr.save() + scr.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Partially Received") + + # Completed + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + sco.load_from_db() + self.assertEqual(sco.status, "Completed") + + # Partially Received (scr cancelled) + scr.load_from_db() + scr.cancel() + sco.load_from_db() + self.assertEqual(sco.status, "Partially Received") + + def test_make_rm_stock_entry(self): + sco = get_subcontracting_order() + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + self.assertEqual(len(ste.items), len(rm_items)) + + def test_make_rm_stock_entry_for_serial_items(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 2", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA2", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 5", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA5", + "fg_item_qty": 6, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + self.assertEqual(len(ste.items), len(rm_items)) + + def test_make_rm_stock_entry_for_batch_items(self): + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 4", + "qty": 5, + "rate": 100, + "fg_item": "Subcontracted Item SA4", + "fg_item_qty": 5, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 6", + "qty": 6, + "rate": 100, + "fg_item": "Subcontracted Item SA6", + "fg_item_qty": 6, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items) + ste = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + self.assertEqual(len(ste.items), len(rm_items)) + + def test_update_reserved_qty_for_subcontracting(self): + # Make stock available for raw materials + make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code="_Test Item Home Desktop 100", + qty=30, + basic_rate=100, + ) + + bin1 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], + as_dict=1, + ) + + # Create SCO + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 10, + "rate": 100, + "fg_item": "_Test FG Item", + "fg_item_qty": 10, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + + bin2 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], + as_dict=1, + ) + + self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) + self.assertNotEqual(bin1.modified, bin2.modified) + + # Create stock transfer + rm_items = [ + { + "item_code": "_Test FG Item", + "rm_item_code": "_Test Item", + "item_name": "_Test Item", + "qty": 6, + "warehouse": "_Test Warehouse - _TC", + "rate": 100, + "amount": 600, + "stock_uom": "Nos", + } + ] + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.to_warehouse = "_Test Warehouse 1 - _TC" + ste.save() + ste.submit() + + bin3 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + make_stock_entry( + target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse 1 - _TC", + item_code="_Test Item Home Desktop 100", + qty=40, + basic_rate=100, + ) + + # Make SCR against the SCO + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + + bin4 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + # Cancel SCR + scr.reload() + scr.cancel() + bin5 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6) + + # Cancel Stock Entry + ste.cancel() + bin6 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) + + # Cancel PO + sco.reload() + sco.cancel() + bin7 = frappe.db.get_value( + "Bin", + filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, + fieldname="reserved_qty_for_sub_contract", + as_dict=1, + ) + + self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) + + def test_exploded_items(self): + item_code = "_Test Subcontracted FG Item 11" + make_subcontracted_item(item_code=item_code) + + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": 1, + }, + ] + + sco1 = get_subcontracting_order(service_items=service_items, include_exploded_items=1) + item_name = frappe.db.get_value("BOM", {"item": item_code}, "name") + bom = frappe.get_doc("BOM", item_name) + exploded_items = sorted([item.item_code for item in bom.exploded_items]) + supplied_items = sorted([item.rm_item_code for item in sco1.supplied_items]) + self.assertEqual(exploded_items, supplied_items) + + sco2 = get_subcontracting_order(service_items=service_items, include_exploded_items=0) + supplied_items1 = sorted([item.rm_item_code for item in sco2.supplied_items]) + bom_items = sorted([item.item_code for item in bom.items]) + self.assertEqual(supplied_items1, bom_items) + + def test_backflush_based_on_stock_entry(self): + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code=item_code) + make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1}) + + set_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 5 + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": order_qty, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": order_qty, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + + make_stock_entry( + target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100 + ) + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 1", + qty=10, + basic_rate=100, + ) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 1", + "item_name": "_Test Item", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 20, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "Test Extra Item 1", + "item_name": "Test Extra Item 1", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + { + "item_code": item_code, + "rm_item_code": "Test Extra Item 2", + "stock_uom": "Nos", + "qty": 10, + "warehouse": "_Test Warehouse - _TC", + "item_name": "Test Extra Item 2", + }, + ] + + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.submit() + + scr = make_subcontracting_receipt(sco.name) + received_qty = 2 + + # partial receipt + scr.get("items")[0].qty = received_qty + scr.save() + scr.submit() + + transferred_items = sorted( + [item.item_code for item in ste.get("items") if ste.subcontracting_order == sco.name] + ) + issued_items = sorted([item.rm_item_code for item in scr.get("supplied_items")]) + + self.assertEqual(transferred_items, issued_items) + self.assertEqual(scr.get_supplied_items_cost(scr.get("items")[0].name), 2000) + + transferred_rm_map = frappe._dict() + for item in rm_items: + transferred_rm_map[item.get("rm_item_code")] = item + + set_backflush_based_on("BOM") + + def test_supplied_qty(self): + item_code = "_Test Subcontracted FG Item 5" + make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1}) + + make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"]) + + set_backflush_based_on("Material Transferred for Subcontract") + + order_qty = 250 + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": order_qty, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": order_qty, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": order_qty, + "rate": 100, + "fg_item": item_code, + "fg_item_qty": order_qty, + }, + ] + + sco = get_subcontracting_order(service_items=service_items) + + # Material receipt entry for the raw materials which will be send to supplier + make_stock_entry( + target="_Test Warehouse - _TC", + item_code="Sub Contracted Raw Material 4", + qty=500, + basic_rate=100, + ) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 4", + "item_name": "_Test Item", + "qty": 250, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + "name": sco.supplied_items[0].name, + }, + { + "item_code": item_code, + "rm_item_code": "Sub Contracted Raw Material 4", + "item_name": "_Test Item", + "qty": 250, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + }, + ] + + # Raw Materials transfer entry from stores to supplier's warehouse + ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items)) + ste.submit() + + # Test sco_rm_detail field has value or not + for item_row in ste.items: + self.assertEqual(item_row.sco_rm_detail, sco.supplied_items[item_row.idx - 1].name) + + sco.load_from_db() + for row in sco.supplied_items: + # Valid that whether transferred quantity is matching with supplied qty or not in the subcontracting order + self.assertEqual(row.supplied_qty, 250.0) + + set_backflush_based_on("BOM") + + +def create_subcontracting_order(**args): + args = frappe._dict(args) + sco = get_mapped_subcontracting_order(source_name=args.po_name) + + for item in sco.items: + item.include_exploded_items = args.get("include_exploded_items", 1) + + if args.get("warehouse"): + for item in sco.items: + item.warehouse = args.warehouse + else: + warehouse = frappe.get_value("Purchase Order", args.po_name, "set_warehouse") + if warehouse: + for item in sco.items: + item.warehouse = warehouse + else: + po = frappe.get_doc("Purchase Order", args.po_name) + warehouses = [] + for item in po.items: + warehouses.append(item.warehouse) + else: + for idx, val in enumerate(sco.items): + val.warehouse = warehouses[idx] + + if not args.do_not_save: + sco.insert() + if not args.do_not_submit: + sco.submit() + + return sco diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json new file mode 100644 index 0000000000..291f47a634 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json @@ -0,0 +1,326 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:26:31.475015", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "bom", + "include_exploded_items", + "column_break_3", + "schedule_date", + "expected_delivery_date", + "description_section", + "description", + "column_break_8", + "image", + "image_view", + "quantity_and_rate_section", + "qty", + "received_qty", + "returned_qty", + "column_break_13", + "stock_uom", + "conversion_factor", + "section_break_16", + "rate", + "amount", + "column_break_19", + "rm_cost_per_qty", + "service_cost_per_qty", + "additional_cost_per_qty", + "warehouse_section", + "warehouse", + "accounting_details_section", + "expense_account", + "manufacture_section", + "manufacturer", + "manufacturer_part_no", + "section_break_34", + "page_break" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1, + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "columns": 2, + "fieldname": "schedule_date", + "fieldtype": "Date", + "label": "Required By", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "bold": 1, + "fieldname": "expected_delivery_date", + "fieldtype": "Date", + "label": "Expected Delivery Date", + "search_index": 1 + }, + { + "collapsible": 1, + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, + "fieldname": "description", + "fieldtype": "Text Editor", + "label": "Description", + "print_width": "300px", + "reqd": 1, + "width": "300px" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 1, + "label": "Image" + }, + { + "fieldname": "image_view", + "fieldtype": "Image", + "label": "Image View", + "options": "image", + "print_hide": 1 + }, + { + "fieldname": "quantity_and_rate_section", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "bold": 1, + "columns": 1, + "default": "1", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "read_only": 1, + "reqd": 1, + "width": "60px" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break", + "print_hide": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "print_width": "100px", + "read_only": 1, + "reqd": 1, + "width": "100px" + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_19", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "warehouse_section", + "fieldtype": "Section Break", + "label": "Warehouse Details" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse", + "print_hide": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_details_section", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "label": "Expense Account", + "options": "Account", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "manufacture_section", + "fieldtype": "Section Break", + "label": "Manufacture" + }, + { + "fieldname": "manufacturer", + "fieldtype": "Link", + "label": "Manufacturer", + "options": "Manufacturer" + }, + { + "fieldname": "manufacturer_part_no", + "fieldtype": "Data", + "label": "Manufacturer Part Number" + }, + { + "depends_on": "item_code", + "fetch_from": "item_code.default_bom", + "fieldname": "bom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "BOM", + "options": "BOM", + "print_hide": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "include_exploded_items", + "fieldtype": "Check", + "label": "Include Exploded Items", + "print_hide": 1 + }, + { + "fieldname": "service_cost_per_qty", + "fieldtype": "Currency", + "label": "Service Cost Per Qty", + "read_only": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "additional_cost_per_qty", + "fieldtype": "Currency", + "label": "Additional Cost Per Qty", + "read_only": 1 + }, + { + "fieldname": "rm_cost_per_qty", + "fieldtype": "Currency", + "label": "Raw Material Cost Per Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "page_break", + "fieldtype": "Check", + "label": "Page Break", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "section_break_34", + "fieldtype": "Section Break" + }, + { + "depends_on": "received_qty", + "fieldname": "received_qty", + "fieldtype": "Float", + "label": "Received Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "returned_qty", + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-04-11 21:28:06.585338", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py new file mode 100644 index 0000000000..174f5b212c --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingOrderItem(Document): + pass diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json new file mode 100644 index 0000000000..f213313ef6 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json @@ -0,0 +1,131 @@ +{ + "actions": [], + "autoname": "hash", + "creation": "2022-04-01 19:23:05.728354", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "column_break_2", + "item_name", + "section_break_4", + "qty", + "column_break_6", + "rate", + "column_break_8", + "amount", + "section_break_10", + "fg_item", + "column_break_12", + "fg_item_qty" + ], + "fields": [ + { + "bold": 1, + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Item Name", + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity", + "print_width": "60px", + "reqd": 1, + "width": "60px" + }, + { + "bold": 1, + "columns": 2, + "fetch_from": "item_code.standard_rate", + "fetch_if_empty": 1, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "currency", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "currency", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "fg_item", + "fieldtype": "Link", + "label": "Finished Good Item", + "options": "Item", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "fg_item_qty", + "fieldtype": "Float", + "label": "Finished Good Item Quantity", + "reqd": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + } + ], + "istable": 1, + "links": [], + "modified": "2022-04-07 11:43:43.094867", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Service Item", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "item_name", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py new file mode 100644 index 0000000000..ad6289d923 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingOrderServiceItem(Document): + pass diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json new file mode 100644 index 0000000000..a206a21ca6 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json @@ -0,0 +1,178 @@ +{ + "actions": [], + "creation": "2022-04-01 19:29:30.923800", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "main_item_code", + "rm_item_code", + "column_break_3", + "stock_uom", + "conversion_factor", + "reserve_warehouse", + "column_break_6", + "bom_detail_no", + "reference_name", + "section_break_9", + "rate", + "column_break_11", + "amount", + "section_break_13", + "required_qty", + "supplied_qty", + "column_break_16", + "consumed_qty", + "returned_qty", + "total_supplied_qty" + ], + "fields": [ + { + "columns": 2, + "fieldname": "main_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "rm_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Raw Material Item Code", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock Uom", + "options": "UOM", + "read_only": 1 + }, + { + "default": "1", + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "reserve_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reserve Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "bom_detail_no", + "fieldtype": "Data", + "label": "BOM Detail No", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Data", + "label": "Reference Name", + "read_only": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "columns": 2, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "read_only": 1 + }, + { + "fieldname": "supplied_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "consumed_qty", + "fieldtype": "Float", + "label": "Consumed Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "returned_qty", + "fieldtype": "Float", + "label": "Returned Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1, + "hidden": 1 + }, + { + "fieldname": "total_supplied_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Total Supplied Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } + ], + "hide_toolbar": 1, + "istable": 1, + "links": [], + "modified": "2022-04-07 12:58:28.208847", + "modified_by": "Administrator", + "module": "Subcontracting", + "name": "Subcontracting Order Supplied Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py new file mode 100644 index 0000000000..5619e3b79a --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SubcontractingOrderSuppliedItem(Document): + pass diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js new file mode 100644 index 0000000000..b2506cd143 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -0,0 +1,157 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide('erpnext.buying'); + +frappe.ui.form.on('Subcontracting Receipt', { + setup: (frm) => { + frm.get_field('supplied_items').grid.cannot_add_rows = true; + frm.get_field('supplied_items').grid.only_sortable(); + + frm.set_query('set_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('rejected_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('supplier_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + + frm.set_query('rejected_warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + }, + + refresh: (frm) => { + if (frm.doc.docstatus > 0) { + frm.add_custom_button(__("Stock Ledger"), function () { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + company: frm.doc.company, + show_cancelled_entries: frm.doc.docstatus === 2 + }; + frappe.set_route("query-report", "Stock Ledger"); + }, __("View")); + + frm.add_custom_button(__('Accounting Ledger'), function () { + frappe.route_options = { + voucher_no: frm.doc.name, + from_date: frm.doc.posting_date, + to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), + company: frm.doc.company, + group_by: "Group by Voucher (Consolidated)", + show_cancelled_entries: frm.doc.docstatus === 2 + }; + frappe.set_route("query-report", "General Ledger"); + }, __("View")); + } + + if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) { + frm.add_custom_button(__('Subcontract Return'), function () { + frappe.model.open_mapped_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return', + frm: frm + }); + }, __('Create')); + frm.page.set_inner_btn_group_as_primary(__('Create')); + } + + if (frm.doc.docstatus == 0) { + frm.add_custom_button(__('Subcontracting Order'), function () { + if (!frm.doc.supplier) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Supplier") + }); + } + + erpnext.utils.map_current_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', + source_doctype: "Subcontracting Order", + target: frm, + setters: { + supplier: frm.doc.supplier, + }, + get_query_filters: { + docstatus: 1, + per_received: ["<", 100], + company: frm.doc.company + } + }); + }, __("Get Items From")); + } + }, + + set_warehouse: (frm) => { + set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse); + }, + + rejected_warehouse: (frm) => { + set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse); + }, +}); + +frappe.ui.form.on('Subcontracting Receipt Item', { + item_code(frm) { + set_missing_values(frm); + }, + + qty(frm) { + set_missing_values(frm); + }, + + rate(frm) { + set_missing_values(frm); + }, +}); + +frappe.ui.form.on('Subcontracting Receipt Supplied Item', { + consumed_qty(frm) { + set_missing_values(frm); + }, +}); + +let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => { + let transaction_controller = new erpnext.TransactionController(); + transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); +}; + +let set_missing_values = (frm) => { + frappe.call({ + doc: frm.doc, + method: 'set_missing_values', + callback: (r) => { + if (!r.exc) frm.refresh(); + }, + }); +}; \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json new file mode 100644 index 0000000000..e9638144a7 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json @@ -0,0 +1,645 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2022-04-18 11:20:44.226738", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "naming_series", + "supplier", + "supplier_name", + "column_break1", + "company", + "posting_date", + "posting_time", + "is_return", + "return_against", + "section_addresses", + "supplier_address", + "contact_person", + "address_display", + "contact_display", + "contact_mobile", + "contact_email", + "col_break_address", + "shipping_address", + "shipping_address_display", + "billing_address", + "billing_address_display", + "sec_warehouse", + "set_warehouse", + "rejected_warehouse", + "col_break_warehouse", + "supplier_warehouse", + "items_section", + "items", + "section_break0", + "total_qty", + "column_break_27", + "total", + "raw_material_details", + "get_current_stock", + "supplied_items", + "section_break_46", + "in_words", + "bill_no", + "bill_date", + "accounting_details_section", + "provisional_expense_account", + "more_info", + "status", + "column_break_39", + "per_returned", + "section_break_47", + "amended_from", + "range", + "column_break4", + "represents_company", + "subscription_detail", + "auto_repeat", + "printing_settings", + "letter_head", + "language", + "instructions", + "column_break_97", + "select_print_heading", + "other_details", + "remarks", + "transporter_info", + "transporter_name", + "column_break5", + "lr_no", + "lr_date" + ], + "fields": [ + { + "allow_on_submit": 1, + "default": "{supplier_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "MAT-SCR-.YYYY.-\nMAT-SCR-RET-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "bold": 1, + "fieldname": "supplier", + "fieldtype": "Link", + "in_global_search": 1, + "label": "Supplier", + "options": "Supplier", + "print_hide": 1, + "print_width": "150px", + "reqd": 1, + "search_index": 1, + "width": "150px" + }, + { + "bold": 1, + "depends_on": "supplier", + "fetch_from": "supplier.supplier_name", + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Supplier Name", + "read_only": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "no_copy": 1, + "print_width": "100px", + "reqd": 1, + "search_index": 1, + "width": "100px" + }, + { + "description": "Time at which materials were received", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "print_hide": 1, + "print_width": "100px", + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "print_hide": 1, + "print_width": "150px", + "remember_last_selected_value": 1, + "reqd": 1, + "width": "150px" + }, + { + "collapsible": 1, + "fieldname": "section_addresses", + "fieldtype": "Section Break", + "label": "Address and Contact" + }, + { + "fieldname": "supplier_address", + "fieldtype": "Link", + "label": "Select Supplier Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Small Text", + "label": "Address", + "read_only": 1 + }, + { + "fieldname": "contact_display", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Contact", + "read_only": 1 + }, + { + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "label": "Mobile No", + "read_only": 1 + }, + { + "fieldname": "contact_email", + "fieldtype": "Small Text", + "label": "Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "col_break_address", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_address", + "fieldtype": "Link", + "label": "Select Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "shipping_address_display", + "fieldtype": "Small Text", + "label": "Shipping Address", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "sec_warehouse", + "fieldtype": "Section Break" + }, + { + "description": "Sets 'Accepted Warehouse' in each row of the Items table.", + "fieldname": "set_warehouse", + "fieldtype": "Link", + "label": "Accepted Warehouse", + "options": "Warehouse", + "print_hide": 1 + }, + { + "description": "Sets 'Rejected Warehouse' in each row of the Items table.", + "fieldname": "rejected_warehouse", + "fieldtype": "Link", + "label": "Rejected Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1 + }, + { + "fieldname": "col_break_warehouse", + "fieldtype": "Column Break" + }, + { + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "label": "Supplier Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "options": "fa fa-shopping-cart" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Subcontracting Receipt Item", + "reqd": 1 + }, + { + "depends_on": "supplied_items", + "fieldname": "get_current_stock", + "fieldtype": "Button", + "label": "Get Current Stock", + "options": "get_current_stock", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "supplied_items", + "depends_on": "supplied_items", + "fieldname": "raw_material_details", + "fieldtype": "Section Break", + "label": "Raw Materials Consumed", + "options": "fa fa-table", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "supplied_items", + "fieldtype": "Table", + "label": "Consumed Items", + "no_copy": 1, + "options": "Subcontracting Receipt Supplied Item", + "print_hide": 1 + }, + { + "fieldname": "section_break0", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_qty", + "fieldtype": "Float", + "label": "Total Quantity", + "read_only": 1 + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "fieldname": "total", + "fieldtype": "Currency", + "label": "Total", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "section_break_46", + "fieldtype": "Section Break" + }, + { + "fieldname": "in_words", + "fieldtype": "Data", + "label": "In Words", + "length": 240, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "bill_no", + "fieldtype": "Data", + "hidden": 1, + "label": "Bill No", + "print_hide": 1 + }, + { + "fieldname": "bill_date", + "fieldtype": "Date", + "hidden": 1, + "label": "Bill Date", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Information", + "options": "fa fa-file-text" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "reqd": 1, + "search_index": 1, + "width": "150px" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "options": "Subcontracting Receipt", + "print_hide": 1, + "print_width": "150px", + "read_only": 1, + "width": "150px" + }, + { + "fieldname": "range", + "fieldtype": "Data", + "hidden": 1, + "label": "Range", + "print_hide": 1 + }, + { + "fieldname": "column_break4", + "fieldtype": "Column Break", + "print_hide": 1, + "print_width": "50%", + "width": "50%" + }, + { + "fieldname": "subscription_detail", + "fieldtype": "Section Break", + "label": "Auto Repeat Detail" + }, + { + "fieldname": "auto_repeat", + "fieldtype": "Link", + "label": "Auto Repeat", + "no_copy": 1, + "options": "Auto Repeat", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "printing_settings", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "language", + "fieldtype": "Data", + "label": "Print Language", + "read_only": 1 + }, + { + "fieldname": "column_break_97", + "fieldtype": "Column Break" + }, + { + "fieldname": "other_details", + "fieldtype": "HTML", + "hidden": 1, + "label": "Other Details", + "options": "