feat: New DocType "Subcontracting Order"
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index b393737..4e0d911 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -627,6 +627,17 @@
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 b851795..5adb8b2 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -136,6 +136,7 @@
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 @@
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 @@
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/__init__.py
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 0000000..80fe944
--- /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 0000000..c6e76c7
--- /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 0000000..d12c9e8
--- /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 0000000..f17d8cd
--- /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 0000000..a2b7245
--- /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 0000000..f58c830
--- /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