feat: New DocType "Subcontracting Order"

This commit is contained in:
Sagar Sharma 2022-04-22 17:09:19 +05:30
parent f49c51ab74
commit 249726b845
9 changed files with 1232 additions and 0 deletions

View File

@ -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"):

View File

@ -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):

View File

@ -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 }));

View File

@ -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
}

View File

@ -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()

View File

@ -0,0 +1,8 @@
from frappe import _
def get_data():
return {
"fieldname": "subcontracting_order",
"transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
}

View File

@ -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];
},
};

View File

@ -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