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:
Mihir Kandoi
2025-10-14 15:00:49 +05:30
committed by GitHub
parent 9772ca75c4
commit f2b948a483
76 changed files with 4970 additions and 229 deletions

View File

@@ -54,7 +54,8 @@ frappe.ui.form.on("Sales Order", {
frm.doc.status !== "Closed" &&
flt(frm.doc.per_delivered) < 100 &&
flt(frm.doc.per_billed) < 100 &&
frm.has_perm("write")
frm.has_perm("write") &&
!frm.doc.is_subcontracted
) {
frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
@@ -84,7 +85,8 @@ frappe.ui.form.on("Sales Order", {
if (
frm.doc.__onload &&
frm.doc.__onload.has_reserved_stock &&
frappe.model.can_cancel("Stock Reservation Entry")
frappe.model.can_cancel("Stock Reservation Entry") &&
!frm.doc.is_subcontracted
) {
frm.add_custom_button(
__("Unreserve"),
@@ -93,16 +95,21 @@ frappe.ui.form.on("Sales Order", {
);
}
frm.doc.items.forEach((item) => {
if (flt(item.stock_reserved_qty) > 0 && frappe.model.can_read("Stock Reservation Entry")) {
frm.add_custom_button(
__("Reserved Stock"),
() => frm.events.show_reserved_stock(frm),
__("Stock Reservation")
);
return;
}
});
if (!frm.doc.is_subcontracted) {
frm.doc.items.forEach((item) => {
if (
flt(item.stock_reserved_qty) > 0 &&
frappe.model.can_read("Stock Reservation Entry")
) {
frm.add_custom_button(
__("Reserved Stock"),
() => frm.events.show_reserved_stock(frm),
__("Stock Reservation")
);
return;
}
});
}
}
if (frm.doc.docstatus === 0) {
@@ -112,7 +119,7 @@ frappe.ui.form.on("Sales Order", {
frm.events.get_items_from_internal_purchase_order(frm);
}
if (frm.doc.docstatus === 0) {
if (frm.doc.docstatus === 0 && !frm.doc.is_subcontracted) {
frappe.call({
method: "erpnext.selling.doctype.sales_order.sales_order.get_stock_reservation_status",
callback: function (r) {
@@ -749,10 +756,28 @@ frappe.ui.form.on("Sales Order", {
frm.schedule_dialog.fields_dict.delivery_schedule.refresh();
},
get_subcontracting_boms_for_finished_goods: function (fg_item) {
return frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom.get_subcontracting_boms_for_finished_goods",
args: {
fg_items: fg_item,
},
});
},
get_subcontracting_boms_for_service_item: function (service_item) {
return frappe.call({
method: "erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom.get_subcontracting_boms_for_service_item",
args: {
service_item: service_item,
},
});
},
});
frappe.ui.form.on("Sales Order Item", {
item_code: function (frm, cdt, cdn) {
item_code: async function (frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (frm.doc.delivery_date) {
row.delivery_date = frm.doc.delivery_date;
@@ -760,6 +785,50 @@ frappe.ui.form.on("Sales Order Item", {
} else {
frm.script_manager.copy_from_first_row("items", row, ["delivery_date"]);
}
if (frm.doc.is_subcontracted) {
if (row.item_code && !row.fg_item) {
var result = await frm.events.get_subcontracting_boms_for_service_item(row.item_code);
if (result.message && Object.keys(result.message).length) {
var finished_goods = Object.keys(result.message);
// Set FG if only one active Subcontracting BOM is found
if (finished_goods.length === 1) {
row.fg_item = result.message[finished_goods[0]].finished_good;
row.uom = result.message[finished_goods[0]].finished_good_uom;
refresh_field("items");
} else {
const dialog = new frappe.ui.Dialog({
title: __("Select Finished Good"),
size: "small",
fields: [
{
fieldname: "finished_good",
fieldtype: "Autocomplete",
label: __("Finished Good"),
options: finished_goods,
},
],
primary_action_label: __("Select"),
primary_action: () => {
var subcontracting_bom = result.message[dialog.get_value("finished_good")];
if (subcontracting_bom) {
row.fg_item = subcontracting_bom.finished_good;
row.uom = subcontracting_bom.finished_good_uom;
refresh_field("items");
}
dialog.hide();
},
});
dialog.show();
}
}
}
}
},
delivery_date: function (frm, cdt, cdn) {
@@ -782,6 +851,50 @@ frappe.ui.form.on("Sales Order Item", {
},
});
},
fg_item: async function (frm, cdt, cdn) {
if (frm.doc.is_subcontracted) {
var row = locals[cdt][cdn];
if (row.fg_item) {
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item);
if (result.message && Object.keys(result.message).length) {
frappe.model.set_value(cdt, cdn, "item_code", result.message.service_item);
frappe.model.set_value(
cdt,
cdn,
"qty",
flt(row.fg_item_qty) * flt(result.message.conversion_factor)
);
frappe.model.set_value(cdt, cdn, "uom", result.message.service_item_uom);
}
}
}
},
qty: async function (frm, cdt, cdn) {
if (frm.doc.is_subcontracted) {
var row = locals[cdt][cdn];
if (row.fg_item) {
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item);
if (
result.message &&
row.item_code == result.message.service_item &&
row.uom == result.message.service_item_uom
) {
frappe.model.set_value(
cdt,
cdn,
"fg_item_qty",
flt(row.qty) / flt(result.message.conversion_factor)
);
}
}
}
},
});
erpnext.selling.SalesOrderController = class SalesOrderController extends erpnext.selling.SellingController {
@@ -795,6 +908,22 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
let allow_delivery = false;
if (doc.docstatus == 1) {
if (
!["Closed", "Completed"].includes(doc.status) &&
flt(doc.per_delivered) < 100 &&
flt(doc.per_billed) < 100
) {
if (!doc.__onload || doc.__onload.can_update_items) {
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
child_docname: "items",
child_doctype: "Sales Order Detail",
cannot_add_row: false,
});
});
}
}
if (this.frm.has_perm("submit")) {
if (doc.status === "On Hold") {
// un-hold
@@ -847,11 +976,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
if (doc.is_subcontracted) {
if (!doc.items.every((item) => item.qty == item.subcontracted_qty)) {
this.frm.add_custom_button(
__("Subcontracting Inward Order"),
() => {
me.make_subcontracting_inward_order();
},
__("Create")
);
}
}
if (
(!doc.__onload || !doc.__onload.has_reserved_stock) &&
flt(doc.per_picked) < 100 &&
flt(doc.per_delivered) < 100 &&
frappe.model.can_create("Pick List")
frappe.model.can_create("Pick List") &&
!doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Pick List"),
@@ -880,7 +1022,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
);
}
if (frappe.model.can_create("Work Order")) {
if (frappe.model.can_create("Work Order") && !doc.is_subcontracted) {
this.frm.add_custom_button(
__("Work Order"),
() => this.make_work_order(),
@@ -890,7 +1032,10 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// sales invoice
if (flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) {
if (
(flt(doc.per_billed) < 100 && frappe.model.can_create("Sales Invoice")) ||
doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Sales Invoice"),
() => me.make_sales_invoice(),
@@ -902,13 +1047,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
if (
(!doc.order_type ||
((order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered) < 100)) &&
frappe.model.can_create("Material Request")
frappe.model.can_create("Material Request") &&
!doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Material Request"),
() => this.make_material_request(),
__("Create")
);
if (!doc.is_subcontracted) {
this.frm.add_custom_button(
__("Material Request"),
() => this.make_material_request(),
__("Create")
);
}
this.frm.add_custom_button(
__("Request for Raw Materials"),
() => this.make_raw_material_request(),
@@ -917,7 +1065,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// Make Purchase Order
if (!this.frm.doc.is_internal_customer && frappe.model.can_create("Purchase Order")) {
if (
!this.frm.doc.is_internal_customer &&
frappe.model.can_create("Purchase Order") &&
!doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Purchase Order"),
() => this.make_purchase_order(),
@@ -991,7 +1143,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
}
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Quotation")) {
if (
this.frm.doc.docstatus === 0 &&
frappe.model.can_read("Quotation") &&
!this.frm.doc.is_subcontracted
) {
this.frm.add_custom_button(
__("Quotation"),
function () {
@@ -1606,6 +1762,14 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
},
});
}
make_subcontracting_inward_order() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_subcontracting_inward_order",
frm: this.frm,
freeze_message: __("Creating Subcontracting Inward Order ..."),
});
}
};
extend_cscript(cur_frm.cscript, new erpnext.selling.SalesOrderController({ frm: cur_frm }));

View File

@@ -25,6 +25,7 @@
"company",
"skip_delivery_note",
"has_unit_price_items",
"is_subcontracted",
"amended_from",
"accounting_dimensions_section",
"cost_center",
@@ -1035,8 +1036,8 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "packed_items",
"depends_on": "packed_items",
"collapsible_depends_on": "eval:!doc.is_subcontracted && doc.packed_items",
"depends_on": "eval:!doc.is_subcontracted && doc.packed_items",
"fieldname": "packing_list",
"fieldtype": "Section Break",
"hide_days": 1,
@@ -1607,7 +1608,7 @@
},
{
"default": "0",
"depends_on": "eval: (doc.docstatus == 0 || doc.reserve_stock)",
"depends_on": "eval: ((doc.docstatus == 0 || doc.reserve_stock) && !doc.is_subcontracted)",
"description": "If checked, Stock will be reserved on <b>Submit</b>",
"fieldname": "reserve_stock",
"fieldtype": "Check",
@@ -1688,13 +1689,21 @@
"fieldtype": "Data",
"is_virtual": 1,
"label": "Last Scanned Warehouse"
},
{
"default": "0",
"fieldname": "is_subcontracted",
"fieldtype": "Check",
"label": "Is Subcontracted",
"print_hide": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2025-07-28 12:14:29.760988",
"modified": "2025-10-12 12:14:29.760988",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -120,6 +120,7 @@ class SalesOrder(SellingController):
incoterm: DF.Link | None
inter_company_order_reference: DF.Link | None
is_internal_customer: DF.Check
is_subcontracted: DF.Check
items: DF.Table[SalesOrderItem]
language: DF.Link | None
letter_head: DF.Link | None
@@ -195,6 +196,10 @@ class SalesOrder(SellingController):
def onload(self) -> None:
super().onload()
if self.get("is_subcontracted"):
self.set_onload("can_update_items", self.can_update_items())
return
if frappe.get_single_value("Stock Settings", "enable_stock_reservation"):
if self.has_unreserved_stock():
self.set_onload("has_unreserved_stock", True)
@@ -202,6 +207,15 @@ class SalesOrder(SellingController):
if has_reserved_stock(self.doctype, self.name):
self.set_onload("has_reserved_stock", True)
def can_update_items(self) -> bool:
result = True
if self.is_subcontracted:
if frappe.db.exists("Subcontracting Inward Order", {"sales_order": self.name, "docstatus": 1}):
result = False
return result
def before_validate(self):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
@@ -233,6 +247,7 @@ class SalesOrder(SellingController):
make_packing_list(self)
self.validate_with_previous_doc()
self.validate_fg_item_for_subcontracting()
self.set_status()
if not self.billing_status:
@@ -243,7 +258,39 @@ class SalesOrder(SellingController):
self.advance_payment_status = "Not Requested"
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.enable_auto_reserve_stock()
if not self.get("is_subcontracted"):
self.enable_auto_reserve_stock()
def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted:
for item in self.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
item.idx, item.fg_item
)
)
if not frappe.db.get_value(
"Subcontracting BOM",
{"finished_good": item.fg_item, "is_active": 1},
"finished_good_bom",
) and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
else:
for item in self.items:
item.set("fg_item", None)
item.set("fg_item_qty", 0)
def enable_auto_reserve_stock(self):
if self.is_new() and frappe.get_single_value("Stock Settings", "auto_reserve_stock"):
@@ -449,7 +496,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code, "used")
if self.get("reserve_stock"):
if self.get("reserve_stock") and not self.get("is_subcontracted"):
self.create_stock_reservation_entries()
def on_cancel(self):
@@ -535,9 +582,23 @@ class SalesOrder(SellingController):
if status == "Draft" and self.docstatus == 1:
self.check_credit_limit()
self.update_reserved_qty()
self.update_subcontracting_order_status()
self.notify_update()
clear_doctype_notifications(self)
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
update_subcontracting_inward_order_status as update_scio_status,
)
if self.is_subcontracted:
scio = frappe.get_cached_value(
"Subcontracting Inward Order", {"sales_order": self.name, "docstatus": 1}, "name"
)
if scio:
update_scio_status(scio, "Closed" if self.status == "Closed" else None)
def update_reserved_qty(self, so_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
item_wh_list = []
@@ -1290,6 +1351,46 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
child_filter = d.name in filtered_items if filtered_items else True
return child_filter
def add_self_rm(doclist):
parent = frappe.qb.DocType("Subcontracting Inward Order")
child = frappe.qb.DocType("Subcontracting Inward Order Received Item")
query = (
frappe.qb.from_(parent)
.join(child)
.on(parent.name == child.parent)
.select(
child.required_qty,
child.consumed_qty,
(child.billed_qty - child.returned_qty).as_("qty"),
child.rm_item_code,
child.stock_uom,
child.name,
)
.where(
(parent.docstatus == 1)
& (parent.sales_order == source_name)
& (child.is_customer_provided_item == 0)
)
)
result = query.run(as_dict=True)
if result:
idx = len(doclist.items) + 1
for item in result:
if (qty := max(item.required_qty, item.consumed_qty) - item.qty) > 0:
doclist.append(
"items",
{
"item_code": item.rm_item_code,
"qty": qty,
"uom": item.stock_uom,
"scio_detail": item.name,
},
)
doclist.process_item_selection(idx)
idx += 1
doclist.has_subcontracted = 1
doclist = get_mapped_doc(
"Sales Order",
source_name,
@@ -1328,6 +1429,9 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False, a
ignore_permissions=ignore_permissions,
)
if frappe.get_cached_value("Sales Order", source_name, "is_subcontracted"):
add_self_rm(doclist)
automatically_fetch_payment_terms = cint(
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
)
@@ -2005,3 +2109,71 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
@frappe.whitelist()
def get_stock_reservation_status():
return frappe.get_single_value("Stock Settings", "enable_stock_reservation")
@frappe.whitelist()
def make_subcontracting_inward_order(source_name, target_doc=None):
if not is_so_fully_subcontracted(source_name):
return get_mapped_subcontracting_inward_order(source_name, target_doc)
else:
frappe.throw(_("This Sales Order has been fully subcontracted."))
def is_so_fully_subcontracted(so_name):
table = frappe.qb.DocType("Sales Order Item")
query = (
frappe.qb.from_(table)
.select(table.name)
.where((table.parent == so_name) & (table.qty != table.subcontracted_qty))
)
return not query.run(as_dict=True)
def get_mapped_subcontracting_inward_order(source_name, target_doc=None):
def post_process(source_doc, target_doc):
if (
frappe.db.count(
"Warehouse", {"customer": source_doc.customer, "disabled": 0, "is_rejected_warehouse": 0}
)
== 1
):
target_doc.customer_warehouse = frappe.get_cached_value(
"Warehouse",
{"customer": source_doc.customer, "disabled": 0, "is_rejected_warehouse": 0},
"name",
)
target_doc.populate_items_table()
if target_doc and isinstance(target_doc, str):
target_doc = json.loads(target_doc)
for key in ["service_items", "items", "received_items"]:
if key in target_doc:
del target_doc[key]
target_doc = json.dumps(target_doc)
target_doc = get_mapped_doc(
"Sales Order",
source_name,
{
"Sales Order": {
"doctype": "Subcontracting Inward Order",
"field_map": {},
"field_no_map": ["total_qty", "total", "net_total"],
"validation": {
"docstatus": ["=", 1],
},
},
"Sales Order Item": {
"doctype": "Subcontracting Inward Order Service Item",
"field_map": {
"name": "sales_order_item",
},
"field_no_map": ["qty", "fg_item_qty", "amount"],
"condition": lambda item: item.qty != item.subcontracted_qty,
},
},
target_doc,
post_process,
)
return target_doc

View File

@@ -30,5 +30,6 @@ def get_data():
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat", "Stock Reservation Entry"]},
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
{"label": _("Schedule"), "items": ["Delivery Schedule Item"]},
{"label": _("Subcontracting Inward"), "items": ["Subcontracting Inward Order"]},
],
}

View File

@@ -2613,6 +2613,7 @@ def make_sales_order(**args):
so.customer = args.customer or "_Test Customer"
so.currency = args.currency or "INR"
so.po_no = args.po_no or ""
so.is_subcontracted = args.is_subcontracted or 0
if args.selling_price_list:
so.selling_price_list = args.selling_price_list

View File

@@ -7,6 +7,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"fg_item",
"fg_item_qty",
"item_code",
"customer_item_code",
"ensure_delivery_based_on_produced_serial_no",
@@ -25,6 +27,7 @@
"quantity_and_rate",
"qty",
"stock_uom",
"subcontracted_qty",
"col_break2",
"uom",
"conversion_factor",
@@ -468,6 +471,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.delivered_by_supplier==1||doc.supplier",
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "drop_ship_section",
"fieldtype": "Section Break",
"label": "Drop Ship",
@@ -490,6 +494,7 @@
},
{
"collapsible": 1,
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "item_weight_details",
"fieldtype": "Section Break",
"label": "Item Weight Details"
@@ -517,6 +522,7 @@
"options": "UOM"
},
{
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "warehouse_and_reference",
"fieldtype": "Section Break",
"label": "Warehouse and Reference"
@@ -879,7 +885,7 @@
{
"allow_on_submit": 1,
"default": "1",
"depends_on": "eval:doc.is_stock_item",
"depends_on": "eval:(doc.is_stock_item && !parent.is_subcontracted)",
"fieldname": "reserve_stock",
"fieldtype": "Check",
"label": "Reserve Stock",
@@ -935,6 +941,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!parent.is_subcontracted",
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
"label": "Available Quantity"
@@ -977,12 +984,39 @@
"fieldname": "add_schedule",
"fieldtype": "Button",
"label": "Add Schedule"
},
{
"allow_on_submit": 1,
"default": "0",
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "subcontracted_qty",
"fieldtype": "Float",
"label": "Subcontracted Quantity",
"no_copy": 1,
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good",
"mandatory_depends_on": "eval:parent.is_subcontracted",
"options": "Item"
},
{
"depends_on": "eval:parent.is_subcontracted",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Qty",
"mandatory_depends_on": "eval:parent.is_subcontracted"
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-08-21 17:01:54.269105",
"modified": "2025-10-13 10:57:43.378448",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",

View File

@@ -42,6 +42,8 @@ class SalesOrderItem(Document):
discount_percentage: DF.Percent
distributed_discount_amount: DF.Currency
ensure_delivery_based_on_produced_serial_no: DF.Check
fg_item: DF.Link | None
fg_item_qty: DF.Float
grant_commission: DF.Check
gross_profit: DF.Currency
image: DF.Attach | None
@@ -84,6 +86,7 @@ class SalesOrderItem(Document):
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
stock_uom_rate: DF.Currency
subcontracted_qty: DF.Float
supplier: DF.Link | None
target_warehouse: DF.Link | None
total_weight: DF.Float

View File

@@ -11,6 +11,7 @@
"customer_group",
"column_break_4",
"territory",
"item_price_tab",
"item_price_settings_section",
"selling_price_list",
"maintain_same_rate_action",
@@ -22,6 +23,7 @@
"validate_selling_price",
"editable_bundle_item_rates",
"allow_negative_rates_for_items",
"transaction_tab",
"sales_transactions_settings_section",
"so_required",
"dn_required",
@@ -38,7 +40,12 @@
"allow_zero_qty_in_quotation",
"allow_zero_qty_in_sales_order",
"experimental_section",
"use_legacy_js_reactivity"
"use_legacy_js_reactivity",
"subcontracting_inward_tab",
"section_break_zwh6",
"allow_delivery_of_overproduced_qty",
"column_break_mla9",
"deliver_scrap_items"
],
"fields": [
{
@@ -232,6 +239,44 @@
"fieldtype": "Check",
"label": "Allow Quotation with Zero Quantity"
},
{
"fieldname": "section_break_zwh6",
"fieldtype": "Section Break",
"label": "Subcontracting Inward Settings"
},
{
"default": "0",
"description": "If enabled, system will allow user to deliver the entire quantity of the finished goods produced against the Subcontracting Inward Order. If disabled, system will allow delivery of only the ordered quantity.",
"fieldname": "allow_delivery_of_overproduced_qty",
"fieldtype": "Check",
"label": "Allow Delivery of Overproduced Qty"
},
{
"fieldname": "column_break_mla9",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, the Scrap Item generated against a Finished Good will also be added in the Stock Entry when delivering that Finished Good.",
"fieldname": "deliver_scrap_items",
"fieldtype": "Check",
"label": "Deliver Scrap Items"
},
{
"fieldname": "item_price_tab",
"fieldtype": "Tab Break",
"label": "Item Price"
},
{
"fieldname": "transaction_tab",
"fieldtype": "Tab Break",
"label": "Transaction"
},
{
"fieldname": "subcontracting_inward_tab",
"fieldtype": "Tab Break",
"label": "Subcontracting Inward"
},
{
"default": "0",
"fieldname": "fallback_to_default_price_list",
@@ -251,7 +296,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-09-24 16:08:48.865885",
"modified": "2025-10-12 16:08:48.865885",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",

View File

@@ -21,6 +21,7 @@ class SellingSettings(Document):
from frappe.types import DF
allow_against_multiple_purchase_orders: DF.Check
allow_delivery_of_overproduced_qty: DF.Check
allow_multiple_items: DF.Check
allow_negative_rates_for_items: DF.Check
allow_sales_order_creation_for_expired_quotation: DF.Check
@@ -29,6 +30,7 @@ class SellingSettings(Document):
blanket_order_allowance: DF.Float
cust_master_name: DF.Literal["Customer Name", "Naming Series", "Auto Name"]
customer_group: DF.Link | None
deliver_scrap_items: DF.Check
dn_required: DF.Literal["No", "Yes"]
dont_reserve_sales_order_qty_on_sales_return: DF.Check
editable_bundle_item_rates: DF.Check