From 249726b845522f9b7858ca2ed1845e284c7a91f9 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 22 Apr 2022 17:09:19 +0530 Subject: [PATCH] feat: New DocType "Subcontracting Order" --- .../controllers/subcontracting_controller.py | 11 + .../stock/doctype/stock_entry/stock_entry.py | 10 + .../doctype/subcontracting_order/__init__.py | 0 .../subcontracting_order.js | 322 ++++++++++++ .../subcontracting_order.json | 485 ++++++++++++++++++ .../subcontracting_order.py | 372 ++++++++++++++ .../subcontracting_order_dashboard.py | 8 + .../subcontracting_order_list.js | 16 + .../test_subcontracting_order.py | 8 + 9 files changed, 1232 insertions(+) create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/__init__.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js create mode 100644 erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index b393737740..4e0d91147e 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -627,6 +627,17 @@ class SubcontractingController(StockController): return supplied_items_cost + def set_subcontracting_order_status(self): + if self.doctype == "Subcontracting Order": + self.update_status() + elif self.doctype == "Subcontracting Receipt": + self.__get_subcontracting_orders + + if self.subcontracting_orders: + for sco in set(self.subcontracting_orders): + sco_doc = frappe.get_doc("Subcontracting Order", sco) + sco_doc.update_status() + @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b851795389..5adb8b273e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -136,6 +136,7 @@ class StockEntry(StockController): self.update_work_order() self.validate_subcontracting_order() self.update_subcontracting_order_supplied_items() + self.update_subcontracting_order_status() self.make_gl_entries() @@ -155,6 +156,7 @@ class StockEntry(StockController): def on_cancel(self): self.update_subcontracting_order_supplied_items() + self.update_subcontracting_order_status() if self.work_order and self.purpose == "Material Consumption for Manufacture": self.validate_work_order_status() @@ -2212,6 +2214,14 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + def update_subcontracting_order_status(self): + if self.subcontracting_order and self.purpose == "Send to Subcontractor": + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + update_subcontracting_order_status, + ) + + update_subcontracting_order_status(self.subcontracting_order) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): diff --git a/erpnext/subcontracting/doctype/subcontracting_order/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order/__init__.py new file mode 100644 index 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..80fe94483b --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -0,0 +1,322 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide('erpnext.buying'); + +frappe.ui.form.on('Subcontracting Order', { + setup: (frm) => { + frm.get_field("items").grid.cannot_add_rows = true; + frm.get_field("items").grid.only_sortable(); + + frm.set_indicator_formatter('item_code', + (doc) => (doc.qty <= doc.received_qty) ? 'green' : 'orange'); + + frm.set_query('supplier_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('purchase_order', () => { + return { + filters: { + docstatus: 1, + is_subcontracted: "Yes" + } + }; + }); + + frm.set_query('set_warehouse', () => { + return { + filters: { + company: frm.doc.company, + is_group: 0 + } + }; + }); + + frm.set_query('warehouse', 'items', () => ({ + filters: { + company: frm.doc.company, + is_group: 0 + } + })); + + frm.set_query('expense_account', 'items', () => ({ + query: 'erpnext.controllers.queries.get_expense_account', + filters: { + company: frm.doc.company + } + })); + + frm.set_query('bom', 'items', (doc, cdt, cdn) => { + let d = locals[cdt][cdn]; + return { + filters: { + item: d.item_code, + is_active: 1, + docstatus: 1, + company: frm.doc.company + } + }; + }); + + frm.set_query('set_reserve_warehouse', () => { + return { + filters: { + company: frm.doc.company, + name: ['!=', frm.doc.supplier_warehouse], + is_group: 0 + } + }; + }); + }, + + onload: (frm) => { + if (!frm.doc.transaction_date) { + frm.set_value('transaction_date', frappe.datetime.get_today()); + } + }, + + purchase_order: (frm) => { + frm.set_value('service_items', null); + frm.set_value('items', null); + frm.set_value('supplied_items', null); + + if (frm.doc.purchase_order) { + erpnext.utils.map_current_doc({ + method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order', + source_name: frm.doc.purchase_order, + target_doc: frm, + freeze: true, + freeze_message: __('Mapping Subcontracting Order ...'), + }); + } + }, + + refresh: function (frm) { + frm.trigger('get_materials_from_supplier'); + }, + + get_materials_from_supplier: function (frm) { + let sco_rm_details = []; + + if (frm.doc.supplied_items && (frm.doc.per_received == 100)) { + frm.doc.supplied_items.forEach(d => { + if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { + sco_rm_details.push(d.name); + } + }); + } + + if (sco_rm_details && sco_rm_details.length) { + frm.add_custom_button(__('Return of Components'), () => { + frm.call({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.get_materials_from_supplier', + freeze: true, + freeze_message: __('Creating Stock Entry'), + args: { subcontracting_order: frm.doc.name, sco_rm_details: sco_rm_details }, + callback: function (r) { + if (r && r.message) { + const doc = frappe.model.sync(r.message); + frappe.set_route("Form", doc[0].doctype, doc[0].name); + } + } + }); + }, __('Create')); + } + } +}); + +erpnext.buying.SubcontractingOrderController = class SubcontractingOrderController { + setup() { + this.frm.custom_make_buttons = { + 'Subcontracting Receipt': 'Subcontracting Receipt', + 'Stock Entry': 'Material to Supplier', + }; + } + + refresh(doc) { + var me = this; + + if (doc.docstatus == 1) { + if (doc.status != 'Completed') { + if (flt(doc.per_received) < 100) { + cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create')); + if (me.has_unsupplied_items()) { + cur_frm.add_custom_button(__('Material to Supplier'), + () => { + me.make_stock_entry(); + }, __('Transfer')); + } + } + cur_frm.page.set_inner_btn_group_as_primary(__('Create')); + } + } + } + + items_add(doc, cdt, cdn) { + if (doc.set_warehouse) { + var row = frappe.get_doc(cdt, cdn); + row.warehouse = doc.set_warehouse; + } + } + + set_warehouse(doc) { + this.set_warehouse_in_children(doc.items, "warehouse", doc.set_warehouse); + } + + set_reserve_warehouse(doc) { + this.set_warehouse_in_children(doc.supplied_items, "reserve_warehouse", doc.set_reserve_warehouse); + } + + set_warehouse_in_children(child_table, warehouse_field, warehouse) { + let transaction_controller = new erpnext.TransactionController(); + transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse); + } + + make_stock_entry() { + var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false); + var me = this; + + if (items.length >= 1) { + me.raw_material_data = []; + me.show_dialog = 1; + let title = __('Transfer Material to Supplier'); + let fields = [ + { fieldtype: 'Section Break', label: __('Raw Materials') }, + { + fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'), + fields: [ + { + fieldtype: 'Data', + fieldname: 'item_code', + label: __('Item'), + read_only: 1, + in_list_view: 1 + }, + { + fieldtype: 'Data', + fieldname: 'rm_item_code', + label: __('Raw Material'), + read_only: 1, + in_list_view: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'qty', + label: __('Quantity'), + in_list_view: 1 + }, + { + fieldtype: 'Data', + read_only: 1, + fieldname: 'warehouse', + label: __('Reserve Warehouse'), + in_list_view: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'rate', + label: __('Rate'), + hidden: 1 + }, + { + fieldtype: 'Float', + read_only: 1, + fieldname: 'amount', + label: __('Amount'), + hidden: 1 + }, + { + fieldtype: 'Link', + read_only: 1, + fieldname: 'uom', + label: __('UOM'), + hidden: 1 + } + ], + data: me.raw_material_data, + get_data: () => me.raw_material_data + } + ]; + + me.dialog = new frappe.ui.Dialog({ + title: title, fields: fields + }); + + if (me.frm.doc['supplied_items']) { + me.frm.doc['supplied_items'].forEach((item) => { + if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) { + me.raw_material_data.push({ + 'name': item.name, + 'item_code': item.main_item_code, + 'rm_item_code': item.rm_item_code, + 'item_name': item.rm_item_code, + 'qty': item.required_qty - item.supplied_qty, + 'warehouse': item.reserve_warehouse, + 'rate': item.rate, + 'amount': item.amount, + 'stock_uom': item.stock_uom + }); + me.dialog.fields_dict.sub_con_rm_items.grid.refresh(); + } + }); + } + + me.dialog.get_field('sub_con_rm_items').check_all_rows(); + + me.dialog.show(); + this.dialog.set_primary_action(__('Transfer'), () => { + me.values = me.dialog.get_values(); + if (me.values) { + me.values.sub_con_rm_items.map((row, i) => { + if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) { + let row_id = i + 1; + frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id])); + } + }); + me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children()); + me.dialog.hide(); + } + }); + } + + me.dialog.get_close_btn().on('click', () => { + me.dialog.hide(); + }); + } + + has_unsupplied_items() { + return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty); + } + + make_subcontracting_receipt() { + frappe.model.open_mapped_doc({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt', + frm: cur_frm, + freeze_message: __('Creating Subcontracting Receipt ...') + }); + } + + make_rm_stock_entry(rm_items) { + frappe.call({ + method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_rm_stock_entry', + args: { + subcontracting_order: cur_frm.doc.name, + rm_items: rm_items + }, + callback: (r) => { + var doclist = frappe.model.sync(r.message); + frappe.set_route('Form', doclist[0].doctype, doclist[0].name); + } + }); + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.buying.SubcontractingOrderController({ frm: cur_frm })); \ No newline at end of file diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json new file mode 100644 index 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..d12c9e825c --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -0,0 +1,372 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe import _ +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt + +from erpnext.controllers.subcontracting_controller import SubcontractingController +from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty +from erpnext.stock.utils import get_bin + + +class SubcontractingOrder(SubcontractingController): + def before_validate(self): + super(SubcontractingOrder, self).before_validate() + + def validate(self): + super(SubcontractingOrder, self).validate() + self.validate_purchase_order_for_subcontracting() + self.validate_items() + self.validate_service_items() + self.validate_supplied_items() + self.set_missing_values() + self.reset_default_field_value("set_warehouse", "items", "warehouse") + + def on_submit(self): + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() + self.update_status() + + def on_cancel(self): + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() + self.update_status() + + def validate_purchase_order_for_subcontracting(self): + if self.purchase_order: + po = frappe.get_doc("Purchase Order", self.purchase_order) + if not po.is_subcontracted: + frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting.")) + + if po.docstatus != 1: + msg = f"Please submit Purchase Order {po.name} before proceeding." + frappe.throw(_(msg)) + + if po.per_received == 100: + msg = f"Cannot create more Subcontracting Orders against the Purchase Order {po.name}." + frappe.throw(_(msg)) + else: + self.service_items = self.items = self.supplied_items = None + frappe.throw(_("Please select a Subcontracting Purchase Order.")) + + def validate_service_items(self): + for item in self.service_items: + if frappe.get_value("Item", item.item_code, "is_stock_item"): + msg = f"Service Item {item.item_name} must be a non-stock item." + frappe.throw(_(msg)) + + def validate_supplied_items(self): + if self.supplier_warehouse: + for item in self.supplied_items: + if self.supplier_warehouse == item.reserve_warehouse: + msg = f"Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {item.main_item_code}." + frappe.throw(_(msg)) + + def set_missing_values(self): + self.set_missing_values_in_additional_costs() + self.set_missing_values_in_service_items() + self.set_missing_values_in_supplied_items() + self.set_missing_values_in_items() + + def set_missing_values_in_additional_costs(self): + if self.get("additional_costs"): + self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs")) + + if self.total_additional_costs: + if self.distribute_additional_costs_based_on == "Amount": + total_amt = sum(flt(item.amount) for item in self.get("items")) + for item in self.items: + item.additional_cost_per_qty = ( + (item.amount * self.total_additional_costs) / total_amt + ) / item.qty + else: + total_qty = sum(flt(item.qty) for item in self.get("items")) + additional_cost_per_qty = self.total_additional_costs / total_qty + for item in self.items: + item.additional_cost_per_qty = additional_cost_per_qty + else: + self.total_additional_costs = 0 + + def set_missing_values_in_service_items(self): + for idx, item in enumerate(self.get("service_items")): + self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty + + def set_missing_values_in_supplied_items(self): + for item in self.get("items"): + bom = frappe.get_doc("BOM", item.bom) + rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items) + item.rm_cost_per_qty = rm_cost / flt(bom.quantity) + + def set_missing_values_in_items(self): + total_qty = total = 0 + for item in self.items: + item.rate = ( + item.rm_cost_per_qty + item.service_cost_per_qty + (item.additional_cost_per_qty or 0) + ) + item.amount = item.qty * item.rate + total_qty += flt(item.qty) + total += flt(item.amount) + else: + self.total_qty = total_qty + self.total = total + + def update_ordered_qty_for_subcontracting(self, sco_item_rows=None): + item_wh_list = [] + for item in self.get("items"): + if ( + (not sco_item_rows or item.name in sco_item_rows) + and [item.item_code, item.warehouse] not in item_wh_list + and frappe.get_cached_value("Item", item.item_code, "is_stock_item") + and item.warehouse + ): + item_wh_list.append([item.item_code, item.warehouse]) + for item_code, warehouse in item_wh_list: + update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)}) + + def update_reserved_qty_for_subcontracting(self): + for item in self.supplied_items: + if item.rm_item_code: + stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse) + stock_bin.update_reserved_qty_for_sub_contracting() + + def populate_items_table(self): + items = [] + + for si in self.service_items: + if si.fg_item: + item = frappe.get_doc("Item", si.fg_item) + bom = frappe.db.get_value("BOM", {"item": item.item_code, "is_active": 1, "is_default": 1}) + + items.append( + { + "item_code": item.item_code, + "item_name": item.item_name, + "schedule_date": self.schedule_date, + "description": item.description, + "qty": si.fg_item_qty, + "stock_uom": item.stock_uom, + "bom": bom, + }, + ) + else: + frappe.throw( + _("Please select Finished Good Item for Service Item {0}").format( + si.item_name or si.item_code + ) + ) + else: + for item in items: + self.append("items", item) + else: + self.set_missing_values() + + def update_status(self, status=None, update_modified=False): + if self.docstatus >= 1 and not status: + if self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif self.per_received >= 100: + status = "Completed" + elif self.per_received > 0 and self.per_received < 100: + status = "Partially Received" + else: + total_required_qty = total_supplied_qty = 0 + for item in self.supplied_items: + total_required_qty += item.required_qty + total_supplied_qty += item.supplied_qty or 0 + if total_supplied_qty: + status = "Partial Material Transferred" + if total_supplied_qty >= total_required_qty: + status = "Material Transferred" + elif self.docstatus == 2: + status = "Cancelled" + + if status: + frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified) + + +@frappe.whitelist() +def make_subcontracting_receipt(source_name, target_doc=None): + return get_mapped_subcontracting_receipt(source_name, target_doc) + + +def get_mapped_subcontracting_receipt(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.qty = flt(obj.qty) - flt(obj.received_qty) + target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) + + target_doc = get_mapped_doc( + "Subcontracting Order", + source_name, + { + "Subcontracting Order": { + "doctype": "Subcontracting Receipt", + "field_map": {"supplier_warehouse": "supplier_warehouse"}, + "validation": { + "docstatus": ["=", 1], + }, + }, + "Subcontracting Order Item": { + "doctype": "Subcontracting Receipt Item", + "field_map": { + "name": "subcontracting_order_item", + "parent": "subcontracting_order", + "bom": "bom", + }, + "postprocess": update_item, + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + }, + }, + target_doc, + ) + + return target_doc + + +def get_item_details(items): + item = frappe.qb.DocType("Item") + item_list = ( + frappe.qb.from_(item) + .select(item.item_code, item.description, item.allow_alternative_item) + .where(item.name.isin(items)) + .run(as_dict=True) + ) + + item_details = {} + for item in item_list: + item_details[item.item_code] = item + + return item_details + + +@frappe.whitelist() +def make_rm_stock_entry(subcontracting_order, rm_items): + rm_items_list = rm_items + + if isinstance(rm_items, str): + rm_items_list = json.loads(rm_items) + elif not rm_items: + frappe.throw(_("No Items available for transfer")) + + if rm_items_list: + fg_items = list(set(item["item_code"] for item in rm_items_list)) + else: + frappe.throw(_("No Items selected for transfer")) + + if subcontracting_order: + subcontracting_order = frappe.get_doc("Subcontracting Order", subcontracting_order) + + if fg_items: + items = tuple(set(item["rm_item_code"] for item in rm_items_list)) + item_wh = get_item_details(items) + + stock_entry = frappe.new_doc("Stock Entry") + stock_entry.purpose = "Send to Subcontractor" + stock_entry.subcontracting_order = subcontracting_order.name + stock_entry.supplier = subcontracting_order.supplier + stock_entry.supplier_name = subcontracting_order.supplier_name + stock_entry.supplier_address = subcontracting_order.supplier_address + stock_entry.address_display = subcontracting_order.address_display + stock_entry.company = subcontracting_order.company + stock_entry.to_warehouse = subcontracting_order.supplier_warehouse + stock_entry.set_stock_entry_type() + + for item_code in fg_items: + for rm_item_data in rm_items_list: + if rm_item_data["item_code"] == item_code: + rm_item_code = rm_item_data["rm_item_code"] + items_dict = { + rm_item_code: { + "sco_rm_detail": rm_item_data.get("name"), + "item_name": rm_item_data["item_name"], + "description": item_wh.get(rm_item_code, {}).get("description", ""), + "qty": rm_item_data["qty"], + "from_warehouse": rm_item_data["warehouse"], + "stock_uom": rm_item_data["stock_uom"], + "serial_no": rm_item_data.get("serial_no"), + "batch_no": rm_item_data.get("batch_no"), + "main_item_code": rm_item_data["item_code"], + "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"), + } + } + stock_entry.add_to_stock_entry_detail(items_dict) + return stock_entry.as_dict() + else: + frappe.throw(_("No Items selected for transfer")) + return subcontracting_order.name + + +def add_items_in_ste(ste_doc, row, qty, sco_rm_details, batch_no=None): + item = ste_doc.append("items", row.item_details) + + sco_rm_detail = list(set(row.sco_rm_details).intersection(sco_rm_details)) + item.update( + { + "qty": qty, + "batch_no": batch_no, + "basic_rate": row.item_details["rate"], + "sco_rm_detail": sco_rm_detail[0] if sco_rm_detail else "", + "s_warehouse": row.item_details["t_warehouse"], + "t_warehouse": row.item_details["s_warehouse"], + "item_code": row.item_details["rm_item_code"], + "subcontracted_item": row.item_details["main_item_code"], + "serial_no": "\n".join(row.serial_no) if row.serial_no else "", + } + ) + + +def make_return_stock_entry_for_subcontract(available_materials, sco_doc, sco_rm_details): + ste_doc = frappe.new_doc("Stock Entry") + ste_doc.purpose = "Material Transfer" + + ste_doc.subcontracting_order = sco_doc.name + ste_doc.company = sco_doc.company + ste_doc.is_return = 1 + + for key, value in available_materials.items(): + if not value.qty: + continue + + if value.batch_no: + for batch_no, qty in value.batch_no.items(): + if qty > 0: + add_items_in_ste(ste_doc, value, value.qty, sco_rm_details, batch_no) + else: + add_items_in_ste(ste_doc, value, value.qty, sco_rm_details) + + ste_doc.set_stock_entry_type() + ste_doc.calculate_rate_and_amount() + + return ste_doc + + +@frappe.whitelist() +def get_materials_from_supplier(subcontracting_order, sco_rm_details): + if isinstance(sco_rm_details, str): + sco_rm_details = json.loads(sco_rm_details) + + doc = frappe.get_cached_doc("Subcontracting Order", subcontracting_order) + doc.initialized_fields() + doc.subcontracting_orders = [doc.name] + doc.get_available_materials() + + if not doc.available_materials: + frappe.throw( + _("Materials are already received against the Subcontracting Order {0}").format( + subcontracting_order + ) + ) + + return make_return_stock_entry_for_subcontract(doc.available_materials, doc, sco_rm_details) + + +@frappe.whitelist() +def update_subcontracting_order_status(sco): + if isinstance(sco, str): + sco = frappe.get_doc("Subcontracting Order", sco) + + sco.update_status() diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py new file mode 100644 index 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..a2b724546b --- /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..f58c8307e4 --- /dev/null +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -0,0 +1,8 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + +class TestSubcontractingOrder(FrappeTestCase): + pass \ No newline at end of file