mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-13 03:45:08 +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:
@@ -256,6 +256,18 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
sales_order_btn() {
|
||||
var me = this;
|
||||
|
||||
let filters = {
|
||||
docstatus: 1,
|
||||
status: ["not in", ["Closed", "On Hold"]],
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
};
|
||||
|
||||
if (me.frm.doc.has_subcontracted) {
|
||||
filters.is_subcontracted = 1;
|
||||
}
|
||||
|
||||
this.$sales_order_btn = this.frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
function () {
|
||||
@@ -266,12 +278,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
setters: {
|
||||
customer: me.frm.doc.customer || undefined,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
status: ["not in", ["Closed", "On Hold"]],
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
get_query_filters: filters,
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
@@ -1094,6 +1101,9 @@ frappe.ui.form.on("Sales Invoice", {
|
||||
if (frm.doc.is_debit_note) {
|
||||
frm.set_df_property("return_against", "label", __("Adjustment Against"));
|
||||
}
|
||||
|
||||
frm.set_df_property("update_stock", "read_only", frm.doc.has_subcontracted);
|
||||
frm.toggle_display("update_stock", !frm.doc.has_subcontracted);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"amended_from",
|
||||
"is_created_using_pos",
|
||||
"pos_closing_entry",
|
||||
"has_subcontracted",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -2229,6 +2230,14 @@
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_subcontracted",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Has Subcontracted",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -2242,7 +2251,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-09-09 14:48:59.472826",
|
||||
"modified": "2025-10-09 14:48:59.472826",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe import _, msgprint, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder import Case
|
||||
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
@@ -31,7 +32,6 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
@@ -131,6 +131,7 @@ class SalesInvoice(SellingController):
|
||||
from_date: DF.Date | None
|
||||
grand_total: DF.Currency
|
||||
group_same_items: DF.Check
|
||||
has_subcontracted: DF.Check
|
||||
ignore_default_payment_terms_template: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
in_words: DF.SmallText | None
|
||||
@@ -336,6 +337,8 @@ class SalesInvoice(SellingController):
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
|
||||
self.is_subcontracted()
|
||||
|
||||
if not (self.is_pos or self.is_debit_note):
|
||||
self.so_dn_required()
|
||||
|
||||
@@ -408,6 +411,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.allow_write_off_only_on_pos()
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.validate_subcontracted_sales_order()
|
||||
self.validate_scio_self_rm_qty()
|
||||
|
||||
def validate_accounts(self):
|
||||
self.validate_write_off_account()
|
||||
@@ -574,6 +579,7 @@ class SalesInvoice(SellingController):
|
||||
self.apply_loyalty_points()
|
||||
|
||||
self.process_common_party_accounting()
|
||||
self.update_billed_qty_in_scio()
|
||||
|
||||
def validate_pos_return(self):
|
||||
if self.is_consolidated:
|
||||
@@ -704,6 +710,8 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
self.cancel_pos_invoice_credit_note_generated_during_sales_invoice_mode()
|
||||
|
||||
self.update_billed_qty_in_scio()
|
||||
|
||||
def update_status_updater_args(self):
|
||||
if not cint(self.update_stock):
|
||||
return
|
||||
@@ -838,6 +846,26 @@ class SalesInvoice(SellingController):
|
||||
timesheet.set_status()
|
||||
timesheet.db_update_all()
|
||||
|
||||
def update_billed_qty_in_scio(self):
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
fieldname = table.returned_qty if self.is_return else table.billed_qty
|
||||
|
||||
data = frappe._dict(
|
||||
{
|
||||
item.scio_detail: item.stock_qty if self._action == "submit" else -item.stock_qty
|
||||
for item in self.items
|
||||
if item.scio_detail
|
||||
}
|
||||
)
|
||||
|
||||
if data:
|
||||
case_expr = Case()
|
||||
for name, qty in data.items():
|
||||
case_expr = case_expr.when(table.name == name, fieldname + qty)
|
||||
frappe.qb.update(table).set(fieldname, case_expr).where(
|
||||
(table.name.isin(list(data.keys()))) & (table.docstatus == 1)
|
||||
).run()
|
||||
|
||||
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
|
||||
for data in timesheet.time_logs:
|
||||
if (
|
||||
@@ -1230,6 +1258,50 @@ class SalesInvoice(SellingController):
|
||||
if not self.is_pos and self.write_off_account:
|
||||
self.write_off_account = None
|
||||
|
||||
def validate_subcontracted_sales_order(self):
|
||||
if self.has_subcontracted:
|
||||
if [item for item in self.items if not item.sales_order and not item.scio_detail]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"All items must be linked to a Sales Order or Subcontracting Inward Order for this Sales Invoice."
|
||||
)
|
||||
)
|
||||
if not all(
|
||||
frappe.get_all(
|
||||
"Sales Order",
|
||||
{"name": ["in", [item.sales_order for item in self.items if item.sales_order]]},
|
||||
pluck="is_subcontracted",
|
||||
)
|
||||
):
|
||||
frappe.throw(_("All linked Sales Orders must be subcontracted."))
|
||||
|
||||
def validate_scio_self_rm_qty(self):
|
||||
self_rms = [item for item in self.items if item.scio_detail]
|
||||
if self_rms:
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(
|
||||
table.required_qty, table.consumed_qty, table.billed_qty, table.returned_qty, table.name
|
||||
)
|
||||
.where((table.docstatus == 1) & (table.name.isin([item.scio_detail for item in self_rms])))
|
||||
)
|
||||
result = query.run(as_dict=True)
|
||||
data = {item.name: item for item in result}
|
||||
for item in self_rms:
|
||||
row = data.get(item.scio_detail)
|
||||
max_qty = max(row.required_qty, row.consumed_qty) - row.billed_qty - row.returned_qty
|
||||
if item.stock_qty > max_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Stock quantity {1} ({2}) for item {3} cannot exceed {4}").format(
|
||||
item.idx,
|
||||
item.stock_qty,
|
||||
item.stock_uom,
|
||||
frappe.bold(item.item_code),
|
||||
frappe.bold(max_qty),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_write_off_account(self):
|
||||
if flt(self.write_off_amount) and not self.write_off_account:
|
||||
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
|
||||
@@ -2151,6 +2223,23 @@ class SalesInvoice(SellingController):
|
||||
if update:
|
||||
self.db_set("status", self.status, update_modified=update_modified)
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracted(self):
|
||||
if not self.has_subcontracted:
|
||||
self.has_subcontracted = bool(
|
||||
frappe.get_cached_value(
|
||||
"Sales Order",
|
||||
{
|
||||
"name": ["in", [item.sales_order for item in self.items if item.sales_order]],
|
||||
"is_subcontracted": 1,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
)
|
||||
if self.has_subcontracted:
|
||||
self.update_stock = 0
|
||||
return self.has_subcontracted
|
||||
|
||||
|
||||
def get_total_in_party_account_currency(doc):
|
||||
total_fieldname = "grand_total" if doc.disable_rounded_total else "rounded_total"
|
||||
@@ -2352,7 +2441,7 @@ def make_delivery_note(source_name, target_doc=None):
|
||||
"cost_center": "cost_center",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
|
||||
},
|
||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||
"Sales Team": {
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"column_break_vwhb",
|
||||
"pos_invoice",
|
||||
"pos_invoice_item",
|
||||
"scio_detail",
|
||||
"internal_transfer_section",
|
||||
"purchase_order",
|
||||
"column_break_92",
|
||||
@@ -978,13 +979,20 @@
|
||||
"options": "POS Invoice",
|
||||
"print_hide": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scio_detail",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "SCIO Detail",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-07 10:25:30.275246",
|
||||
"modified": "2025-09-04 11:08:25.583561",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -995,4 +1003,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ class SalesInvoiceItem(Document):
|
||||
rate_with_margin: DF.Currency
|
||||
sales_invoice_item: DF.Data | None
|
||||
sales_order: DF.Link | None
|
||||
scio_detail: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
service_end_date: DF.Date | None
|
||||
|
||||
@@ -435,7 +435,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!doc.items.every((item) => item.qty == item.subcontracted_quantity)) {
|
||||
if (!doc.items.every((item) => item.qty == item.subcontracted_qty)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Subcontracting Order"),
|
||||
() => {
|
||||
|
||||
@@ -972,7 +972,7 @@ def make_subcontracting_order(source_name, target_doc=None, save=False, submit=F
|
||||
|
||||
return target_doc
|
||||
else:
|
||||
frappe.throw(_("This PO has been fully subcontracted."))
|
||||
frappe.throw(_("This Purchase Order has been fully subcontracted."))
|
||||
|
||||
|
||||
def is_po_fully_subcontracted(po_name):
|
||||
@@ -980,7 +980,7 @@ def is_po_fully_subcontracted(po_name):
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name)
|
||||
.where((table.parent == po_name) & (table.qty != table.subcontracted_quantity))
|
||||
.where((table.parent == po_name) & (table.qty != table.subcontracted_qty))
|
||||
)
|
||||
return not query.run(as_dict=True)
|
||||
|
||||
@@ -1035,7 +1035,7 @@ def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
"material_request_item": "material_request_item",
|
||||
},
|
||||
"field_no_map": ["qty", "fg_item_qty", "amount"],
|
||||
"condition": lambda item: item.qty != item.subcontracted_quantity,
|
||||
"condition": lambda item: item.qty != item.subcontracted_qty,
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
@@ -1095,9 +1095,9 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
|
||||
# Test - 2: Subcontracted Quantity for the PO Items of each line item should be updated accordingly
|
||||
po.reload()
|
||||
self.assertEqual(po.items[0].subcontracted_quantity, 5)
|
||||
self.assertEqual(po.items[1].subcontracted_quantity, 0)
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
|
||||
self.assertEqual(po.items[0].subcontracted_qty, 5)
|
||||
self.assertEqual(po.items[1].subcontracted_qty, 0)
|
||||
self.assertEqual(po.items[2].subcontracted_qty, 12.5)
|
||||
|
||||
# Test - 3: Amount for both FG Item and its Service Item should be updated correctly based on change in Quantity
|
||||
self.assertEqual(sco.items[0].amount, 2000)
|
||||
@@ -1133,10 +1133,10 @@ class TestPurchaseOrder(IntegrationTestCase):
|
||||
|
||||
# Test - 8: Subcontracted Quantity for each PO Item should be subtracted if SCO gets cancelled
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 25)
|
||||
self.assertEqual(po.items[2].subcontracted_qty, 25)
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
self.assertEqual(po.items[2].subcontracted_quantity, 12.5)
|
||||
self.assertEqual(po.items[2].subcontracted_qty, 12.5)
|
||||
|
||||
sco = make_subcontracting_order(po.name)
|
||||
sco.save()
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"quantity_and_rate",
|
||||
"qty",
|
||||
"stock_uom",
|
||||
"subcontracted_quantity",
|
||||
"subcontracted_qty",
|
||||
"col_break2",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
@@ -933,8 +933,9 @@
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
|
||||
"fieldname": "subcontracted_quantity",
|
||||
"fieldname": "subcontracted_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Subcontracted Quantity",
|
||||
"no_copy": 1,
|
||||
@@ -947,7 +948,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-13 17:27:43.468602",
|
||||
"modified": "2025-10-12 10:57:31.552812",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -85,7 +85,7 @@ class PurchaseOrderItem(Document):
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link
|
||||
stock_uom_rate: DF.Currency
|
||||
subcontracted_quantity: DF.Float
|
||||
subcontracted_qty: DF.Float
|
||||
supplier_part_no: DF.Data | None
|
||||
supplier_quotation: DF.Link | None
|
||||
supplier_quotation_item: DF.Link | None
|
||||
|
||||
@@ -3804,9 +3804,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
any_qty_changed = True
|
||||
|
||||
if (
|
||||
parent.doctype == "Purchase Order"
|
||||
parent.doctype in ["Sales Order", "Purchase Order"]
|
||||
and parent.is_subcontracted
|
||||
and not parent.is_old_subcontracting_flow
|
||||
and not parent.get("is_old_subcontracting_flow")
|
||||
):
|
||||
validate_fg_item_for_subcontracting(d, new_child_flag)
|
||||
child_item.fg_item_qty = flt(d["fg_item_qty"])
|
||||
@@ -3891,7 +3891,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.set_qty_as_per_stock_uom()
|
||||
parent.calculate_taxes_and_totals()
|
||||
parent.set_total_in_words()
|
||||
if parent_doctype == "Sales Order":
|
||||
if parent_doctype == "Sales Order" and not parent.is_subcontracted:
|
||||
make_packing_list(parent)
|
||||
parent.set_gross_profit()
|
||||
frappe.get_cached_doc("Authorization Control").validate_approving_authority(
|
||||
@@ -3938,6 +3938,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
if parent.is_subcontracted and not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Inward Order is created against the Sales Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
@@ -3957,7 +3963,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
# Cancel and Recreate Stock Reservation Entries.
|
||||
if parent_doctype == "Sales Order":
|
||||
if parent_doctype == "Sales Order" and not parent.is_subcontracted:
|
||||
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
|
||||
cancel_stock_reservation_entries,
|
||||
has_reserved_stock,
|
||||
|
||||
@@ -95,9 +95,11 @@ class StockController(AccountsController):
|
||||
"Stock Reconciliation",
|
||||
]:
|
||||
for item in self.get("items"):
|
||||
if (item.get("valuation_rate") == 0 or item.get("incoming_rate") == 0) and item.get(
|
||||
"allow_zero_valuation_rate"
|
||||
) == 0:
|
||||
if (
|
||||
(item.get("valuation_rate") == 0 or item.get("incoming_rate") == 0)
|
||||
and item.get("allow_zero_valuation_rate") == 0
|
||||
and frappe.get_cached_value("Item", item.item_code, "is_stock_item")
|
||||
):
|
||||
frappe.toast(
|
||||
_(
|
||||
"Row #{0}: Item {1} has zero rate but 'Allow Zero Valuation Rate' is not enabled."
|
||||
@@ -544,10 +546,14 @@ class StockController(AccountsController):
|
||||
break
|
||||
|
||||
elif row.batch_no:
|
||||
batches = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": row.serial_and_batch_bundle}
|
||||
batches = sorted(
|
||||
frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": row.serial_and_batch_bundle, "batch_no": ("is", "set")},
|
||||
pluck="batch_no",
|
||||
distinct=True,
|
||||
)
|
||||
)
|
||||
batches = sorted([d.batch_no for d in batches])
|
||||
|
||||
if batches != [row.batch_no]:
|
||||
throw_error = True
|
||||
|
||||
@@ -35,6 +35,14 @@ class SubcontractingController(StockController):
|
||||
"order_supplied_items_field": "Purchase Order Item Supplied",
|
||||
}
|
||||
)
|
||||
elif self.doctype == "Subcontracting Inward Order":
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
"order_doctype": "Subcontracting Inward Order",
|
||||
"order_field": "subcontracting_inward_order",
|
||||
"rm_detail_field": "scio_detail",
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
@@ -47,14 +55,22 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
def before_validate(self):
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
|
||||
if self.doctype in [
|
||||
"Subcontracting Order",
|
||||
"Subcontracting Inward Order",
|
||||
"Subcontracting Receipt",
|
||||
]:
|
||||
self.remove_empty_rows()
|
||||
self.set_items_conversion_factor()
|
||||
|
||||
def validate(self):
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt", "Subcontracting Inward Order"]:
|
||||
self.validate_items()
|
||||
self.create_raw_materials_supplied()
|
||||
self.create_raw_materials_supplied_or_received(
|
||||
raw_material_table="supplied_items"
|
||||
if self.doctype != "Subcontracting Inward Order"
|
||||
else "received_items"
|
||||
)
|
||||
self.set_valuation_rate_for_rm()
|
||||
else:
|
||||
super().validate()
|
||||
@@ -109,7 +125,7 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
|
||||
def remove_empty_rows(self):
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
for key in ["service_items", "items", "supplied_items", "received_items"]:
|
||||
if self.get(key):
|
||||
idx = 1
|
||||
for item in self.get(key)[:]:
|
||||
@@ -133,33 +149,47 @@ class SubcontractingController(StockController):
|
||||
if not is_stock_item:
|
||||
frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Inward Order"
|
||||
and item.delivery_warehouse == self.customer_warehouse
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Delivery Warehouse cannot be same as Customer Warehouse for Item {1}."
|
||||
).format(item.idx, frappe.bold(item.item_name))
|
||||
)
|
||||
|
||||
if not item.get("is_scrap_item"):
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
)
|
||||
|
||||
if (
|
||||
self.doctype == "Subcontracting Order" and not item.subcontracting_conversion_factor
|
||||
): # this condition will only be true if user has recently updated from develop branch
|
||||
service_item_qty = frappe.get_value(
|
||||
"Subcontracting Order Service Item",
|
||||
filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
|
||||
fieldname=["qty"],
|
||||
if self.doctype != "Subcontracting Receipt" and item.qty > flt(
|
||||
get_pending_subcontracted_quantity(
|
||||
self.doctype,
|
||||
self.purchase_order if self.doctype == "Subcontracting Order" else self.sales_order,
|
||||
).get(
|
||||
item.purchase_order_item
|
||||
if self.doctype == "Subcontracting Order"
|
||||
else item.sales_order_item
|
||||
)
|
||||
item.subcontracting_conversion_factor = service_item_qty / item.qty
|
||||
|
||||
if self.doctype not in "Subcontracting Receipt" and item.qty > flt(
|
||||
get_pending_subcontracted_quantity(self.purchase_order).get(item.purchase_order_item)
|
||||
/ item.subcontracting_conversion_factor,
|
||||
frappe.get_precision("Purchase Order Item", "qty"),
|
||||
frappe.get_precision(
|
||||
"Purchase Order Item"
|
||||
if self.doctype == "Subcontracting Order"
|
||||
else "Sales Order Item",
|
||||
"qty",
|
||||
),
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Item {1}'s quantity cannot be higher than the available quantity."
|
||||
).format(item.idx, item.item_name)
|
||||
)
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
item.amount = item.qty * item.rate
|
||||
|
||||
if item.bom:
|
||||
is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
|
||||
@@ -198,7 +228,10 @@ class SubcontractingController(StockController):
|
||||
self.__changed_name = []
|
||||
self.__reference_name = []
|
||||
|
||||
if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new():
|
||||
if (
|
||||
self.doctype in ["Purchase Order", "Subcontracting Order", "Subcontracting Inward Order"]
|
||||
or self.is_new()
|
||||
):
|
||||
self.set(self.raw_material_table, [])
|
||||
return
|
||||
|
||||
@@ -220,8 +253,13 @@ class SubcontractingController(StockController):
|
||||
self.__changed_name.extend(item_dict.keys())
|
||||
|
||||
def __get_backflush_based_on(self):
|
||||
self.backflush_based_on = frappe.db.get_single_value(
|
||||
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
|
||||
self.backflush_based_on = (
|
||||
frappe.db.get_single_value(
|
||||
"Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
)
|
||||
if self.subcontract_data.order_doctype == "Subcontracting Order"
|
||||
else "Material Transferred for Subcontract"
|
||||
)
|
||||
|
||||
def initialized_fields(self):
|
||||
@@ -233,7 +271,7 @@ class SubcontractingController(StockController):
|
||||
def __get_subcontract_orders(self):
|
||||
self.subcontract_orders = []
|
||||
|
||||
if self.doctype in ["Purchase Order", "Subcontracting Order"]:
|
||||
if self.doctype in ["Purchase Order", "Subcontracting Order", "Subcontracting Inward Order"]:
|
||||
return
|
||||
|
||||
self.subcontract_orders = [
|
||||
@@ -543,8 +581,13 @@ class SubcontractingController(StockController):
|
||||
return frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
|
||||
|
||||
def __update_reserve_warehouse(self, row, item):
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
if (
|
||||
self.doctype == self.subcontract_data.order_doctype
|
||||
and self.doctype != "Subcontracting Inward Order"
|
||||
):
|
||||
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
|
||||
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item"):
|
||||
row.warehouse = self.customer_warehouse
|
||||
|
||||
def __set_alternative_item(self, bom_item):
|
||||
if self.alternative_item_details.get(bom_item.rm_item_code):
|
||||
@@ -619,7 +662,7 @@ class SubcontractingController(StockController):
|
||||
|
||||
return serial_nos
|
||||
|
||||
def __add_supplied_item(self, item_row, bom_item, qty):
|
||||
def __add_supplied_or_received_item(self, item_row, bom_item, qty):
|
||||
bom_item.conversion_factor = item_row.conversion_factor
|
||||
rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
if rm_obj.get("qty"):
|
||||
@@ -632,7 +675,8 @@ class SubcontractingController(StockController):
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = flt(qty, rm_obj.precision("required_qty"))
|
||||
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
|
||||
if self.doctype != "Subcontracting Inward Order":
|
||||
rm_obj.amount = flt(rm_obj.required_qty * rm_obj.rate, rm_obj.precision("amount"))
|
||||
else:
|
||||
rm_obj.consumed_qty = flt(qty, rm_obj.precision("consumed_qty"))
|
||||
rm_obj.required_qty = flt(bom_item.required_qty or qty, rm_obj.precision("required_qty"))
|
||||
@@ -841,14 +885,14 @@ class SubcontractingController(StockController):
|
||||
|
||||
return qty
|
||||
|
||||
def __set_supplied_items(self):
|
||||
def __set_supplied_or_received_items(self):
|
||||
self.bom_items = {}
|
||||
|
||||
has_supplied_items = True if self.get(self.raw_material_table) else False
|
||||
has_items = True if self.get(self.raw_material_table) else False
|
||||
for row in self.items:
|
||||
if self.doctype != self.subcontract_data.order_doctype and (
|
||||
(self.__changed_name and row.name not in self.__changed_name)
|
||||
or (has_supplied_items and not self.__changed_name)
|
||||
or (has_items and not self.__changed_name)
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -862,7 +906,7 @@ class SubcontractingController(StockController):
|
||||
bom_item.main_item_code = row.item_code
|
||||
self.__update_reserve_warehouse(bom_item, row)
|
||||
self.__set_alternative_item(bom_item)
|
||||
self.__add_supplied_item(row, bom_item, qty)
|
||||
self.__add_supplied_or_received_item(row, bom_item, qty)
|
||||
|
||||
elif self.backflush_based_on != "BOM":
|
||||
for key, transfer_item in self.available_materials.items():
|
||||
@@ -872,7 +916,7 @@ class SubcontractingController(StockController):
|
||||
) and transfer_item.qty > 0:
|
||||
qty = flt(self.__get_qty_based_on_material_transfer(row, transfer_item))
|
||||
transfer_item.qty -= qty
|
||||
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
|
||||
self.__add_supplied_or_received_item(row, transfer_item.get("item_details"), qty)
|
||||
|
||||
if self.qty_to_be_received:
|
||||
self.qty_to_be_received[
|
||||
@@ -940,13 +984,13 @@ class SubcontractingController(StockController):
|
||||
):
|
||||
return row
|
||||
|
||||
def __prepare_supplied_items(self):
|
||||
def __prepare_supplied_or_received_items(self):
|
||||
self.initialized_fields()
|
||||
self.__get_subcontract_orders()
|
||||
self.__get_pending_qty_to_receive()
|
||||
self.get_available_materials()
|
||||
self.__remove_changed_rows()
|
||||
self.__set_supplied_items()
|
||||
self.__set_supplied_or_received_items()
|
||||
self.__modify_serial_and_batch_bundle()
|
||||
self.__set_rate_for_serial_and_batch_bundle()
|
||||
|
||||
@@ -973,7 +1017,7 @@ class SubcontractingController(StockController):
|
||||
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
|
||||
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
|
||||
|
||||
def __validate_supplied_items(self):
|
||||
def __validate_supplied_or_received_items(self):
|
||||
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
return
|
||||
|
||||
@@ -991,10 +1035,10 @@ class SubcontractingController(StockController):
|
||||
|
||||
self.raw_material_table = raw_material_table
|
||||
self.__identify_change_in_item_table()
|
||||
self.__prepare_supplied_items()
|
||||
self.__validate_supplied_items()
|
||||
self.__prepare_supplied_or_received_items()
|
||||
self.__validate_supplied_or_received_items()
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table="supplied_items"):
|
||||
def create_raw_materials_supplied_or_received(self, raw_material_table="supplied_items"):
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]:
|
||||
@@ -1247,14 +1291,16 @@ def get_item_details(items):
|
||||
return item_details
|
||||
|
||||
|
||||
def get_pending_subcontracted_quantity(po_name):
|
||||
table = frappe.qb.DocType("Purchase Order Item")
|
||||
def get_pending_subcontracted_quantity(doctype, name):
|
||||
table = frappe.qb.DocType(
|
||||
"Purchase Order Item" if doctype == "Subcontracting Order" else "Sales Order Item"
|
||||
)
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.name, table.qty, table.subcontracted_quantity)
|
||||
.where(table.parent == po_name)
|
||||
.select(table.name, table.qty, table.subcontracted_qty)
|
||||
.where(table.parent == name)
|
||||
)
|
||||
return {item.name: item.qty - item.subcontracted_quantity for item in query.run(as_dict=True)}
|
||||
return {item.name: item.qty - item.subcontracted_qty for item in query.run(as_dict=True)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
1021
erpnext/controllers/subcontracting_inward_controller.py
Normal file
1021
erpnext/controllers/subcontracting_inward_controller.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,7 @@ class TestSubcontractingController(IntegrationTestCase):
|
||||
def test_create_raw_materials_supplied(self):
|
||||
sco = get_subcontracting_order()
|
||||
sco.supplied_items = None
|
||||
sco.create_raw_materials_supplied()
|
||||
sco.create_raw_materials_supplied_or_received()
|
||||
self.assertIsNotNone(sco.supplied_items)
|
||||
|
||||
def test_sco_with_bom(self):
|
||||
|
||||
@@ -220,9 +220,9 @@ class BOM(WebsiteGenerator):
|
||||
def onload(self):
|
||||
super().onload()
|
||||
|
||||
self.set_onload_for_muulti_level_bom()
|
||||
self.set_onload_for_multi_level_bom()
|
||||
|
||||
def set_onload_for_muulti_level_bom(self):
|
||||
def set_onload_for_multi_level_bom(self):
|
||||
use_multi_level_bom = frappe.db.get_value(
|
||||
"Property Setter",
|
||||
{"field_name": "use_multi_level_bom", "doc_type": "Work Order", "property": "default"},
|
||||
|
||||
@@ -936,7 +936,7 @@ class ProductionPlan(Document):
|
||||
material_request_type = item.material_request_type or item_doc.default_material_request_type
|
||||
|
||||
# key for Sales Order:Material Request Type:Customer
|
||||
key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "")
|
||||
key = "{}:{}:{}".format(item.sales_order, material_request_type, "")
|
||||
schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days))
|
||||
|
||||
if key not in material_request_map:
|
||||
@@ -949,7 +949,6 @@ class ProductionPlan(Document):
|
||||
"status": "Draft",
|
||||
"company": self.company,
|
||||
"material_request_type": material_request_type,
|
||||
"customer": item_doc.customer or "",
|
||||
}
|
||||
)
|
||||
material_request_list.append(material_request)
|
||||
|
||||
@@ -692,7 +692,6 @@ class TestProductionPlan(IntegrationTestCase):
|
||||
mr = frappe.get_doc("Material Request", material_request)
|
||||
|
||||
self.assertTrue(mr.material_request_type, "Customer Provided")
|
||||
self.assertTrue(mr.customer, "_Test Customer")
|
||||
|
||||
def test_production_plan_with_multi_level_bom(self):
|
||||
"""
|
||||
@@ -2524,4 +2523,7 @@ def make_bom(**args):
|
||||
if not args.do_not_submit:
|
||||
bom.submit()
|
||||
|
||||
if args.set_as_default_bom and not args.do_not_save and not args.do_not_submit:
|
||||
frappe.set_value("Item", args.item, "default_bom", bom.name)
|
||||
|
||||
return bom
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"image",
|
||||
"bom_no",
|
||||
"mps",
|
||||
"subcontracting_inward_order",
|
||||
"subcontracting_inward_order_item",
|
||||
"sales_order",
|
||||
"column_break1",
|
||||
"company",
|
||||
@@ -23,6 +25,7 @@
|
||||
"track_semi_finished_goods",
|
||||
"reserve_stock",
|
||||
"column_break_agjv",
|
||||
"max_producible_qty",
|
||||
"material_transferred_for_manufacturing",
|
||||
"additional_transferred_qty",
|
||||
"produced_qty",
|
||||
@@ -154,6 +157,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.subcontracting_inward_order",
|
||||
"fieldname": "allow_alternative_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Alternative Item"
|
||||
@@ -164,7 +168,8 @@
|
||||
"fieldname": "use_multi_level_bom",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Multi-Level BOM",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only_depends_on": "eval:doc.subcontracting_inward_order"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -219,6 +224,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.subcontracting_inward_order",
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
@@ -235,7 +241,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "skip_transfer",
|
||||
"depends_on": "eval:doc.skip_transfer && !doc.subcontracting_inward_order",
|
||||
"fieldname": "from_wip_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backflush Raw Materials From Work-in-Progress Warehouse"
|
||||
@@ -247,6 +253,7 @@
|
||||
"options": "fa fa-building"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!(doc.skip_transfer && doc.subcontracting_inward_order)",
|
||||
"description": "This is a location where operations are executed.",
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
@@ -259,7 +266,8 @@
|
||||
"fieldname": "fg_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "subcontracting_inward_order"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -418,6 +426,7 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.subcontracting_inward_order",
|
||||
"description": "Manufacture against Material Request",
|
||||
"fieldname": "material_request",
|
||||
"fieldtype": "Link",
|
||||
@@ -495,7 +504,8 @@
|
||||
"fieldname": "source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "eval:doc.subcontracting_inward_order"
|
||||
},
|
||||
{
|
||||
"description": "In Mins",
|
||||
@@ -595,7 +605,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "reserve_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": " Reserve Stock"
|
||||
"label": "Reserve Stock",
|
||||
"read_only_depends_on": "subcontracting_inward_order"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==1",
|
||||
@@ -622,6 +633,32 @@
|
||||
"label": "Additional Transferred Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "subcontracting_inward_order",
|
||||
"fieldname": "subcontracting_inward_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subcontracting Inward Order",
|
||||
"options": "Subcontracting Inward Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontracting_inward_order_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Subcontracting Inward Order Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "max_producible_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Max Producible Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -630,7 +667,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-09-29 15:57:47.022616",
|
||||
"modified": "2025-10-12 14:24:57.699749",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -102,6 +102,7 @@ class WorkOrder(Document):
|
||||
material_request: DF.Link | None
|
||||
material_request_item: DF.Data | None
|
||||
material_transferred_for_manufacturing: DF.Float
|
||||
max_producible_qty: DF.Float
|
||||
mps: DF.Link | None
|
||||
naming_series: DF.Literal["MFG-WO-.YYYY.-"]
|
||||
operations: DF.Table[WorkOrderOperation]
|
||||
@@ -138,6 +139,8 @@ class WorkOrder(Document):
|
||||
"Cancelled",
|
||||
]
|
||||
stock_uom: DF.Link | None
|
||||
subcontracting_inward_order: DF.Link | None
|
||||
subcontracting_inward_order_item: DF.Data | None
|
||||
total_operating_cost: DF.Currency
|
||||
track_semi_finished_goods: DF.Check
|
||||
transfer_material_against: DF.Literal["", "Work Order", "Job Card"]
|
||||
@@ -180,7 +183,11 @@ class WorkOrder(Document):
|
||||
if self.bom_no:
|
||||
validate_bom_no(self.production_item, self.bom_no)
|
||||
|
||||
self.validate_sales_order()
|
||||
if not self.subcontracting_inward_order:
|
||||
self.validate_sales_order()
|
||||
else:
|
||||
self.validate_self_rm_warehouse()
|
||||
|
||||
self.set_default_warehouse()
|
||||
self.validate_warehouse_belongs_to_company()
|
||||
self.check_wip_warehouse_skip()
|
||||
@@ -203,6 +210,7 @@ class WorkOrder(Document):
|
||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||
self.enable_auto_reserve_stock()
|
||||
self.validate_operations_sequence()
|
||||
self.validate_subcontracting_inward_order()
|
||||
|
||||
def validate_dates(self):
|
||||
if self.actual_start_date and self.actual_end_date:
|
||||
@@ -210,7 +218,7 @@ class WorkOrder(Document):
|
||||
frappe.throw(_("Actual End Date cannot be before Actual Start Date"))
|
||||
|
||||
def validate_fg_warehouse_for_reservation(self):
|
||||
if self.reserve_stock and self.sales_order:
|
||||
if self.reserve_stock and self.sales_order and not self.subcontracting_inward_order:
|
||||
warehouses = frappe.get_all(
|
||||
"Sales Order Item",
|
||||
filters={"parent": self.sales_order, "item_code": self.production_item},
|
||||
@@ -257,6 +265,24 @@ class WorkOrder(Document):
|
||||
)
|
||||
sequence_id = op.sequence_id
|
||||
|
||||
def validate_subcontracting_inward_order(self):
|
||||
if scio := self.subcontracting_inward_order:
|
||||
if self.source_warehouse != (
|
||||
rm_receipt_warehouse := frappe.get_cached_value(
|
||||
"Subcontracting Inward Order",
|
||||
scio,
|
||||
"customer_warehouse",
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Source Warehouse {0} must be same as Customer Warehouse {1} in the Subcontracting Inward Order"
|
||||
).format(
|
||||
frappe.bold(self.source_warehouse),
|
||||
frappe.bold(rm_receipt_warehouse),
|
||||
)
|
||||
)
|
||||
|
||||
def set_warehouses(self):
|
||||
for row in self.required_items:
|
||||
if not row.source_warehouse:
|
||||
@@ -330,6 +356,15 @@ class WorkOrder(Document):
|
||||
else:
|
||||
frappe.throw(_("Sales Order {0} is not valid").format(self.sales_order))
|
||||
|
||||
def validate_self_rm_warehouse(self):
|
||||
for item in [item for item in self.required_items if not item.is_customer_provided_item]:
|
||||
if frappe.get_cached_value("Warehouse", item.source_warehouse, "customer"):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Source Warehouse {1} for item {2} cannot be a customer warehouse.").format(
|
||||
item.idx, frappe.bold(item.source_warehouse), frappe.bold(item.item_code)
|
||||
)
|
||||
)
|
||||
|
||||
def check_sales_order_on_hold_or_close(self):
|
||||
status = frappe.db.get_value("Sales Order", self.sales_order, "status")
|
||||
if status in ("Closed", "On Hold"):
|
||||
@@ -636,6 +671,8 @@ class WorkOrder(Document):
|
||||
if self.reserve_stock:
|
||||
self.update_stock_reservation()
|
||||
|
||||
self.update_subcontracting_inward_order_received_items()
|
||||
|
||||
def on_cancel(self):
|
||||
self.validate_cancel()
|
||||
self.db_set("status", "Cancelled")
|
||||
@@ -657,10 +694,68 @@ class WorkOrder(Document):
|
||||
if self.reserve_stock:
|
||||
self.update_stock_reservation()
|
||||
|
||||
self.update_subcontracting_inward_order_received_items()
|
||||
|
||||
def update_stock_reservation(self):
|
||||
self.set_qty_change()
|
||||
make_stock_reservation_entries(self)
|
||||
self.db_set("status", self.get_status())
|
||||
|
||||
def set_qty_change(self):
|
||||
if scio_item_name := self.get("subcontracting_inward_order_item"):
|
||||
scio_rm_item_names = frappe.db.get_all(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
filters={"reference_name": scio_item_name, "docstatus": 1, "is_customer_provided_item": 1},
|
||||
pluck="name",
|
||||
)
|
||||
self.qty_change = frappe._dict()
|
||||
|
||||
data = frappe.get_all(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
{"name": ["in", scio_rm_item_names]},
|
||||
["rm_item_code", "required_qty as bom_qty", "work_order_qty", "received_qty"],
|
||||
)
|
||||
for d in data:
|
||||
wo_item = next(
|
||||
wo_item for wo_item in self.get("required_items") if wo_item.item_code == d.rm_item_code
|
||||
)
|
||||
|
||||
if (
|
||||
d.work_order_qty + (wo_item.required_qty if self._action == "submit" else 0)
|
||||
) == d.bom_qty and d.received_qty > d.bom_qty:
|
||||
self.qty_change[wo_item.name] = d.received_qty - d.bom_qty
|
||||
|
||||
def update_subcontracting_inward_order_received_items(self):
|
||||
if scio_item_name := self.get("subcontracting_inward_order_item"):
|
||||
scio_rm_data = frappe.get_all(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
filters={"reference_name": scio_item_name, "docstatus": 1},
|
||||
fields=["name", "rm_item_code"],
|
||||
)
|
||||
|
||||
required_qty = {
|
||||
wo_item.item_code: wo_item.required_qty
|
||||
for wo_item in self.get("required_items")
|
||||
if wo_item.item_code in [d.rm_item_code for d in scio_rm_data]
|
||||
}
|
||||
|
||||
table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
|
||||
case_expr = Case()
|
||||
for item in scio_rm_data:
|
||||
case_expr = case_expr.when(
|
||||
table.rm_item_code == item.rm_item_code,
|
||||
table.work_order_qty
|
||||
+ (
|
||||
required_qty[item.rm_item_code]
|
||||
if self._action == "submit"
|
||||
else -required_qty[item.rm_item_code]
|
||||
),
|
||||
)
|
||||
|
||||
frappe.qb.update(table).set(table.work_order_qty, case_expr).where(
|
||||
(table.name.isin([d.name for d in scio_rm_data])) & (table.docstatus == 1)
|
||||
).run()
|
||||
|
||||
def create_serial_no_batch_no(self):
|
||||
if not (self.has_serial_no or self.has_batch_no):
|
||||
return
|
||||
@@ -1229,6 +1324,15 @@ class WorkOrder(Document):
|
||||
OverProductionError,
|
||||
)
|
||||
|
||||
if self.subcontracting_inward_order and self.qty > self.max_producible_qty:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Warning: Quantity exceeds maximum producible quantity based on quantity of raw materials received through the Subcontracting Inward Order {0}."
|
||||
).format(frappe.bold(self.subcontracting_inward_order)),
|
||||
alert=True,
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
def validate_transfer_against(self):
|
||||
if self.docstatus != 1:
|
||||
# let user configure operations until they're ready to submit
|
||||
@@ -1330,6 +1434,11 @@ class WorkOrder(Document):
|
||||
},
|
||||
)
|
||||
|
||||
if self.subcontracting_inward_order and not frappe.get_cached_value(
|
||||
"Item", item.item_code, "is_customer_provided_item"
|
||||
):
|
||||
self.required_items[-1].source_warehouse = item.default_warehouse
|
||||
|
||||
if not self.project:
|
||||
self.project = item.get("project")
|
||||
|
||||
@@ -1532,7 +1641,7 @@ class WorkOrder(Document):
|
||||
|
||||
stock_entry.reload()
|
||||
if stock_entry.purpose == "Manufacture" and (
|
||||
self.sales_order or self.production_plan_sub_assembly_item
|
||||
self.sales_order or self.production_plan_sub_assembly_item or self.subcontracting_inward_order
|
||||
):
|
||||
items = self.get_finished_goods_for_reservation(stock_entry)
|
||||
elif stock_entry.purpose == "Material Transfer for Manufacture":
|
||||
@@ -1577,6 +1686,8 @@ class WorkOrder(Document):
|
||||
if self.production_plan_sub_assembly_item:
|
||||
# Reserve the sub-assembly item for the final product for the work order.
|
||||
item_details = self.get_wo_details()
|
||||
elif self.subcontracting_inward_order:
|
||||
item_details = self.get_scio_details()
|
||||
else:
|
||||
# Reserve the final product for the sales order.
|
||||
item_details = self.get_so_details()
|
||||
@@ -1665,6 +1776,25 @@ class WorkOrder(Document):
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
def get_scio_details(self):
|
||||
return frappe.get_all(
|
||||
"Subcontracting Inward Order Item",
|
||||
filters={
|
||||
"name": self.subcontracting_inward_order_item,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=[
|
||||
"item_code",
|
||||
"name",
|
||||
"qty as stock_qty",
|
||||
"produced_qty as stock_reserved_qty",
|
||||
"delivery_warehouse as warehouse",
|
||||
"parent as voucher_no",
|
||||
"parenttype as voucher_type",
|
||||
"delivered_qty",
|
||||
],
|
||||
)
|
||||
|
||||
def get_so_details(self):
|
||||
return frappe.get_all(
|
||||
"Sales Order Item",
|
||||
@@ -1767,7 +1897,8 @@ class WorkOrder(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_stock_reservation_entries(doc, items=None, table_name=None, is_transfer=True, notify=False):
|
||||
def make_stock_reservation_entries(doc, items=None, is_transfer=True, notify=False):
|
||||
is_transfer = cint(is_transfer)
|
||||
if isinstance(doc, str):
|
||||
doc = parse_json(doc)
|
||||
doc = frappe.get_doc("Work Order", doc.get("name"))
|
||||
@@ -1781,6 +1912,14 @@ def make_stock_reservation_entries(doc, items=None, table_name=None, is_transfer
|
||||
sre.transfer_reservation_entries_to(
|
||||
doc.production_plan, from_doctype="Production Plan", to_doctype="Work Order"
|
||||
)
|
||||
elif doc.subcontracting_inward_order and is_transfer:
|
||||
sre.transfer_reservation_entries_to(
|
||||
doc.subcontracting_inward_order,
|
||||
from_doctype="Subcontracting Inward Order",
|
||||
to_doctype="Work Order",
|
||||
against_fg_item=doc.subcontracting_inward_order_item,
|
||||
qty_change=doc.qty_change,
|
||||
)
|
||||
else:
|
||||
sre_created = sre.make_stock_reservation_entries()
|
||||
if sre_created:
|
||||
@@ -2045,6 +2184,9 @@ def make_stock_entry(
|
||||
qty if qty is not None else (flt(work_order.qty) - flt(work_order.produced_qty))
|
||||
)
|
||||
|
||||
if purpose == "Manufacture" and work_order.subcontracting_inward_order:
|
||||
stock_entry.subcontracting_inward_order = work_order.subcontracting_inward_order
|
||||
|
||||
if work_order.bom_no:
|
||||
stock_entry.inspection_required = frappe.db.get_value("BOM", work_order.bom_no, "inspection_required")
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"column_break_jash",
|
||||
"stock_reserved_qty",
|
||||
"is_additional_item",
|
||||
"is_customer_provided_item",
|
||||
"voucher_detail_reference"
|
||||
],
|
||||
"fields": [
|
||||
@@ -52,7 +53,8 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Source Warehouse",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "eval:parent.subcontracting_inward_order && doc.is_customer_provided_item"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
@@ -91,6 +93,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!parent.subcontracting_inward_order",
|
||||
"fieldname": "allow_alternative_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Alternative Item"
|
||||
@@ -190,12 +193,20 @@
|
||||
"label": "Voucher Detail Reference",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.is_customer_provided_item",
|
||||
"fieldname": "is_customer_provided_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Customer Provided Item",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-12 17:36:00.115181",
|
||||
"modified": "2025-10-12 14:27:16.721532",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Item",
|
||||
|
||||
@@ -23,6 +23,7 @@ class WorkOrderItem(Document):
|
||||
description: DF.Text | None
|
||||
include_item_in_manufacturing: DF.Check
|
||||
is_additional_item: DF.Check
|
||||
is_customer_provided_item: DF.Check
|
||||
item_code: DF.Link | None
|
||||
item_name: DF.Data | None
|
||||
operation: DF.Link | None
|
||||
|
||||
@@ -440,3 +440,5 @@ erpnext.patches.v16_0.make_workstation_operating_components #1
|
||||
erpnext.patches.v16_0.set_reporting_currency
|
||||
erpnext.patches.v16_0.set_posting_datetime_for_sabb_and_drop_indexes
|
||||
erpnext.patches.v16_0.update_serial_no_reference_name
|
||||
erpnext.patches.v16_0.rename_subcontracted_quantity
|
||||
erpnext.patches.v16_0.add_new_stock_entry_types
|
||||
14
erpnext/patches/v16_0/add_new_stock_entry_types.py
Normal file
14
erpnext/patches/v16_0/add_new_stock_entry_types.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for stock_entry_type in [
|
||||
"Receive from Customer",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
]:
|
||||
if not frappe.db.exists("Stock Entry Type", stock_entry_type):
|
||||
frappe.new_doc("Stock Entry Type", purpose=stock_entry_type, is_standard=1).insert(
|
||||
set_name=stock_entry_type, ignore_permissions=True
|
||||
)
|
||||
7
erpnext/patches/v16_0/rename_subcontracted_quantity.py
Normal file
7
erpnext/patches/v16_0/rename_subcontracted_quantity.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
|
||||
def execute():
|
||||
if frappe.db.has_column("Purchase Order Item", "subcontracted_quantity"):
|
||||
rename_field("Purchase Order Item", "subcontracted_quantity", "subcontracted_qty")
|
||||
@@ -195,6 +195,7 @@ $.extend(erpnext.stock_reservation, {
|
||||
args: {
|
||||
doc: frm.doc,
|
||||
items: data.items,
|
||||
is_transfer: 0,
|
||||
table_name: table_name,
|
||||
notify: true,
|
||||
},
|
||||
|
||||
@@ -645,7 +645,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
get_query: function () {
|
||||
let filters;
|
||||
if (frm.doc.doctype == "Sales Order") {
|
||||
filters = { is_sales_item: 1 };
|
||||
filters = { is_sales_item: 1, is_stock_item: !frm.doc.is_subcontracted };
|
||||
} else if (frm.doc.doctype == "Purchase Order") {
|
||||
if (frm.doc.is_subcontracted) {
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
@@ -801,7 +801,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
}
|
||||
|
||||
if (
|
||||
frm.doc.doctype == "Purchase Order" &&
|
||||
["Purchase Order", "Sales Order"].includes(frm.doc.doctype) &&
|
||||
frm.doc.is_subcontracted &&
|
||||
!frm.doc.is_old_subcontracting_flow
|
||||
) {
|
||||
@@ -857,7 +857,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
},
|
||||
],
|
||||
primary_action: function () {
|
||||
if (frm.doctype == "Sales Order" && has_reserved_stock) {
|
||||
if (frm.doctype == "Sales Order" && has_reserved_stock && frm.doc.is_subcontracted == 0) {
|
||||
this.hide();
|
||||
frappe.confirm(
|
||||
__(
|
||||
|
||||
@@ -543,6 +543,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
based_on: based_on,
|
||||
posting_date: this.frm.doc.posting_date,
|
||||
posting_time: this.frm.doc.posting_time,
|
||||
scio_detail: this.item.scio_detail,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -122,6 +122,30 @@ def install(country=None):
|
||||
"purpose": "Material Consumption for Manufacture",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": _("Receive from Customer"),
|
||||
"purpose": "Receive from Customer",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": _("Return Raw Material to Customer"),
|
||||
"purpose": "Return Raw Material to Customer",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": _("Subcontracting Delivery"),
|
||||
"purpose": "Subcontracting Delivery",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
"doctype": "Stock Entry Type",
|
||||
"name": _("Subcontracting Return"),
|
||||
"purpose": "Subcontracting Return",
|
||||
"is_standard": 1,
|
||||
},
|
||||
# territory: with two default territories, one for home country and one named Rest of the World
|
||||
{
|
||||
"doctype": "Territory",
|
||||
|
||||
@@ -221,7 +221,13 @@ frappe.ui.form.on("Item", {
|
||||
|
||||
const stock_exists = frm.doc.__onload && frm.doc.__onload.stock_exists ? 1 : 0;
|
||||
|
||||
["is_stock_item", "has_serial_no", "has_batch_no", "has_variants"].forEach((fieldname) => {
|
||||
[
|
||||
"is_stock_item",
|
||||
"is_customer_provided_item",
|
||||
"has_serial_no",
|
||||
"has_batch_no",
|
||||
"has_variants",
|
||||
].forEach((fieldname) => {
|
||||
frm.set_df_property(fieldname, "read_only", stock_exists);
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"lead_time_days",
|
||||
"last_purchase_rate",
|
||||
"is_customer_provided_item",
|
||||
"customer",
|
||||
"supplier_details",
|
||||
"delivered_by_supplier",
|
||||
"column_break2",
|
||||
@@ -206,9 +205,11 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_customer_provided_item",
|
||||
"fieldname": "allow_alternative_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Alternative Item"
|
||||
"label": "Allow Alternative Item",
|
||||
"read_only_depends_on": "eval:doc.is_customer_provided_item"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
@@ -585,13 +586,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Customer Provided Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_customer_provided_item==1",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
@@ -771,10 +765,9 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If subcontracted to a vendor",
|
||||
"fieldname": "is_sub_contracted_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Supply Raw Materials for Purchase",
|
||||
"label": "Is Subcontracted Item",
|
||||
"oldfieldname": "is_sub_contracted_item",
|
||||
"oldfieldtype": "Select"
|
||||
},
|
||||
@@ -954,7 +947,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2025-10-01 16:58:40.946604",
|
||||
"modified": "2025-10-12 16:58:40.946604",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -82,7 +82,6 @@ class Item(Document):
|
||||
country_of_origin: DF.Link | None
|
||||
create_new_batch: DF.Check
|
||||
cumulative_time: DF.Int
|
||||
customer: DF.Link | None
|
||||
customer_code: DF.SmallText | None
|
||||
customer_items: DF.Table[ItemCustomerDetail]
|
||||
customs_tariff_number: DF.Link | None
|
||||
@@ -965,7 +964,13 @@ class Item(Document):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
restricted_fields = ("has_serial_no", "is_stock_item", "valuation_method", "has_batch_no")
|
||||
restricted_fields = (
|
||||
"has_serial_no",
|
||||
"is_customer_provided_item",
|
||||
"is_stock_item",
|
||||
"valuation_method",
|
||||
"has_batch_no",
|
||||
)
|
||||
|
||||
values = frappe.db.get_value("Item", self.name, restricted_fields, as_dict=True)
|
||||
if not values:
|
||||
|
||||
@@ -67,6 +67,10 @@ class PackedItem(Document):
|
||||
|
||||
def make_packing_list(doc):
|
||||
"Make/Update packing list for Product Bundle Item."
|
||||
|
||||
if doc.get("is_subcontracted"):
|
||||
return
|
||||
|
||||
if doc.get("_action") and doc._action == "update_after_submit":
|
||||
return
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import json
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
import frappe
|
||||
import frappe.query_builder
|
||||
import frappe.query_builder.functions
|
||||
from frappe import _, _dict, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
@@ -1916,10 +1918,9 @@ def get_serial_and_batch_ledger(**kwargs):
|
||||
def get_auto_data(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
if cint(kwargs.has_serial_no):
|
||||
return get_available_serial_nos(kwargs)
|
||||
|
||||
return get_serial_nos_from_sre(kwargs) if kwargs.scio_detail else get_available_serial_nos(kwargs)
|
||||
elif cint(kwargs.has_batch_no):
|
||||
return get_auto_batch_nos(kwargs)
|
||||
return get_batch_nos_from_sre(kwargs) if kwargs.scio_detail else get_auto_batch_nos(kwargs)
|
||||
|
||||
|
||||
def get_available_batches_qty(available_batches):
|
||||
@@ -2021,6 +2022,28 @@ def get_available_serial_nos(kwargs):
|
||||
)
|
||||
|
||||
|
||||
def get_serial_nos_from_sre(kwargs):
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
child_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.join(child_table)
|
||||
.on(table.name == child_table.parent)
|
||||
.select(child_table.serial_no, child_table.batch_no, child_table.warehouse)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.voucher_detail_no == kwargs.scio_detail)
|
||||
& (child_table.qty != child_table.delivered_qty)
|
||||
)
|
||||
.limit(cint(kwargs.qty) or 10000000)
|
||||
)
|
||||
if kwargs.based_on == "LIFO":
|
||||
query = query.orderby(child_table.creation, order=frappe.query_builder.Order.desc)
|
||||
else:
|
||||
query = query.orderby(child_table.creation)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_non_expired_batches(batches):
|
||||
filters = {}
|
||||
if isinstance(batches, list):
|
||||
@@ -2094,13 +2117,13 @@ def get_bundle_wise_serial_nos(data, kwargs):
|
||||
def get_reserved_voucher_details(kwargs):
|
||||
reserved_voucher_details = []
|
||||
|
||||
value = {
|
||||
"Delivery Note": ["Delivery Note Item", "against_sales_order"],
|
||||
"Stock Entry": ["Stock Entry", "work_order"],
|
||||
"Work Order": ["Work Order", "production_plan"],
|
||||
field_mapper = {
|
||||
"Delivery Note": [["Delivery Note Item", "against_sales_order"]],
|
||||
"Stock Entry": [["Stock Entry", "work_order"], ["Stock Entry", "subcontracting_inward_order"]],
|
||||
"Work Order": [["Work Order", "production_plan"], ["Work Order", "subcontracting_inward_order"]],
|
||||
}.get(kwargs.get("sabb_voucher_type"))
|
||||
|
||||
if not value or not kwargs.get("sabb_voucher_no"):
|
||||
if not field_mapper or not kwargs.get("sabb_voucher_no"):
|
||||
return reserved_voucher_details
|
||||
|
||||
voucher_based_filters = {
|
||||
@@ -2119,11 +2142,15 @@ def get_reserved_voucher_details(kwargs):
|
||||
},
|
||||
}.get(kwargs.get("sabb_voucher_type"))
|
||||
|
||||
reserved_voucher_details = frappe.get_all(
|
||||
value[0],
|
||||
pluck=value[1],
|
||||
filters=voucher_based_filters,
|
||||
)
|
||||
reserved_voucher_details = []
|
||||
for row in field_mapper:
|
||||
reserved_voucher_details.extend(
|
||||
frappe.get_all(
|
||||
row[0],
|
||||
pluck=row[1],
|
||||
filters=voucher_based_filters,
|
||||
)
|
||||
)
|
||||
|
||||
return reserved_voucher_details
|
||||
|
||||
@@ -2429,6 +2456,43 @@ def get_auto_batch_nos(kwargs):
|
||||
return get_qty_based_available_batches(available_batches, qty)
|
||||
|
||||
|
||||
def get_batch_nos_from_sre(kwargs):
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
|
||||
table = frappe.qb.DocType("Stock Reservation Entry")
|
||||
child_table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
if kwargs.based_on == "LIFO":
|
||||
creation_field = Max(child_table.creation).as_("sort_creation")
|
||||
order = frappe.query_builder.Order.desc
|
||||
else:
|
||||
creation_field = Min(child_table.creation).as_("sort_creation")
|
||||
order = frappe.query_builder.Order.asc
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.join(child_table)
|
||||
.on(table.name == child_table.parent)
|
||||
.select(
|
||||
child_table.batch_no,
|
||||
child_table.warehouse,
|
||||
Sum(child_table.qty - child_table.delivered_qty).as_("qty"),
|
||||
creation_field,
|
||||
)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.voucher_detail_no == kwargs.scio_detail)
|
||||
& (child_table.qty != child_table.delivered_qty)
|
||||
)
|
||||
.groupby(child_table.batch_no, child_table.warehouse)
|
||||
.orderby("sort_creation", order=order)
|
||||
.orderby(child_table.batch_no, order=frappe.query_builder.Order.asc)
|
||||
)
|
||||
|
||||
result = query.run(as_dict=True)
|
||||
return get_qty_based_available_batches(result, flt(kwargs.qty)) if flt(kwargs.qty) else result
|
||||
|
||||
|
||||
def get_batches_to_be_considered(sales_order_name):
|
||||
parent = frappe.qb.DocType("Stock Reservation Entry")
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
@@ -87,15 +87,13 @@ frappe.ui.form.on("Stock Entry", {
|
||||
frappe.throw(__("Please enter Item Code to get Batch Number"));
|
||||
} else {
|
||||
if (
|
||||
in_list(
|
||||
[
|
||||
"Material Transfer for Manufacture",
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
],
|
||||
doc.purpose
|
||||
)
|
||||
[
|
||||
"Material Transfer for Manufacture",
|
||||
"Manufacture",
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Receive from Customer",
|
||||
].includes(doc.purpose)
|
||||
) {
|
||||
filters = {
|
||||
item_code: item.item_code,
|
||||
@@ -214,7 +212,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
refresh: function (frm) {
|
||||
frm.trigger("get_items_from_transit_entry");
|
||||
|
||||
if (!frm.doc.docstatus) {
|
||||
if (!frm.doc.docstatus && !frm.doc.subcontracting_inward_order) {
|
||||
frm.trigger("validate_purpose_consumption");
|
||||
frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
@@ -299,7 +297,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) {
|
||||
frm.add_custom_button(
|
||||
__("Purchase Invoice"),
|
||||
function () {
|
||||
@@ -367,7 +365,11 @@ frappe.ui.form.on("Stock Entry", {
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 0 && frm.doc.purpose == "Material Issue") {
|
||||
if (
|
||||
frm.doc.docstatus === 0 &&
|
||||
frm.doc.purpose == "Material Issue" &&
|
||||
!frm.doc.subcontracting_inward_order
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Expired Batches"),
|
||||
function () {
|
||||
@@ -420,6 +422,17 @@ frappe.ui.form.on("Stock Entry", {
|
||||
}
|
||||
|
||||
frm.events.set_route_options_for_new_doc(frm);
|
||||
|
||||
frm.set_df_property(
|
||||
"items",
|
||||
"cannot_add_rows",
|
||||
frm.doc.subcontracting_inward_order &&
|
||||
[
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Return",
|
||||
"Subcontracting Delivery",
|
||||
].includes(frm.doc.purpose)
|
||||
);
|
||||
},
|
||||
|
||||
set_route_options_for_new_doc(frm) {
|
||||
@@ -445,7 +458,7 @@ frappe.ui.form.on("Stock Entry", {
|
||||
},
|
||||
|
||||
get_items_from_transit_entry: function (frm) {
|
||||
if (frm.doc.docstatus === 0) {
|
||||
if (frm.doc.docstatus === 0 && !frm.doc.subcontracting_inward_order) {
|
||||
frm.add_custom_button(
|
||||
__("Transit Entry"),
|
||||
function () {
|
||||
@@ -609,7 +622,8 @@ frappe.ui.form.on("Stock Entry", {
|
||||
frm.doc.docstatus === 0 &&
|
||||
["Material Issue", "Material Receipt", "Material Transfer", "Send to Subcontractor"].includes(
|
||||
frm.doc.purpose
|
||||
)
|
||||
) &&
|
||||
!frm.doc.subcontracting_inward_order
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Bill of Materials"),
|
||||
@@ -622,10 +636,6 @@ frappe.ui.form.on("Stock Entry", {
|
||||
},
|
||||
|
||||
get_items_from_bom: function (frm) {
|
||||
let filters = function () {
|
||||
return { filters: { docstatus: 1 } };
|
||||
};
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldname: "bom",
|
||||
@@ -1084,10 +1094,28 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
this.frm.add_fetch("purchase_order", "supplier", "supplier");
|
||||
} else {
|
||||
this.frm.add_fetch("subcontracting_order", "supplier", "supplier");
|
||||
this.frm.add_fetch("subcontracting_inward_order", "customer", "customer");
|
||||
}
|
||||
|
||||
frappe.dynamic_link = { doc: this.frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||
this.frm.set_query("supplier_address", erpnext.queries.address_query);
|
||||
|
||||
const operator = this.frm.doc.subcontracting_inward_order ? "in" : "not in";
|
||||
this.frm.set_query("stock_entry_type", function () {
|
||||
return {
|
||||
filters: {
|
||||
purpose: [
|
||||
operator,
|
||||
[
|
||||
"Receive from Customer",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onload_post_render() {
|
||||
@@ -1100,7 +1128,6 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
}
|
||||
|
||||
refresh() {
|
||||
var me = this;
|
||||
erpnext.toggle_naming_series();
|
||||
this.toggle_related_fields(this.frm.doc);
|
||||
this.toggle_enable_bom();
|
||||
@@ -1369,6 +1396,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
doc.delivery_note_no =
|
||||
doc.sales_invoice_no =
|
||||
null;
|
||||
} else if (doc.purpose === "Receive from Customer") {
|
||||
doc.supplier = doc.supplier_name = doc.supplier_address = doc.purchase_receipt_no = null;
|
||||
} else {
|
||||
doc.customer =
|
||||
doc.customer_name =
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"job_card",
|
||||
"purchase_order",
|
||||
"subcontracting_order",
|
||||
"subcontracting_inward_order",
|
||||
"delivery_note_no",
|
||||
"sales_invoice_no",
|
||||
"pick_list",
|
||||
@@ -129,7 +130,7 @@
|
||||
"label": "Purpose",
|
||||
"oldfieldname": "purpose",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
|
||||
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble\nReceive from Customer\nReturn Raw Material to Customer\nSubcontracting Delivery\nSubcontracting Return",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@@ -708,8 +709,17 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Additional Transfer Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "subcontracting_inward_order",
|
||||
"fieldname": "subcontracting_inward_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subcontracting Inward Order",
|
||||
"options": "Subcontracting Inward Order",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -79,11 +78,12 @@ class MaxSampleAlreadyRetainedError(frappe.ValidationError):
|
||||
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.subcontracting_inward_controller import SubcontractingInwardController
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
|
||||
|
||||
|
||||
class StockEntry(StockController):
|
||||
class StockEntry(StockController, SubcontractingInwardController):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -138,6 +138,10 @@ class StockEntry(StockController):
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
"Receive from Customer",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
]
|
||||
remarks: DF.Text | None
|
||||
sales_invoice_no: DF.Link | None
|
||||
@@ -147,6 +151,7 @@ class StockEntry(StockController):
|
||||
source_address_display: DF.TextEditor | None
|
||||
source_warehouse_address: DF.Link | None
|
||||
stock_entry_type: DF.Link
|
||||
subcontracting_inward_order: DF.Link | None
|
||||
subcontracting_order: DF.Link | None
|
||||
supplier: DF.Link | None
|
||||
supplier_address: DF.Link | None
|
||||
@@ -174,6 +179,15 @@ class StockEntry(StockController):
|
||||
"order_supplied_items_field": "Purchase Order Item Supplied",
|
||||
}
|
||||
)
|
||||
elif self.subcontracting_inward_order:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
"order_doctype": "Subcontracting Inward Order",
|
||||
"order_field": "subcontracting_inward_order",
|
||||
"rm_detail_field": "scio_detail",
|
||||
"order_received_items_field": "Subcontracting Inward Order Received Item",
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
@@ -248,15 +262,20 @@ class StockEntry(StockController):
|
||||
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.validate_subcontract_order()
|
||||
|
||||
super().validate_subcontracting_inward()
|
||||
|
||||
def on_submit(self):
|
||||
self.make_bundle_using_old_serial_batch_fields()
|
||||
self.update_work_order()
|
||||
self.update_disassembled_order()
|
||||
self.adjust_stock_reservation_entries_for_return()
|
||||
self.update_sre_for_subcontracting_delivery()
|
||||
self.update_stock_ledger()
|
||||
self.make_stock_reserve_for_wip_and_fg()
|
||||
|
||||
self.validate_subcontract_order()
|
||||
self.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
self.update_pick_list_status()
|
||||
@@ -273,6 +292,8 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
|
||||
self.set_material_request_transfer_status("Completed")
|
||||
|
||||
super().on_submit_subcontracting_inward()
|
||||
|
||||
def on_cancel(self):
|
||||
self.delink_asset_repair_sabb()
|
||||
self.validate_closed_subcontracting_order()
|
||||
@@ -284,7 +305,8 @@ class StockEntry(StockController):
|
||||
self.validate_work_order_status()
|
||||
|
||||
self.update_work_order()
|
||||
self.update_disassembled_order(is_cancel=True)
|
||||
self.update_disassembled_order()
|
||||
self.cancel_stock_reservation_entries_for_inward()
|
||||
self.update_stock_ledger()
|
||||
|
||||
self.ignore_linked_doctypes = (
|
||||
@@ -299,6 +321,8 @@ class StockEntry(StockController):
|
||||
self.update_cost_in_project()
|
||||
self.update_transferred_qty()
|
||||
self.update_quality_inspection()
|
||||
self.adjust_stock_reservation_entries_for_return()
|
||||
self.update_sre_for_subcontracting_delivery()
|
||||
self.delete_auto_created_batches()
|
||||
self.delete_linked_stock_entry()
|
||||
|
||||
@@ -307,6 +331,8 @@ class StockEntry(StockController):
|
||||
if self.purpose == "Material Transfer" and self.outgoing_stock_entry:
|
||||
self.set_material_request_transfer_status("In Transit")
|
||||
|
||||
super().on_cancel_subcontracting_inward()
|
||||
|
||||
def on_update(self):
|
||||
super().on_update()
|
||||
self.set_serial_and_batch_bundle()
|
||||
@@ -369,11 +395,17 @@ class StockEntry(StockController):
|
||||
"Send to Subcontractor",
|
||||
"Material Consumption for Manufacture",
|
||||
"Disassemble",
|
||||
"Receive from Customer",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
]
|
||||
|
||||
if self.purpose not in valid_purposes:
|
||||
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
|
||||
|
||||
super().validate_purpose()
|
||||
|
||||
def delete_linked_stock_entry(self):
|
||||
if self.purpose == "Send to Warehouse":
|
||||
for d in frappe.get_all(
|
||||
@@ -507,6 +539,9 @@ class StockEntry(StockController):
|
||||
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
|
||||
)
|
||||
|
||||
if self.purpose == "Subcontracting Delivery":
|
||||
item.expense_account = frappe.get_value("Company", self.company, "default_expense_account")
|
||||
|
||||
def validate_fg_completed_qty(self):
|
||||
if self.purpose != "Manufacture":
|
||||
return
|
||||
@@ -576,7 +611,10 @@ class StockEntry(StockController):
|
||||
title=_("Difference Account in Items Table"),
|
||||
)
|
||||
|
||||
if self.purpose != "Material Issue" and acc_details.account_type == "Cost of Goods Sold":
|
||||
if (
|
||||
self.purpose not in ["Material Issue", "Subcontracting Delivery"]
|
||||
and acc_details.account_type == "Cost of Goods Sold"
|
||||
):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"At row #{0}: you have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account"
|
||||
@@ -595,6 +633,8 @@ class StockEntry(StockController):
|
||||
"Send to Subcontractor",
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Consumption for Manufacture",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
]
|
||||
|
||||
target_mandatory = [
|
||||
@@ -602,6 +642,8 @@ class StockEntry(StockController):
|
||||
"Material Transfer",
|
||||
"Send to Subcontractor",
|
||||
"Material Transfer for Manufacture",
|
||||
"Receive from Customer",
|
||||
"Subcontracting Return",
|
||||
]
|
||||
|
||||
validate_for_manufacture = any([d.bom_no for d in self.get("items")])
|
||||
@@ -862,7 +904,7 @@ class StockEntry(StockController):
|
||||
if d.s_warehouse or d.set_basic_rate_manually:
|
||||
continue
|
||||
|
||||
if d.allow_zero_valuation_rate:
|
||||
if d.allow_zero_valuation_rate and self.purpose != "Receive from Customer":
|
||||
d.basic_rate = 0.0
|
||||
items.append(d.item_code)
|
||||
|
||||
@@ -1086,13 +1128,15 @@ class StockEntry(StockController):
|
||||
self.purpose = frappe.get_cached_value("Stock Entry Type", self.stock_entry_type, "purpose")
|
||||
|
||||
def make_serial_and_batch_bundle_for_outward(self):
|
||||
if self.docstatus == 0:
|
||||
return
|
||||
|
||||
serial_or_batch_items = get_serial_or_batch_items(self.items)
|
||||
if not serial_or_batch_items:
|
||||
return
|
||||
|
||||
serial_nos, batch_nos = self.set_serial_batch_fields_for_subcontracting_inward()
|
||||
|
||||
if self.docstatus == 0:
|
||||
return
|
||||
|
||||
already_picked_serial_nos = []
|
||||
|
||||
for row in self.items:
|
||||
@@ -1118,7 +1162,9 @@ class StockEntry(StockController):
|
||||
"ignore_serial_nos": already_picked_serial_nos,
|
||||
"qty": row.transfer_qty * -1,
|
||||
}
|
||||
).update_serial_and_batch_entries()
|
||||
).update_serial_and_batch_entries(
|
||||
serial_nos=serial_nos.get(row.name), batch_nos=batch_nos.get(row.name)
|
||||
)
|
||||
elif not row.serial_and_batch_bundle:
|
||||
bundle_doc = SerialBatchCreation(
|
||||
{
|
||||
@@ -1133,7 +1179,9 @@ class StockEntry(StockController):
|
||||
"company": self.company,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
).make_serial_and_batch_bundle()
|
||||
).make_serial_and_batch_bundle(
|
||||
serial_nos=serial_nos.get(row.name), batch_nos=batch_nos.get(row.name)
|
||||
)
|
||||
|
||||
if not bundle_doc:
|
||||
continue
|
||||
@@ -1146,6 +1194,32 @@ class StockEntry(StockController):
|
||||
|
||||
row.serial_and_batch_bundle = bundle_doc.name
|
||||
|
||||
def set_serial_batch_fields_for_subcontracting_inward(self):
|
||||
serial_nos, batch_nos = frappe._dict(), frappe._dict()
|
||||
for row in self.items:
|
||||
if self.purpose in [
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
]:
|
||||
if not row.serial_and_batch_bundle:
|
||||
serial_nos_list, batch_nos_list = self.get_serial_nos_and_batches_from_sres(
|
||||
row.scio_detail, only_pending=self.purpose != "Subcontracting Return"
|
||||
)
|
||||
|
||||
if len(batch_nos_list) > 1:
|
||||
row.use_serial_batch_fields = 0
|
||||
|
||||
if row.use_serial_batch_fields:
|
||||
if serial_nos_list and not row.serial_no:
|
||||
row.serial_no = "\n".join(serial_nos_list)
|
||||
if batch_nos_list and not row.batch_no:
|
||||
row.batch_no = next(iter(batch_nos_list.keys()))
|
||||
|
||||
serial_nos[row.name], batch_nos[row.name] = serial_nos_list, batch_nos_list
|
||||
|
||||
return serial_nos, batch_nos
|
||||
|
||||
def validate_subcontract_order(self):
|
||||
"""Throw exception if more raw material is transferred against Subcontract Order than in
|
||||
the raw materials supplied table"""
|
||||
@@ -1329,8 +1403,12 @@ class StockEntry(StockController):
|
||||
)
|
||||
|
||||
def validate_closed_subcontracting_order(self):
|
||||
if self.get("subcontracting_order"):
|
||||
check_on_hold_or_closed_status("Subcontracting Order", self.subcontracting_order)
|
||||
order = self.get("subcontracting_order") or self.get("subcontracting_inward_order")
|
||||
if order:
|
||||
check_on_hold_or_closed_status(
|
||||
"Subcontracting Order" if self.get("subcontracting_order") else "Subcontracting Inward Order",
|
||||
order,
|
||||
)
|
||||
|
||||
def mark_finished_and_scrap_items(self):
|
||||
if self.purpose != "Repack" and any(
|
||||
@@ -1740,12 +1818,12 @@ class StockEntry(StockController):
|
||||
if not pro_doc.operations:
|
||||
pro_doc.set_actual_dates()
|
||||
|
||||
def update_disassembled_order(self, is_cancel=False):
|
||||
def update_disassembled_order(self):
|
||||
if not self.work_order:
|
||||
return
|
||||
if self.purpose == "Disassemble" and self.fg_completed_qty:
|
||||
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
pro_doc.run_method("update_disassembled_qty", self.fg_completed_qty, is_cancel)
|
||||
pro_doc.run_method("update_disassembled_qty", self.fg_completed_qty, self._action == "cancel")
|
||||
|
||||
def make_stock_reserve_for_wip_and_fg(self):
|
||||
if self.is_stock_reserve_for_work_order():
|
||||
@@ -1754,6 +1832,7 @@ class StockEntry(StockController):
|
||||
self.purpose == "Manufacture"
|
||||
and not pro_doc.sales_order
|
||||
and not pro_doc.production_plan_sub_assembly_item
|
||||
and not pro_doc.subcontracting_inward_order
|
||||
):
|
||||
return
|
||||
|
||||
@@ -1774,7 +1853,7 @@ class StockEntry(StockController):
|
||||
def is_stock_reserve_for_work_order(self):
|
||||
if (
|
||||
self.work_order
|
||||
and self.stock_entry_type in ["Material Transfer for Manufacture", "Manufacture"]
|
||||
and self.purpose in ["Material Transfer for Manufacture", "Manufacture"]
|
||||
and frappe.get_cached_value("Work Order", self.work_order, "reserve_stock")
|
||||
):
|
||||
return True
|
||||
@@ -2828,7 +2907,8 @@ class StockEntry(StockController):
|
||||
|
||||
child_qty = flt(item_row["qty"], precision)
|
||||
if not self.is_return and child_qty <= 0 and not item_row.get("is_scrap_item"):
|
||||
continue
|
||||
if self.purpose != "Receive from Customer":
|
||||
continue
|
||||
|
||||
se_child = self.append("items")
|
||||
stock_uom = item_row.get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
|
||||
@@ -2837,7 +2917,7 @@ class StockEntry(StockController):
|
||||
se_child.item_code = item_row.get("item_code") or cstr(d)
|
||||
se_child.uom = item_row["uom"] if item_row.get("uom") else stock_uom
|
||||
se_child.stock_uom = stock_uom
|
||||
se_child.qty = child_qty
|
||||
se_child.qty = child_qty if child_qty > 0 else 0
|
||||
se_child.allow_alternative_item = item_row.get("allow_alternative_item", 0)
|
||||
se_child.subcontracted_item = item_row.get("main_item_code")
|
||||
se_child.cost_center = item_row.get("cost_center") or get_default_cost_center(
|
||||
@@ -2847,6 +2927,7 @@ class StockEntry(StockController):
|
||||
se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
|
||||
se_child.po_detail = item_row.get("po_detail")
|
||||
se_child.sco_rm_detail = item_row.get("sco_rm_detail")
|
||||
se_child.scio_detail = item_row.get("scio_detail")
|
||||
|
||||
for field in [
|
||||
self.subcontract_data.rm_detail_field,
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"sample_quantity",
|
||||
"rates_section",
|
||||
"basic_rate",
|
||||
"customer_provided_item_cost",
|
||||
"additional_cost",
|
||||
"landed_cost_voucher_amount",
|
||||
"valuation_rate",
|
||||
@@ -75,6 +76,7 @@
|
||||
"ste_detail",
|
||||
"po_detail",
|
||||
"sco_rm_detail",
|
||||
"scio_detail",
|
||||
"putaway_rule",
|
||||
"column_break_51",
|
||||
"reference_purchase_receipt",
|
||||
@@ -97,7 +99,8 @@
|
||||
"label": "Source Warehouse",
|
||||
"oldfieldname": "s_warehouse",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Warehouse"
|
||||
"options": "Warehouse",
|
||||
"read_only_depends_on": "eval:in_list([\"Subcontracting Delivery\", \"Return Raw Material to Customer\"], parent.purpose)"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break1",
|
||||
@@ -557,7 +560,8 @@
|
||||
"default": "0",
|
||||
"fieldname": "is_finished_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Finished Item"
|
||||
"label": "Is Finished Item",
|
||||
"read_only_depends_on": "eval:in_list([\"Subcontracting Delivery\", \"Subcontracting Return\"], parent.purpose)"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_card_item",
|
||||
@@ -614,6 +618,26 @@
|
||||
"label": "Landed Cost Voucher Amount",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.purpose == \"Receive from Customer\"",
|
||||
"fieldname": "customer_provided_item_cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Customer Provided Item Cost",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scio_detail",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "SCIO Detail",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -621,7 +645,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-09 10:24:34.717676",
|
||||
"modified": "2025-10-13 12:13:43.389334",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -27,6 +27,7 @@ class StockEntryDetail(Document):
|
||||
bom_no: DF.Link | None
|
||||
conversion_factor: DF.Float
|
||||
cost_center: DF.Link | None
|
||||
customer_provided_item_cost: DF.Currency
|
||||
description: DF.TextEditor | None
|
||||
expense_account: DF.Link | None
|
||||
has_item_scanned: DF.Check
|
||||
@@ -53,6 +54,7 @@ class StockEntryDetail(Document):
|
||||
retain_sample: DF.Check
|
||||
s_warehouse: DF.Link | None
|
||||
sample_quantity: DF.Int
|
||||
scio_detail: DF.Data | None
|
||||
sco_rm_detail: DF.Data | None
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.Text | None
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Purpose",
|
||||
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble",
|
||||
"options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nDisassemble\nReceive from Customer\nReturn Raw Material to Customer\nSubcontracting Delivery\nSubcontracting Return",
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
@@ -36,8 +36,9 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2025-01-15 16:00:22.696958",
|
||||
"modified": "2025-09-04 13:03:31.283348",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Type",
|
||||
@@ -86,9 +87,10 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1,
|
||||
"translated_doctype": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ class StockEntryType(Document):
|
||||
add_to_transit: DF.Check
|
||||
is_standard: DF.Check
|
||||
purpose: DF.Literal[
|
||||
"",
|
||||
"Material Issue",
|
||||
"Material Receipt",
|
||||
"Material Transfer",
|
||||
@@ -31,6 +30,10 @@ class StockEntryType(Document):
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
"Receive from Customer",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
]
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -50,6 +53,10 @@ class StockEntryType(Document):
|
||||
"Repack",
|
||||
"Send to Subcontractor",
|
||||
"Disassemble",
|
||||
"Receive from Customer",
|
||||
"Return Raw Material to Customer",
|
||||
"Subcontracting Delivery",
|
||||
"Subcontracting Return",
|
||||
]:
|
||||
frappe.throw(f"Stock Entry Type {self.name} cannot be set as standard")
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "voucher_type",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "\nSales Order\nWork Order\nProduction Plan",
|
||||
"options": "\nSales Order\nWork Order\nSubcontracting Inward Order\nProduction Plan",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
@@ -288,7 +288,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "From Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order\nProduction Plan",
|
||||
"options": "\nPick List\nPurchase Receipt\nStock Entry\nWork Order\nProduction Plan\nSubcontracting Inward Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1
|
||||
@@ -344,7 +344,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-25 19:48:33.170835",
|
||||
"modified": "2025-10-12 19:48:33.170835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reservation Entry",
|
||||
|
||||
@@ -33,7 +33,13 @@ class StockReservationEntry(Document):
|
||||
from_voucher_detail_no: DF.Data | None
|
||||
from_voucher_no: DF.DynamicLink | None
|
||||
from_voucher_type: DF.Literal[
|
||||
"", "Pick List", "Purchase Receipt", "Stock Entry", "Work Order", "Production Plan"
|
||||
"",
|
||||
"Pick List",
|
||||
"Purchase Receipt",
|
||||
"Stock Entry",
|
||||
"Work Order",
|
||||
"Production Plan",
|
||||
"Subcontracting Inward Order",
|
||||
]
|
||||
has_batch_no: DF.Check
|
||||
has_serial_no: DF.Check
|
||||
@@ -57,7 +63,9 @@ class StockReservationEntry(Document):
|
||||
voucher_detail_no: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_qty: DF.Float
|
||||
voucher_type: DF.Literal["", "Sales Order", "Work Order", "Production Plan"]
|
||||
voucher_type: DF.Literal[
|
||||
"", "Sales Order", "Work Order", "Subcontracting Inward Order", "Production Plan"
|
||||
]
|
||||
warehouse: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -236,12 +244,11 @@ class StockReservationEntry(Document):
|
||||
def validate_reservation_based_on_qty(self) -> None:
|
||||
"""Validates `Reserved Qty` when `Reservation Based On` is `Qty`."""
|
||||
|
||||
if self.reservation_based_on == "Qty":
|
||||
if self.reservation_based_on == "Qty" and self.voucher_type != "Subcontracting Inward Order":
|
||||
self.validate_with_allowed_qty(self.reserved_qty)
|
||||
|
||||
def auto_reserve_serial_and_batch(self, based_on: str | None = None) -> None:
|
||||
"""Auto pick Serial and Batch Nos to reserve when `Reservation Based On` is `Serial and Batch`."""
|
||||
|
||||
if (
|
||||
not self.from_voucher_type
|
||||
and (self.get("_action") == "submit")
|
||||
@@ -409,7 +416,8 @@ class StockReservationEntry(Document):
|
||||
frappe.throw(msg)
|
||||
|
||||
# Should be called after validating Serial and Batch Nos.
|
||||
self.validate_with_allowed_qty(qty_to_be_reserved)
|
||||
if self.voucher_type != "Subcontracting Inward Order":
|
||||
self.validate_with_allowed_qty(qty_to_be_reserved)
|
||||
self.db_set("reserved_qty", qty_to_be_reserved)
|
||||
|
||||
def update_reserved_qty_in_voucher(
|
||||
@@ -1215,15 +1223,25 @@ class StockReservation:
|
||||
|
||||
return available_qty
|
||||
|
||||
def transfer_reservation_entries_to(self, docnames, from_doctype, to_doctype):
|
||||
def transfer_reservation_entries_to(
|
||||
self, docnames, from_doctype, to_doctype, against_fg_item=None, qty_change=None
|
||||
):
|
||||
if isinstance(docnames, str):
|
||||
docnames = [docnames]
|
||||
|
||||
items_to_reserve = self.get_items_to_reserve(docnames, from_doctype, to_doctype)
|
||||
|
||||
if qty_change:
|
||||
for key, value in qty_change.items():
|
||||
row = next((item for item in items_to_reserve if item.voucher_detail_no == key), None)
|
||||
if row:
|
||||
row.qty += value
|
||||
row.required_qty += value
|
||||
|
||||
if not items_to_reserve:
|
||||
return
|
||||
|
||||
reservation_entries = self.get_reserved_entries(from_doctype, docnames)
|
||||
reservation_entries = self.get_reserved_entries(from_doctype, docnames, against_fg_item)
|
||||
if not reservation_entries:
|
||||
return
|
||||
|
||||
@@ -1386,7 +1404,7 @@ class StockReservation:
|
||||
sre.save()
|
||||
sre.submit()
|
||||
|
||||
def get_reserved_entries(self, doctype, docnames):
|
||||
def get_reserved_entries(self, doctype, docnames, against_fg_item=None):
|
||||
if isinstance(docnames, str):
|
||||
docnames = [docnames]
|
||||
|
||||
@@ -1426,6 +1444,17 @@ class StockReservation:
|
||||
.orderby(sabb_entry.idx)
|
||||
)
|
||||
|
||||
if against_fg_item:
|
||||
query = query.where(
|
||||
sre.voucher_detail_no.isin(
|
||||
frappe.get_all(
|
||||
"Subcontracting Inward Order Received Item",
|
||||
{"reference_name": against_fg_item, "docstatus": 1},
|
||||
pluck="name",
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def get_items_to_reserve(self, docnames, from_doctype, to_doctype):
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"column_break_4",
|
||||
"account",
|
||||
"company",
|
||||
"customer",
|
||||
"address_and_contact",
|
||||
"address_html",
|
||||
"column_break_10",
|
||||
@@ -258,6 +259,15 @@
|
||||
"fieldname": "is_rejected_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Rejected Warehouse"
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"depends_on": "eval:!doc.disabled",
|
||||
"description": "Only to be used for Subcontracting Inward.",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -265,7 +275,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-26 11:19:04.673115",
|
||||
"modified": "2025-09-05 14:47:17.140099",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
|
||||
@@ -29,6 +29,7 @@ class Warehouse(NestedSet):
|
||||
address_line_2: DF.Data | None
|
||||
city: DF.Data | None
|
||||
company: DF.Link
|
||||
customer: DF.Link | None
|
||||
default_in_transit_warehouse: DF.Link | None
|
||||
disabled: DF.Check
|
||||
email_id: DF.Data | None
|
||||
|
||||
@@ -337,7 +337,7 @@ def validate_item_details(ctx: ItemDetailsCtx, item):
|
||||
|
||||
throw(_(msg), title=_("Template Item Selected"))
|
||||
|
||||
elif ctx.transaction_type == "buying" and ctx.doctype != "Material Request":
|
||||
elif ctx.doctype != "Material Request":
|
||||
if ctx.is_subcontracted:
|
||||
if ctx.is_old_subcontracting_flow:
|
||||
if item.is_sub_contracted_item != 1:
|
||||
|
||||
@@ -1026,13 +1026,23 @@ class SerialBatchCreation:
|
||||
for d in remove_list:
|
||||
package.remove(d)
|
||||
|
||||
def make_serial_and_batch_bundle(self):
|
||||
def make_serial_and_batch_bundle(
|
||||
self, serial_nos=None, batch_nos=None
|
||||
): # passing None instead of [] due to ruff linter error B006
|
||||
serial_nos = serial_nos or []
|
||||
batch_nos = batch_nos or []
|
||||
|
||||
doc = frappe.new_doc("Serial and Batch Bundle")
|
||||
valid_columns = doc.meta.get_valid_columns()
|
||||
for key, value in self.__dict__.items():
|
||||
if key in valid_columns:
|
||||
doc.set(key, value)
|
||||
|
||||
if serial_nos:
|
||||
self.serial_nos = serial_nos
|
||||
if batch_nos:
|
||||
self.batches = batch_nos
|
||||
|
||||
if self.type_of_transaction == "Outward":
|
||||
self.set_auto_serial_batch_entries_for_outward()
|
||||
elif self.type_of_transaction == "Inward":
|
||||
@@ -1081,10 +1091,21 @@ class SerialBatchCreation:
|
||||
self.batch_no = batches[0]
|
||||
self.serial_nos = self.get_auto_created_serial_nos()
|
||||
|
||||
def update_serial_and_batch_entries(self):
|
||||
def update_serial_and_batch_entries(
|
||||
self, serial_nos=None, batch_nos=None
|
||||
): # passing None instead of [] due to ruff linter error B006
|
||||
serial_nos = serial_nos or []
|
||||
batch_nos = batch_nos or []
|
||||
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", self.serial_and_batch_bundle)
|
||||
doc.type_of_transaction = self.type_of_transaction
|
||||
doc.set("entries", [])
|
||||
|
||||
if serial_nos:
|
||||
self.serial_nos = serial_nos
|
||||
if batch_nos:
|
||||
self.batch_nos = batch_nos
|
||||
|
||||
self.set_auto_serial_batch_entries_for_outward()
|
||||
self.set_serial_batch_entries(doc)
|
||||
if not doc.get("entries"):
|
||||
@@ -1429,3 +1450,28 @@ def get_batchwise_qty(voucher_type, voucher_no):
|
||||
return frappe._dict({})
|
||||
|
||||
return frappe._dict(batches)
|
||||
|
||||
|
||||
def get_serial_batch_list_from_item(item):
|
||||
serial_list, batch_list = [], []
|
||||
if item.serial_and_batch_bundle:
|
||||
table = frappe.qb.DocType("Serial and Batch Entry")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(table.serial_no, table.batch_no)
|
||||
.where(table.parent == item.serial_and_batch_bundle)
|
||||
)
|
||||
result = query.run(as_dict=True)
|
||||
|
||||
for row in result:
|
||||
if row.serial_no and row.serial_no not in serial_list:
|
||||
serial_list.append(row.serial_no)
|
||||
if row.batch_no and row.batch_no not in batch_list:
|
||||
batch_list.append(row.batch_no)
|
||||
else:
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
serial_list = get_serial_nos(item.serial_no) if item.serial_no else []
|
||||
batch_list = [item.batch_no] if item.batch_no else []
|
||||
|
||||
return serial_list, batch_list
|
||||
|
||||
@@ -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