feat: New DocType "Subcontracting Order"
This commit is contained in:
parent
f49c51ab74
commit
249726b845
@ -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"):
|
||||
|
@ -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):
|
||||
|
@ -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 }));
|
@ -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
|
||||
}
|
@ -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()
|
@ -0,0 +1,8 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "subcontracting_order",
|
||||
"transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
|
||||
}
|
@ -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];
|
||||
},
|
||||
};
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user