mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-23 23:19:20 +00:00
feat: subcontracting inward (#47728)
* feat: subcontracting inward * feat: stock reservation * feat: subcontracting delivery * feat: all remaining stuff * fix: linter errors * fix: patch * fix: modify stock entry type validation * fix: customer provided item cost field mandatory validation * fix: failing tests * fix: failing tests * fix: subcontracting controlller * refactor: semi final * refactor: final * chore: resolve conflicts * refactor: changes requested * fix: reservation transfer of extra qty * fix: consider add cost for customer provided rate field * test: create test data * test: subcontracted sales order (partial) * test: fin * fix: do not add self RM in DN created from SI * fix: failing test case * fix: conflicting function name * refactor: final changes * fix: more bugs * perf: various and major performance improvements * fix: consider warehouse as well in all queries * fix: same item code with diff warehouse in manufacture entry * refactor: readability * fix: frontend validations * perf: replace query inside loop with single query * fix: set additional item flag to true when extra customer provided item is received * fix: bugs found by coderabbit * fix: more coderabbit bugs * fix: add validation to disallow cancellation of manufacturing entry * perf: use cached values wherever it makes sense * test: fix redundant insert to child tables * fix: consider SI return of billed self RM * fix: bug found by coderabbit --------- Co-authored-by: Mihir Kandoi <mihirkandoi@Mihirs-MacBook-Air.local>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// client script for Subcontracting Inward Order Item is not necessarily required as the server side code will do everything that is necessary.
|
||||
// this is just so that the user does not get potentially confused
|
||||
frappe.ui.form.on("Subcontracting Inward Order Item", {
|
||||
qty(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
const service_item = frm.doc.service_items[row.idx - 1];
|
||||
frappe.model.set_value(
|
||||
service_item.doctype,
|
||||
service_item.name,
|
||||
"qty",
|
||||
row.qty * row.subcontracting_conversion_factor
|
||||
);
|
||||
frappe.model.set_value(service_item.doctype, service_item.name, "fg_item_qty", row.qty);
|
||||
},
|
||||
before_items_remove(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
frm.toggle_enable(["service_items"], true);
|
||||
frm.get_field("service_items").grid.grid_rows[row.idx - 1].remove();
|
||||
frm.toggle_enable(["service_items"], false);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Subcontracting Inward Order", {
|
||||
setup: (frm) => {
|
||||
frm.get_field("items").grid.cannot_add_rows = true;
|
||||
|
||||
frm.set_query("customer_warehouse", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
is_rejected_warehouse: 0,
|
||||
company: frm.doc.company,
|
||||
customer: frm.doc.customer,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("sales_order", () => {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
is_subcontracted: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("delivery_warehouse", "items", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
is_rejected_warehouse: 0,
|
||||
company: frm.doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("set_delivery_warehouse", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
is_rejected_warehouse: 0,
|
||||
company: frm.doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
set_delivery_warehouse: (frm) => {
|
||||
frm.doc.items.forEach((item) =>
|
||||
frappe.model.set_value(
|
||||
item.doctype,
|
||||
item.name,
|
||||
"delivery_warehouse",
|
||||
frm.doc.set_delivery_warehouse
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
sales_order: (frm) => {
|
||||
frm.set_value("service_items", null);
|
||||
frm.set_value("items", null);
|
||||
frm.set_value("received_items", null);
|
||||
|
||||
if (frm.doc.sales_order) {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_subcontracting_inward_order",
|
||||
source_name: frm.doc.sales_order,
|
||||
target_doc: frm,
|
||||
freeze: true,
|
||||
freeze_message: __("Mapping Subcontracting Inward Order ..."),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (frm.has_perm("submit")) {
|
||||
if (frm.doc.status == "Closed") {
|
||||
frm.add_custom_button(
|
||||
__("Re-open"),
|
||||
() => frm.events.update_subcontracting_inward_order_status(frm),
|
||||
__("Status")
|
||||
);
|
||||
} else {
|
||||
frm.add_custom_button(
|
||||
__("Close"),
|
||||
() => frm.events.update_subcontracting_inward_order_status(frm, "Closed"),
|
||||
__("Status")
|
||||
);
|
||||
}
|
||||
}
|
||||
if (frm.doc.status != "Closed") {
|
||||
const is_raw_materials_received = frm.doc.received_items.some((item) =>
|
||||
item.is_customer_provided_item
|
||||
? item.received_qty - item.work_order_qty - item.returned_qty > 0
|
||||
: false
|
||||
);
|
||||
if (is_raw_materials_received) {
|
||||
frm.add_custom_button(
|
||||
__("Raw Materials to Customer"),
|
||||
() => frm.trigger("make_rm_return"),
|
||||
__("Return")
|
||||
);
|
||||
if (frm.doc.per_produced < 100) {
|
||||
frm.add_custom_button(
|
||||
__("Work Order"),
|
||||
() => frm.events.make_work_order(frm),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.per_produced < 100) {
|
||||
frm.add_custom_button(
|
||||
__("Material from Customer"),
|
||||
() => frm.events.make_stock_entry(frm),
|
||||
__("Receive")
|
||||
);
|
||||
}
|
||||
if (frm.doc.per_produced > 0 && frm.doc.per_delivered < 100) {
|
||||
frm.add_custom_button(
|
||||
__("Subcontracting Delivery"),
|
||||
() => frm.events.make_subcontracting_delivery(frm),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
if (frm.doc.per_delivered > 0 && frm.doc.per_returned < 100) {
|
||||
frm.add_custom_button(
|
||||
__("Finished Goods Return"),
|
||||
() => frm.events.make_subcontracting_return(frm),
|
||||
__("Return")
|
||||
);
|
||||
}
|
||||
if (frm.doc.per_produced < 100) {
|
||||
frm.page.set_inner_btn_group_as_primary(__("Receive"));
|
||||
} else if (frm.doc.per_delivered < 100) {
|
||||
frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
} else if (frm.doc.per_delivered >= 100 && frm.doc.per_returned < 100) {
|
||||
frm.page.set_inner_btn_group_as_primary(__("Return"));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
update_subcontracting_inward_order_status(frm, status) {
|
||||
frappe.call({
|
||||
method: "erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order.update_subcontracting_inward_order_status",
|
||||
args: {
|
||||
scio: frm.doc.name,
|
||||
status: status,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frm.reload_doc();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
make_work_order(frm) {
|
||||
frappe.call({
|
||||
method: "make_work_order",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
make_stock_entry(frm) {
|
||||
frappe.call({
|
||||
method: "make_rm_stock_entry_inward",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: (r) => {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
make_rm_return(frm) {
|
||||
frappe.call({
|
||||
method: "make_rm_return",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: (r) => {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
make_subcontracting_delivery(frm) {
|
||||
frappe.call({
|
||||
method: "make_subcontracting_delivery",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: (r) => {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
make_subcontracting_return(frm) {
|
||||
frappe.call({
|
||||
method: "make_subcontracting_return",
|
||||
freeze: true,
|
||||
doc: frm.doc,
|
||||
callback: (r) => {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,374 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_auto_repeat": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2025-03-24 12:50:26.464612",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"sales_order",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"currency",
|
||||
"column_break_7",
|
||||
"company",
|
||||
"transaction_date",
|
||||
"customer_warehouse",
|
||||
"amended_from",
|
||||
"items_section",
|
||||
"set_delivery_warehouse",
|
||||
"items",
|
||||
"raw_materials_received_section",
|
||||
"received_items",
|
||||
"scrap_items_generated_section",
|
||||
"scrap_items",
|
||||
"service_items_section",
|
||||
"service_items",
|
||||
"tab_other_info",
|
||||
"order_status_section",
|
||||
"status",
|
||||
"per_raw_material_received",
|
||||
"per_produced",
|
||||
"per_delivered",
|
||||
"column_break_39",
|
||||
"per_raw_material_returned",
|
||||
"per_process_loss",
|
||||
"per_returned",
|
||||
"tab_connections"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "{customer_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": "SCI-ORD-.YYYY.-",
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subcontracting Sales Order",
|
||||
"options": "Sales Order",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Customer",
|
||||
"options": "Customer",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Customer Name",
|
||||
"read_only": 1,
|
||||
"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": "sales_order.transaction_date",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Inward Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"depends_on": "sales_order",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Items",
|
||||
"options": "Subcontracting Inward Order Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "service_items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Service Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Service Items",
|
||||
"options": "Subcontracting Inward Order Service Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "received_items",
|
||||
"depends_on": "received_items",
|
||||
"fieldname": "raw_materials_received_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Raw Materials Required"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "received_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Required Items",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Inward Order Received Item",
|
||||
"print_hide": 1,
|
||||
"read_only": 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\nOngoing\nProduced\nDelivered\nCancelled\nClosed",
|
||||
"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_delivered",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Delivered",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_other_info",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Other Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "tab_connections",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "per_produced",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Produced",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Items"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "per_process_loss",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Process Loss",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_delivery_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Set Delivery Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "scrap_items",
|
||||
"fieldname": "scrap_items_generated_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Scrap Items Generated"
|
||||
},
|
||||
{
|
||||
"fieldname": "scrap_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Scrap Items",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Inward Order Scrap Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "per_returned",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Returned",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "per_raw_material_returned",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Raw Material Returned",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "per_raw_material_received",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Raw Material Received",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.default_currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Customer Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-05 14:41:46.859510",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"permlevel": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status, transaction_date, customer",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import comma_and, flt, get_link_to_form
|
||||
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
|
||||
|
||||
class SubcontractingInwardOrder(SubcontractingController):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_item.subcontracting_inward_order_item import (
|
||||
SubcontractingInwardOrderItem,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_received_item.subcontracting_inward_order_received_item import (
|
||||
SubcontractingInwardOrderReceivedItem,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_scrap_item.subcontracting_inward_order_scrap_item import (
|
||||
SubcontractingInwardOrderScrapItem,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order_service_item.subcontracting_inward_order_service_item import (
|
||||
SubcontractingInwardOrderServiceItem,
|
||||
)
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
currency: DF.Link | None
|
||||
customer: DF.Link
|
||||
customer_name: DF.Data
|
||||
customer_warehouse: DF.Link
|
||||
items: DF.Table[SubcontractingInwardOrderItem]
|
||||
naming_series: DF.Literal["SCI-ORD-.YYYY.-"]
|
||||
per_delivered: DF.Percent
|
||||
per_process_loss: DF.Percent
|
||||
per_produced: DF.Percent
|
||||
per_raw_material_received: DF.Percent
|
||||
per_raw_material_returned: DF.Percent
|
||||
per_returned: DF.Percent
|
||||
received_items: DF.Table[SubcontractingInwardOrderReceivedItem]
|
||||
sales_order: DF.Link
|
||||
scrap_items: DF.Table[SubcontractingInwardOrderScrapItem]
|
||||
service_items: DF.Table[SubcontractingInwardOrderServiceItem]
|
||||
set_delivery_warehouse: DF.Link | None
|
||||
status: DF.Literal["Draft", "Open", "Ongoing", "Produced", "Delivered", "Cancelled", "Closed"]
|
||||
title: DF.Data | None
|
||||
transaction_date: DF.Date
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.set_is_customer_provided_item()
|
||||
self.validate_customer_provided_items()
|
||||
self.validate_customer_warehouse()
|
||||
self.validate_service_items()
|
||||
self.set_missing_values()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_status()
|
||||
self.update_subcontracted_quantity_in_so()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_status()
|
||||
self.update_subcontracted_quantity_in_so()
|
||||
|
||||
def update_status(self, status=None, update_modified=True):
|
||||
if self.status == "Closed" and self.status != status:
|
||||
check_on_hold_or_closed_status("Sales Order", self.sales_order)
|
||||
|
||||
total_to_be_received = total_received = total_rm_returned = 0
|
||||
for rm in self.get("received_items"):
|
||||
if rm.get("is_customer_provided_item"):
|
||||
total_to_be_received += flt(rm.required_qty)
|
||||
total_received += flt(rm.received_qty)
|
||||
total_rm_returned += flt(rm.returned_qty)
|
||||
|
||||
total_to_be_produced = total_produced = total_process_loss = total_delivered = total_fg_returned = 0
|
||||
for item in self.get("items"):
|
||||
total_to_be_produced += flt(item.qty)
|
||||
total_produced += flt(item.produced_qty)
|
||||
total_process_loss += flt(item.process_loss_qty)
|
||||
total_delivered += flt(item.delivered_qty)
|
||||
total_fg_returned += flt(item.returned_qty)
|
||||
|
||||
per_raw_material_received = flt(total_received / total_to_be_received * 100, 2)
|
||||
per_raw_material_returned = flt(total_rm_returned / total_received * 100, 2) if total_received else 0
|
||||
per_produced = flt(total_produced / total_to_be_produced * 100, 2)
|
||||
per_process_loss = flt(total_process_loss / total_produced * 100, 2) if total_produced else 0
|
||||
per_delivered = flt(total_delivered / total_to_be_produced * 100, 2)
|
||||
per_returned = flt(total_fg_returned / total_delivered * 100, 2) if total_delivered else 0
|
||||
|
||||
self.db_set("per_raw_material_received", per_raw_material_received, update_modified=update_modified)
|
||||
self.db_set("per_raw_material_returned", per_raw_material_returned, update_modified=update_modified)
|
||||
self.db_set("per_produced", per_produced, update_modified=update_modified)
|
||||
self.db_set("per_process_loss", per_process_loss, update_modified=update_modified)
|
||||
self.db_set("per_delivered", per_delivered, update_modified=update_modified)
|
||||
self.db_set("per_returned", per_returned, update_modified=update_modified)
|
||||
|
||||
if self.docstatus >= 1 and not status:
|
||||
if self.docstatus == 1:
|
||||
if self.status == "Draft":
|
||||
status = "Open"
|
||||
elif self.per_delivered == 100:
|
||||
status = "Delivered"
|
||||
elif self.per_produced == 100:
|
||||
status = "Produced"
|
||||
elif self.per_raw_material_received > 0:
|
||||
status = "Ongoing"
|
||||
else:
|
||||
status = "Open"
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
|
||||
if status and self.status != status:
|
||||
self.db_set("status", status, update_modified=update_modified)
|
||||
|
||||
def update_subcontracted_quantity_in_so(self):
|
||||
for service_item in self.service_items:
|
||||
doc = frappe.get_doc("Sales Order Item", service_item.sales_order_item)
|
||||
doc.subcontracted_qty = (
|
||||
(doc.subcontracted_qty + service_item.qty)
|
||||
if self._action == "submit"
|
||||
else (doc.subcontracted_qty - service_item.qty)
|
||||
)
|
||||
doc.save()
|
||||
|
||||
def validate_customer_warehouse(self):
|
||||
if frappe.get_cached_value("Warehouse", self.customer_warehouse, "customer") != self.customer:
|
||||
frappe.throw(
|
||||
_("Customer Warehouse {0} does not belong to Customer {1}.").format(
|
||||
frappe.bold(self.customer_warehouse), frappe.bold(self.customer)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_service_items(self):
|
||||
sales_order_items = [item.sales_order_item for item in self.items]
|
||||
self.service_items = [
|
||||
service_item
|
||||
for service_item in self.service_items
|
||||
if service_item.sales_order_item in sales_order_items
|
||||
]
|
||||
|
||||
for service_item in self.service_items:
|
||||
item = next(item for item in self.items if item.sales_order_item == service_item.sales_order_item)
|
||||
service_item.qty = item.qty * item.subcontracting_conversion_factor
|
||||
service_item.fg_item_qty = item.qty
|
||||
service_item.amount = service_item.qty * service_item.rate
|
||||
|
||||
def populate_items_table(self):
|
||||
items = []
|
||||
|
||||
for si in self.service_items:
|
||||
if si.fg_item:
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
so_item = frappe.get_doc("Sales Order Item", si.sales_order_item)
|
||||
available_qty = so_item.qty - so_item.subcontracted_qty
|
||||
|
||||
if available_qty == 0:
|
||||
continue
|
||||
|
||||
si.qty = available_qty
|
||||
conversion_factor = so_item.qty / so_item.fg_item_qty
|
||||
si.fg_item_qty = flt(
|
||||
available_qty / conversion_factor, frappe.get_precision("Sales Order Item", "qty")
|
||||
)
|
||||
si.amount = available_qty * si.rate
|
||||
|
||||
bom = (
|
||||
frappe.db.get_value(
|
||||
"Subcontracting BOM",
|
||||
{"finished_good": item.name, "is_active": 1},
|
||||
"finished_good_bom",
|
||||
)
|
||||
or item.default_bom
|
||||
)
|
||||
|
||||
items.append(
|
||||
{
|
||||
"item_code": item.name,
|
||||
"item_name": item.item_name,
|
||||
"expected_delivery_date": frappe.get_cached_value(
|
||||
"Sales Order Item", si.sales_order_item, "delivery_date"
|
||||
),
|
||||
"description": item.description,
|
||||
"qty": si.fg_item_qty,
|
||||
"subcontracting_conversion_factor": conversion_factor,
|
||||
"stock_uom": item.stock_uom,
|
||||
"bom": bom,
|
||||
"sales_order_item": si.sales_order_item,
|
||||
}
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Please select Finished Good Item for Service Item {0}").format(
|
||||
si.item_name or si.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if items:
|
||||
for item in items:
|
||||
self.append("items", item)
|
||||
|
||||
def validate_customer_provided_items(self):
|
||||
"""Check if atleast one raw material is customer provided"""
|
||||
for item in self.get("items"):
|
||||
raw_materials = [rm for rm in self.get("received_items") if rm.main_item_code == item.item_code]
|
||||
if not any([rm.is_customer_provided_item for rm in raw_materials]):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Atleast one raw material for Finished Good Item {0} should be customer provided."
|
||||
).format(frappe.bold(item.item_code))
|
||||
)
|
||||
|
||||
def set_is_customer_provided_item(self):
|
||||
for item in self.get("received_items"):
|
||||
item.is_customer_provided_item = frappe.get_cached_value(
|
||||
"Item", item.rm_item_code, "is_customer_provided_item"
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_work_order(self):
|
||||
"""Create Work Order from Subcontracting Inward Order."""
|
||||
wo_list = []
|
||||
|
||||
for item in self.get_production_items():
|
||||
work_order = self.create_work_order(item)
|
||||
if work_order:
|
||||
wo_list.append(work_order)
|
||||
|
||||
self.show_list_created_message("Work Order", wo_list)
|
||||
|
||||
if not wo_list:
|
||||
frappe.msgprint(_("No Work Orders were created"))
|
||||
|
||||
return wo_list
|
||||
|
||||
def get_production_items(self):
|
||||
item_list = []
|
||||
|
||||
for d in self.items:
|
||||
if d.produced_qty >= d.qty:
|
||||
continue
|
||||
|
||||
item_details = {
|
||||
"production_item": d.item_code,
|
||||
"use_multi_level_bom": d.include_exploded_items,
|
||||
"subcontracting_inward_order": self.name,
|
||||
"bom_no": d.bom,
|
||||
"stock_uom": d.stock_uom,
|
||||
"company": self.company,
|
||||
"project": frappe.get_cached_value("Sales Order", self.sales_order, "project"),
|
||||
"source_warehouse": self.customer_warehouse,
|
||||
"subcontracting_inward_order_item": d.name,
|
||||
"reserve_stock": 1,
|
||||
"fg_warehouse": d.delivery_warehouse,
|
||||
}
|
||||
|
||||
qty = min(
|
||||
[
|
||||
flt(
|
||||
(item.received_qty - item.returned_qty - item.work_order_qty)
|
||||
/ flt(item.required_qty / d.qty, d.precision("qty")),
|
||||
d.precision("qty"),
|
||||
)
|
||||
for item in self.get("received_items")
|
||||
if item.reference_name == d.name and item.is_customer_provided_item
|
||||
]
|
||||
)
|
||||
qty = int(qty) if frappe.get_cached_value("UOM", d.stock_uom, "must_be_whole_number") else qty
|
||||
|
||||
item_details.update({"qty": qty, "max_producible_qty": qty})
|
||||
item_list.append(item_details)
|
||||
|
||||
return item_list
|
||||
|
||||
def create_work_order(self, item):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
|
||||
|
||||
if flt(item.get("qty")) <= 0:
|
||||
return
|
||||
|
||||
wo = frappe.new_doc("Work Order")
|
||||
wo.update(item)
|
||||
|
||||
wo.set_work_order_operations()
|
||||
wo.set_required_items()
|
||||
|
||||
try:
|
||||
wo.flags.ignore_mandatory = True
|
||||
wo.flags.ignore_validate = True
|
||||
wo.insert()
|
||||
return wo.name
|
||||
except OverProductionError:
|
||||
pass
|
||||
|
||||
def show_list_created_message(self, doctype, doc_list=None):
|
||||
if not doc_list:
|
||||
return
|
||||
|
||||
frappe.flags.mute_messages = False
|
||||
if doc_list:
|
||||
doc_list = [get_link_to_form(doctype, p) for p in doc_list]
|
||||
frappe.msgprint(_("{0} created").format(comma_and(doc_list)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry_inward(self, target_doc=None):
|
||||
def calculate_qty_as_per_bom(rm_item):
|
||||
data = frappe.get_value(
|
||||
"Subcontracting Inward Order Item",
|
||||
{"name": rm_item.reference_name},
|
||||
["process_loss_qty", "include_exploded_items"],
|
||||
as_dict=True,
|
||||
)
|
||||
stock_qty = frappe.get_value(
|
||||
"BOM Explosion Item" if data.include_exploded_items else "BOM Item",
|
||||
{"name": rm_item.bom_detail_no},
|
||||
"stock_qty",
|
||||
)
|
||||
qty = flt(
|
||||
stock_qty * data.process_loss_qty,
|
||||
frappe.get_precision("Subcontracting Inward Order Received Item", "required_qty"),
|
||||
)
|
||||
return rm_item.required_qty - rm_item.received_qty + rm_item.returned_qty + qty
|
||||
|
||||
if target_doc and target_doc.get("items"):
|
||||
target_doc.items = []
|
||||
|
||||
stock_entry = get_mapped_doc(
|
||||
"Subcontracting Inward Order",
|
||||
self.name,
|
||||
{
|
||||
"Subcontracting Inward Order": {
|
||||
"doctype": "Stock Entry",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
ignore_child_tables=True,
|
||||
)
|
||||
|
||||
stock_entry.purpose = "Receive from Customer"
|
||||
stock_entry.subcontracting_inward_order = self.name
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
for rm_item in self.received_items:
|
||||
if not rm_item.required_qty or not rm_item.is_customer_provided_item:
|
||||
continue
|
||||
|
||||
items_dict = {
|
||||
rm_item.get("rm_item_code"): {
|
||||
"scio_detail": rm_item.get("name"),
|
||||
"qty": calculate_qty_as_per_bom(rm_item),
|
||||
"to_warehouse": rm_item.get("warehouse"),
|
||||
"stock_uom": rm_item.get("stock_uom"),
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if target_doc:
|
||||
return stock_entry
|
||||
else:
|
||||
return stock_entry.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_return(self, target_doc=None):
|
||||
if target_doc and target_doc.get("items"):
|
||||
target_doc.items = []
|
||||
|
||||
stock_entry = get_mapped_doc(
|
||||
"Subcontracting Inward Order",
|
||||
self.name,
|
||||
{
|
||||
"Subcontracting Inward Order": {
|
||||
"doctype": "Stock Entry",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
ignore_child_tables=True,
|
||||
)
|
||||
|
||||
stock_entry.purpose = "Return Raw Material to Customer"
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.subcontracting_inward_order = self.name
|
||||
|
||||
for rm_item in self.received_items:
|
||||
items_dict = {
|
||||
rm_item.get("rm_item_code"): {
|
||||
"scio_detail": rm_item.get("name"),
|
||||
"qty": rm_item.received_qty - rm_item.work_order_qty - rm_item.returned_qty,
|
||||
"from_warehouse": rm_item.get("warehouse"),
|
||||
"stock_uom": rm_item.get("stock_uom"),
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if target_doc:
|
||||
return stock_entry
|
||||
else:
|
||||
return stock_entry.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_delivery(self, target_doc=None):
|
||||
if target_doc and target_doc.get("items"):
|
||||
target_doc.items = []
|
||||
|
||||
stock_entry = get_mapped_doc(
|
||||
"Subcontracting Inward Order",
|
||||
self.name,
|
||||
{
|
||||
"Subcontracting Inward Order": {
|
||||
"doctype": "Stock Entry",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
ignore_child_tables=True,
|
||||
)
|
||||
|
||||
stock_entry.purpose = "Subcontracting Delivery"
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.subcontracting_inward_order = self.name
|
||||
scio_details = []
|
||||
|
||||
allow_over = frappe.get_single_value("Selling Settings", "allow_delivery_of_overproduced_qty")
|
||||
for fg_item in self.items:
|
||||
qty = (
|
||||
fg_item.produced_qty
|
||||
if allow_over
|
||||
else min(fg_item.qty, fg_item.produced_qty) - fg_item.delivered_qty - fg_item.returned_qty
|
||||
)
|
||||
if qty < 0:
|
||||
continue
|
||||
|
||||
scio_details.append(fg_item.name)
|
||||
items_dict = {
|
||||
fg_item.item_code: {
|
||||
"qty": qty,
|
||||
"from_warehouse": fg_item.delivery_warehouse,
|
||||
"stock_uom": fg_item.stock_uom,
|
||||
"scio_detail": fg_item.name,
|
||||
"is_finished_item": 1,
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if (
|
||||
frappe.get_single_value("Selling Settings", "deliver_scrap_items")
|
||||
and self.scrap_items
|
||||
and scio_details
|
||||
):
|
||||
scrap_items = [
|
||||
scrap_item for scrap_item in self.scrap_items if scrap_item.reference_name in scio_details
|
||||
]
|
||||
for scrap_item in scrap_items:
|
||||
qty = scrap_item.produced_qty - scrap_item.delivered_qty
|
||||
if qty > 0:
|
||||
items_dict = {
|
||||
scrap_item.item_code: {
|
||||
"qty": scrap_item.produced_qty - scrap_item.delivered_qty,
|
||||
"from_warehouse": scrap_item.warehouse,
|
||||
"stock_uom": scrap_item.stock_uom,
|
||||
"scio_detail": scrap_item.name,
|
||||
"is_scrap_item": 1,
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if target_doc:
|
||||
return stock_entry
|
||||
else:
|
||||
return stock_entry.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_subcontracting_return(self, target_doc=None):
|
||||
if target_doc and target_doc.get("items"):
|
||||
target_doc.items = []
|
||||
|
||||
stock_entry = get_mapped_doc(
|
||||
"Subcontracting Inward Order",
|
||||
self.name,
|
||||
{
|
||||
"Subcontracting Inward Order": {
|
||||
"doctype": "Stock Entry",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
"field_map": {"name": "subcontracting_inward_order"},
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
ignore_child_tables=True,
|
||||
)
|
||||
|
||||
stock_entry.purpose = "Subcontracting Return"
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
for fg_item in self.items:
|
||||
qty = fg_item.delivered_qty - fg_item.returned_qty
|
||||
if qty < 0:
|
||||
continue
|
||||
|
||||
items_dict = {
|
||||
fg_item.item_code: {
|
||||
"qty": qty,
|
||||
"stock_uom": fg_item.stock_uom,
|
||||
"scio_detail": fg_item.name,
|
||||
"is_finished_item": 1,
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if target_doc:
|
||||
return stock_entry
|
||||
else:
|
||||
return stock_entry.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_subcontracting_inward_order_status(scio, status=None):
|
||||
if isinstance(scio, str):
|
||||
scio = frappe.get_doc("Subcontracting Inward Order", scio)
|
||||
|
||||
scio.update_status(status)
|
||||
@@ -0,0 +1,17 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "subcontracting_inward_order",
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Transactions"),
|
||||
"items": ["Stock Entry"],
|
||||
},
|
||||
{
|
||||
"label": _("Manufacturing"),
|
||||
"items": ["Work Order"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.listview_settings["Subcontracting Inward Order"] = {
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
Draft: "red",
|
||||
Open: "orange",
|
||||
Ongoing: "yellow",
|
||||
Produced: "blue",
|
||||
Delivered: "green",
|
||||
Closed: "grey",
|
||||
Cancelled: "red",
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,559 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record dependencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_stock_entry_from_wo
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_subcontracting_inward_order
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
class IntegrationTestSubcontractingInwardOrder(IntegrationTestCase):
|
||||
"""
|
||||
Integration tests for SubcontractingInwardOrder.
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
create_test_data()
|
||||
make_stock_entry(
|
||||
item_code="Self RM", qty=100, to_warehouse="Stores - _TC", purpose="Material Receipt"
|
||||
)
|
||||
return super().setUp()
|
||||
|
||||
def test_customer_provided_item_cost_field(self):
|
||||
so, scio = create_so_scio()
|
||||
|
||||
rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
|
||||
rm_in.save()
|
||||
for item in rm_in.get("items"):
|
||||
item.basic_rate = 10
|
||||
rm_in.append(
|
||||
"additional_costs",
|
||||
{
|
||||
"expense_account": "Freight and Forwarding Charges - _TC",
|
||||
"description": "Test",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
rm_in.submit()
|
||||
|
||||
for item in rm_in.get("items"):
|
||||
self.assertEqual(item.customer_provided_item_cost, 15)
|
||||
|
||||
def test_add_extra_customer_provided_item(self):
|
||||
so, scio = create_so_scio()
|
||||
|
||||
rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
|
||||
rm_in.save()
|
||||
rm_in.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "Basic RM 2",
|
||||
"qty": 5,
|
||||
"t_warehouse": rm_in.items[0].t_warehouse,
|
||||
"basic_rate": 10,
|
||||
"transfer_qty": 5,
|
||||
"uom": "Nos",
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
rm_in.submit()
|
||||
|
||||
scio.reload()
|
||||
self.assertTrue(
|
||||
next((item for item in scio.received_items if item.rm_item_code == "Basic RM 2"), None)
|
||||
)
|
||||
|
||||
def test_add_extra_item_during_manufacture(self):
|
||||
make_stock_entry(
|
||||
item_code="Self RM 2", qty=5, to_warehouse="Stores - _TC", purpose="Material Receipt"
|
||||
)
|
||||
so, scio = create_so_scio()
|
||||
frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit()
|
||||
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
next(
|
||||
item for item in wo.required_items if item.item_code == "Self RM"
|
||||
).source_warehouse = "Stores - _TC"
|
||||
wo.submit()
|
||||
|
||||
manufacture = frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture"))
|
||||
manufacture.save()
|
||||
frappe.new_doc(
|
||||
"Stock Entry Detail",
|
||||
parent=manufacture.name,
|
||||
parenttype="Stock Entry",
|
||||
parentfield="items",
|
||||
idx=6,
|
||||
item_code="Self RM 2",
|
||||
qty=5,
|
||||
s_warehouse="Stores - _TC",
|
||||
basic_rate=10,
|
||||
transfer_qty=5,
|
||||
uom="Nos",
|
||||
conversion_factor=1,
|
||||
cost_center="Main - _TC",
|
||||
).insert()
|
||||
manufacture.reload()
|
||||
manufacture.submit()
|
||||
scio.reload()
|
||||
self.assertTrue(
|
||||
next((item for item in scio.received_items if item.rm_item_code == "Self RM 2"), None)
|
||||
)
|
||||
|
||||
def test_work_order_creation_qty(self):
|
||||
new_bom = frappe.copy_doc(frappe.get_doc("BOM", "BOM-Basic FG Item-001"))
|
||||
new_bom.items = new_bom.items[:3]
|
||||
new_bom.items[1].qty = 2
|
||||
new_bom.items[2].qty = 3
|
||||
new_bom.submit()
|
||||
sc_bom = frappe.get_doc("Subcontracting BOM", "SB-0001")
|
||||
sc_bom.finished_good_bom = new_bom.name
|
||||
sc_bom.save()
|
||||
|
||||
so, scio = create_so_scio()
|
||||
|
||||
rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
|
||||
rm_in.items[0].qty = 3
|
||||
rm_in.items[1].qty = 5
|
||||
rm_in.items[2].qty = 12
|
||||
rm_in.submit()
|
||||
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
self.assertEqual(wo.qty, 2)
|
||||
|
||||
def test_rm_return(self):
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
|
||||
|
||||
so, scio = create_so_scio()
|
||||
|
||||
rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
|
||||
rm_in.items[3].qty = 2
|
||||
rm_in.submit()
|
||||
|
||||
serial_nos = get_serial_nos(rm_in.items[3].serial_and_batch_bundle)
|
||||
batch_nos = list(get_batch_nos(rm_in.items[3].serial_and_batch_bundle).keys())
|
||||
|
||||
scio.reload()
|
||||
rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
|
||||
backup = rm_in.items[-1]
|
||||
rm_in.items.clear()
|
||||
rm_in.items.append(backup)
|
||||
|
||||
rm_in.items[0].qty = 1
|
||||
rm_in.submit()
|
||||
|
||||
serial_nos += get_serial_nos(rm_in.items[0].serial_and_batch_bundle)
|
||||
batch_nos += list(get_batch_nos(rm_in.items[0].serial_and_batch_bundle).keys())
|
||||
|
||||
scio.reload()
|
||||
rm_return = frappe.new_doc("Stock Entry").update(scio.make_rm_return())
|
||||
rm_return.submit()
|
||||
|
||||
self.assertEqual(
|
||||
sorted(get_serial_nos(rm_return.items[-1].serial_and_batch_bundle)), sorted(serial_nos)
|
||||
)
|
||||
self.assertEqual(
|
||||
sorted(list(get_batch_nos(rm_return.items[-1].serial_and_batch_bundle).keys())), sorted(batch_nos)
|
||||
)
|
||||
|
||||
def test_subcontracting_delivery(self):
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_batch_list_from_item
|
||||
|
||||
extra_serial, _ = get_serial_batch_list_from_item(
|
||||
make_stock_entry(
|
||||
item_code="FG Item with Serial",
|
||||
qty=1,
|
||||
to_warehouse="Stores - _TC",
|
||||
purpose="Material Receipt",
|
||||
).items[0]
|
||||
)
|
||||
so, scio = create_so_scio(service_item="Service Item 2", fg_item="FG Item with Serial")
|
||||
frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit()
|
||||
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.submit()
|
||||
|
||||
manufacture = frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture"))
|
||||
manufacture.submit()
|
||||
|
||||
serial_list, _ = get_serial_batch_list_from_item(
|
||||
next(item for item in manufacture.items if item.is_finished_item)
|
||||
)
|
||||
|
||||
scio.reload()
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
delivery.items[0].use_serial_batch_fields = 1
|
||||
delivery.save()
|
||||
delivery_serial_list, _ = get_serial_batch_list_from_item(delivery.items[0])
|
||||
self.assertEqual(sorted(serial_list), sorted(delivery_serial_list))
|
||||
|
||||
delivery_serial_list[-1] = extra_serial[0]
|
||||
delivery.items[0].serial_no = "\n".join(delivery_serial_list)
|
||||
self.assertRaises(frappe.ValidationError, delivery.submit)
|
||||
|
||||
def test_fg_item_fields(self):
|
||||
so, scio = create_so_scio()
|
||||
frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit()
|
||||
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.submit()
|
||||
|
||||
manufacture = frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture"))
|
||||
manufacture.save()
|
||||
manufacture.fg_completed_qty = 5
|
||||
manufacture.process_loss_qty = 1
|
||||
manufacture.items[-1].qty = 4
|
||||
manufacture.submit()
|
||||
|
||||
scio.reload()
|
||||
self.assertEqual(scio.items[0].qty, 5)
|
||||
self.assertEqual(scio.items[0].process_loss_qty, 1)
|
||||
self.assertEqual(scio.items[0].produced_qty, 4)
|
||||
rm_in = scio.make_rm_stock_entry_inward()
|
||||
for item in rm_in.get("items"):
|
||||
self.assertEqual(item.qty, 1)
|
||||
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
delivery.items[0].qty = 5
|
||||
self.assertRaises(frappe.ValidationError, delivery.submit)
|
||||
delivery.items[0].qty = 2
|
||||
delivery.submit()
|
||||
|
||||
scio.reload()
|
||||
fg_return = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_return())
|
||||
self.assertEqual(fg_return.items[0].qty, 2)
|
||||
fg_return.items[0].qty = 1
|
||||
fg_return.items[0].t_warehouse = "Stores - _TC"
|
||||
fg_return.submit()
|
||||
|
||||
scio.reload()
|
||||
self.assertEqual(scio.items[0].delivered_qty, 2)
|
||||
self.assertEqual(scio.items[0].returned_qty, 1)
|
||||
|
||||
@IntegrationTestCase.change_settings("Selling Settings", {"allow_delivery_of_overproduced_qty": 1})
|
||||
@IntegrationTestCase.change_settings(
|
||||
"Manufacturing Settings", {"overproduction_percentage_for_work_order": 20}
|
||||
)
|
||||
def test_over_production_delivery(self):
|
||||
so, scio = create_so_scio()
|
||||
frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit()
|
||||
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.submit()
|
||||
|
||||
manufacture = frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture"))
|
||||
manufacture.items[-1].qty = 6
|
||||
manufacture.fg_completed_qty = 6
|
||||
manufacture.submit()
|
||||
|
||||
scio.reload()
|
||||
self.assertEqual(scio.items[0].produced_qty, 6)
|
||||
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
self.assertEqual(delivery.items[0].qty, 6)
|
||||
delivery.submit()
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "allow_delivery_of_overproduced_qty", 0)
|
||||
delivery.cancel()
|
||||
scio.reload()
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
self.assertEqual(delivery.items[0].qty, 5)
|
||||
delivery.items[0].qty = 6
|
||||
self.assertRaises(frappe.ValidationError, delivery.submit)
|
||||
|
||||
@IntegrationTestCase.change_settings("Selling Settings", {"deliver_scrap_items": 1})
|
||||
def test_scrap_delivery(self):
|
||||
new_bom = frappe.copy_doc(frappe.get_doc("BOM", "BOM-Basic FG Item-001"))
|
||||
new_bom.scrap_items.append(frappe.new_doc("BOM Scrap Item", item_code="Basic RM 2", qty=1))
|
||||
new_bom.submit()
|
||||
sc_bom = frappe.get_doc("Subcontracting BOM", "SB-0001")
|
||||
sc_bom.finished_good_bom = new_bom.name
|
||||
sc_bom.save()
|
||||
|
||||
so, scio = create_so_scio()
|
||||
frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit()
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.submit()
|
||||
|
||||
frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture")).submit()
|
||||
|
||||
scio.reload()
|
||||
self.assertEqual(scio.scrap_items[0].item_code, "Basic RM 2")
|
||||
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
self.assertEqual(delivery.items[-1].item_code, "Basic RM 2")
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "deliver_scrap_items", 0)
|
||||
delivery = frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery())
|
||||
self.assertNotEqual(delivery.items[-1].item_code, "Basic RM 2")
|
||||
|
||||
def test_self_rm_billed_qty(self):
|
||||
so, scio = create_so_scio()
|
||||
frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()).submit()
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.submit()
|
||||
frappe.new_doc("Stock Entry").update(make_stock_entry_from_wo(wo.name, "Manufacture")).submit()
|
||||
scio.reload()
|
||||
frappe.new_doc("Stock Entry").update(scio.make_subcontracting_delivery()).submit()
|
||||
scio.reload()
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
self.assertEqual(si.items[-1].item_code, "Self RM")
|
||||
self.assertEqual(si.items[-1].qty, 5)
|
||||
si.items[-1].qty = 3
|
||||
si.submit()
|
||||
scio.reload()
|
||||
self.assertEqual(scio.received_items[-1].billed_qty, 3)
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
self.assertEqual(si.items[-1].qty, 2)
|
||||
si.submit()
|
||||
scio.reload()
|
||||
self.assertEqual(scio.received_items[-1].billed_qty, 5)
|
||||
|
||||
scio.reload()
|
||||
si = make_sales_invoice(so.name)
|
||||
self.assertEqual(len(si.items), 1)
|
||||
|
||||
def test_extra_items_reservation_transfer(self):
|
||||
so, scio = create_so_scio()
|
||||
rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward())
|
||||
rm_in.items[-2].qty = 7
|
||||
rm_in.submit()
|
||||
|
||||
wo_list = []
|
||||
scio.reload()
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.qty = 3
|
||||
wo.submit()
|
||||
wo_list.append(wo.name)
|
||||
self.assertEqual(wo.required_items[-2].stock_reserved_qty, 3)
|
||||
|
||||
scio.reload()
|
||||
self.assertEqual(scio.received_items[-2].work_order_qty, 3)
|
||||
|
||||
wo = frappe.get_doc("Work Order", scio.make_work_order()[0])
|
||||
wo.skip_transfer = 1
|
||||
wo.required_items[-1].source_warehouse = "Stores - _TC"
|
||||
wo.qty = 2
|
||||
wo.submit()
|
||||
wo_list.append(wo.name)
|
||||
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.reserved_qty))
|
||||
.where(
|
||||
(table.voucher_type == "Work Order")
|
||||
& (table.item_code == rm_in.items[-2].item_code)
|
||||
& (table.voucher_no.isin(wo_list))
|
||||
)
|
||||
)
|
||||
reserved_qty = query.run()[0][0]
|
||||
self.assertEqual(reserved_qty, 7)
|
||||
|
||||
|
||||
def create_so_scio(service_item="Service Item 1", fg_item="Basic FG Item"):
|
||||
item_list = [{"item_code": service_item, "qty": 5, "fg_item": fg_item, "fg_item_qty": 5}]
|
||||
so = make_sales_order(is_subcontracted=1, item_list=item_list)
|
||||
scio = make_subcontracting_inward_order(so.name)
|
||||
scio.items[0].delivery_warehouse = "_Test Warehouse - _TC"
|
||||
scio.submit()
|
||||
scio.reload()
|
||||
return so, scio
|
||||
|
||||
|
||||
def create_test_data():
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
make_subcontracting_boms()
|
||||
create_warehouse("_Test Customer Warehouse - _TC", {"customer": "_Test Customer"})
|
||||
|
||||
|
||||
def make_subcontracted_items():
|
||||
sub_contracted_items = {
|
||||
"Basic FG Item": {},
|
||||
"FG Item with Serial": {
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "FGS.####",
|
||||
},
|
||||
"FG Item with Batch": {
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_series": "FGB.####",
|
||||
},
|
||||
"FG Item with Serial and Batch": {
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "FGS.####",
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_series": "FGB.####",
|
||||
},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
if not frappe.db.exists("Item", item):
|
||||
properties.update({"is_stock_item": 1, "is_sub_contracted_item": 1})
|
||||
make_item(item, properties)
|
||||
|
||||
|
||||
def make_raw_materials():
|
||||
customer_provided_raw_materials = {
|
||||
"Basic RM": {},
|
||||
"Basic RM 2": {},
|
||||
"RM with Serial": {"has_serial_no": 1, "serial_no_series": "RMS.####"},
|
||||
"RM with Batch": {
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "RMB.####",
|
||||
},
|
||||
"RM with Serial and Batch": {
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "RMS.####",
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "RMB.####",
|
||||
},
|
||||
}
|
||||
|
||||
for item, properties in customer_provided_raw_materials.items():
|
||||
if not frappe.db.exists("Item", item):
|
||||
properties.update({"is_stock_item": 1, "is_purchase_item": 0, "is_customer_provided_item": 1})
|
||||
make_item(item, properties)
|
||||
|
||||
self_raw_materials = {
|
||||
"Self RM": {},
|
||||
"Self RM 2": {},
|
||||
}
|
||||
|
||||
for item, properties in self_raw_materials.items():
|
||||
if not frappe.db.exists("Item", item):
|
||||
properties.update({"is_stock_item": 1, "valuation_rate": 10})
|
||||
make_item(item, properties)
|
||||
|
||||
|
||||
def make_service_items():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import make_service_item
|
||||
|
||||
service_items = {
|
||||
"Service Item 1": {},
|
||||
"Service Item 2": {},
|
||||
"Service Item 3": {},
|
||||
"Service Item 4": {},
|
||||
}
|
||||
|
||||
for item, properties in service_items.items():
|
||||
make_service_item(item, properties)
|
||||
|
||||
|
||||
def make_bom_for_subcontracted_items():
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
boms = {
|
||||
"Basic FG Item": [
|
||||
"Basic RM",
|
||||
"RM with Serial",
|
||||
"RM with Batch",
|
||||
"RM with Serial and Batch",
|
||||
"Self RM",
|
||||
],
|
||||
"FG Item with Serial": [
|
||||
"Basic RM",
|
||||
"RM with Serial",
|
||||
"RM with Batch",
|
||||
"RM with Serial and Batch",
|
||||
"Self RM",
|
||||
],
|
||||
"FG Item with Batch": [
|
||||
"Basic RM",
|
||||
"RM with Serial",
|
||||
"RM with Batch",
|
||||
"RM with Serial and Batch",
|
||||
"Self RM",
|
||||
],
|
||||
"FG Item with Serial and Batch": [
|
||||
"Basic RM",
|
||||
"RM with Serial",
|
||||
"RM with Batch",
|
||||
"RM with Serial and Batch",
|
||||
"Self RM",
|
||||
],
|
||||
}
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
if not frappe.db.exists("BOM", {"item": item_code}):
|
||||
make_bom(
|
||||
item=item_code, raw_materials=raw_materials, rate=100, currency="INR", set_as_default_bom=1
|
||||
)
|
||||
|
||||
|
||||
def make_subcontracting_boms():
|
||||
subcontracting_boms = [
|
||||
{
|
||||
"finished_good": "Basic FG Item",
|
||||
"service_item": "Service Item 1",
|
||||
},
|
||||
{
|
||||
"finished_good": "FG Item with Serial",
|
||||
"service_item": "Service Item 2",
|
||||
},
|
||||
{
|
||||
"finished_good": "FG Item with Batch",
|
||||
"service_item": "Service Item 3",
|
||||
},
|
||||
{
|
||||
"finished_good": "FG Item with Serial and Batch",
|
||||
"service_item": "Service Item 4",
|
||||
},
|
||||
]
|
||||
|
||||
for subcontracting_bom in subcontracting_boms:
|
||||
if not frappe.db.exists("Subcontracting BOM", {"finished_good": subcontracting_bom["finished_good"]}):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Subcontracting BOM",
|
||||
"finished_good": subcontracting_bom["finished_good"],
|
||||
"service_item": subcontracting_bom["service_item"],
|
||||
"is_active": 1,
|
||||
}
|
||||
)
|
||||
doc.insert()
|
||||
doc.save()
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2025-03-24 12:53:33.849013",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"column_break_3",
|
||||
"bom",
|
||||
"delivery_warehouse",
|
||||
"include_exploded_items",
|
||||
"quantity_section",
|
||||
"qty",
|
||||
"produced_qty",
|
||||
"returned_qty",
|
||||
"column_break_13",
|
||||
"stock_uom",
|
||||
"process_loss_qty",
|
||||
"delivered_qty",
|
||||
"conversion_factor",
|
||||
"sales_order_item",
|
||||
"subcontracting_conversion_factor"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Item Name",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 1,
|
||||
"default": "1",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"print_width": "60px",
|
||||
"reqd": 1,
|
||||
"width": "60px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Conversion Factor",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "item_code",
|
||||
"fetch_from": "item_code.default_bom",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "BOM",
|
||||
"options": "BOM",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "include_exploded_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include Exploded Items",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delivered_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Delivered Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sales Order Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontracting_conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Subcontracting Conversion Factor",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Produced Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "quantity_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "delivery_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Delivery Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 10:29:29.256455",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
class SubcontractingInwardOrderItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
bom: DF.Link
|
||||
conversion_factor: DF.Float
|
||||
delivered_qty: DF.Float
|
||||
delivery_warehouse: DF.Link
|
||||
include_exploded_items: DF.Check
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
process_loss_qty: DF.Float
|
||||
produced_qty: DF.Float
|
||||
qty: DF.Float
|
||||
returned_qty: DF.Float
|
||||
sales_order_item: DF.Data | None
|
||||
stock_uom: DF.Link
|
||||
subcontracting_conversion_factor: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
def update_manufacturing_qty_fields(self):
|
||||
table = frappe.qb.DocType("Work Order")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
Sum(table.produced_qty).as_("produced_qty"),
|
||||
Sum(table.process_loss_qty).as_("process_loss_qty"),
|
||||
)
|
||||
.where((table.subcontracting_inward_order_item == self.name) & (table.docstatus == 1))
|
||||
)
|
||||
result = query.run(as_dict=True)[0]
|
||||
|
||||
self.db_set("produced_qty", result.produced_qty)
|
||||
self.db_set("process_loss_qty", result.process_loss_qty)
|
||||
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2025-03-24 13:56:42.877800",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"rm_item_code",
|
||||
"is_customer_provided_item",
|
||||
"is_additional_item",
|
||||
"column_break_3",
|
||||
"stock_uom",
|
||||
"warehouse",
|
||||
"column_break_6",
|
||||
"bom_detail_no",
|
||||
"reference_name",
|
||||
"section_break_13",
|
||||
"required_qty",
|
||||
"billed_qty",
|
||||
"received_qty",
|
||||
"column_break_16",
|
||||
"consumed_qty",
|
||||
"work_order_qty",
|
||||
"returned_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "main_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "rm_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Raw Material Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "BOM Detail No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.required_qty",
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_customer_provided_item",
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Received Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.returned_qty",
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.work_order_qty",
|
||||
"fieldname": "work_order_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Work Order Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_customer_provided_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Customer Provided Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_customer_provided_item",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_customer_provided_item",
|
||||
"fieldname": "billed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Billed Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.bom_detail_no",
|
||||
"fieldname": "is_additional_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Additional Item",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 10:18:58.905093",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order Received Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SubcontractingInwardOrderReceivedItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
billed_qty: DF.Float
|
||||
bom_detail_no: DF.Data | None
|
||||
consumed_qty: DF.Float
|
||||
is_additional_item: DF.Check
|
||||
is_customer_provided_item: DF.Check
|
||||
main_item_code: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
received_qty: DF.Float
|
||||
reference_name: DF.Data | None
|
||||
required_qty: DF.Float
|
||||
returned_qty: DF.Float
|
||||
rm_item_code: DF.Link
|
||||
stock_uom: DF.Link
|
||||
warehouse: DF.Link | None
|
||||
work_order_qty: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2025-08-12 11:34:16.393300",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"fg_item_code",
|
||||
"column_break_hoxe",
|
||||
"stock_uom",
|
||||
"warehouse",
|
||||
"column_break_rptg",
|
||||
"reference_name",
|
||||
"section_break_gqk9",
|
||||
"produced_qty",
|
||||
"column_break_n4xc",
|
||||
"delivered_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hoxe",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_rptg",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Name",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_gqk9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Produced Qty",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delivered_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Delivered Qty",
|
||||
"non_negative": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fg_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Finished Good Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_n4xc",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-14 10:28:30.192350",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order Scrap Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SubcontractingInwardOrderScrapItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
delivered_qty: DF.Float
|
||||
fg_item_code: DF.Link
|
||||
item_code: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
produced_qty: DF.Float
|
||||
reference_name: DF.Data
|
||||
stock_uom: DF.Link
|
||||
warehouse: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,147 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2025-03-24 14:01:02.572511",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"column_break_2",
|
||||
"item_name",
|
||||
"section_break_4",
|
||||
"qty",
|
||||
"uom",
|
||||
"column_break_6",
|
||||
"rate",
|
||||
"amount",
|
||||
"section_break_10",
|
||||
"fg_item",
|
||||
"column_break_12",
|
||||
"fg_item_qty",
|
||||
"sales_order_item"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Item Name",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 1,
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"print_width": "60px",
|
||||
"reqd": 1,
|
||||
"width": "60px"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 2,
|
||||
"fetch_from": "item_code.standard_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fg_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finished Good Item",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "fg_item_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Finished Good Item Quantity",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_10",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Sales Order Item",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-05 13:33:49.154869",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Inward Order Service Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SubcontractingInwardOrderServiceItem(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Currency
|
||||
fg_item: DF.Link
|
||||
fg_item_qty: DF.Float
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
sales_order_item: DF.Data | None
|
||||
uom: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -252,12 +252,12 @@ class SubcontractingOrder(SubcontractingController):
|
||||
if si.fg_item:
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
qty, subcontracted_quantity, fg_item_qty = frappe.db.get_value(
|
||||
qty, subcontracted_qty, fg_item_qty = frappe.db.get_value(
|
||||
"Purchase Order Item",
|
||||
si.purchase_order_item,
|
||||
["qty", "subcontracted_quantity", "fg_item_qty"],
|
||||
["qty", "subcontracted_qty", "fg_item_qty"],
|
||||
)
|
||||
available_qty = flt(qty) - flt(subcontracted_quantity)
|
||||
available_qty = flt(qty) - flt(subcontracted_qty)
|
||||
|
||||
if available_qty == 0:
|
||||
continue
|
||||
@@ -342,23 +342,23 @@ class SubcontractingOrder(SubcontractingController):
|
||||
|
||||
def update_subcontracted_quantity_in_po(self, cancel=False):
|
||||
for service_item in self.service_items:
|
||||
subcontracted_quantity = flt(
|
||||
subcontracted_qty = flt(
|
||||
frappe.db.get_value(
|
||||
"Purchase Order Item", service_item.purchase_order_item, "subcontracted_quantity"
|
||||
"Purchase Order Item", service_item.purchase_order_item, "subcontracted_qty"
|
||||
)
|
||||
)
|
||||
|
||||
subcontracted_quantity = (
|
||||
(subcontracted_quantity + service_item.qty)
|
||||
subcontracted_qty = (
|
||||
(subcontracted_qty + service_item.qty)
|
||||
if not cancel
|
||||
else (subcontracted_quantity - service_item.qty)
|
||||
else (subcontracted_qty - service_item.qty)
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Purchase Order Item",
|
||||
service_item.purchase_order_item,
|
||||
"subcontracted_quantity",
|
||||
subcontracted_quantity,
|
||||
"subcontracted_qty",
|
||||
subcontracted_qty,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user