Merge branch 'develop' into subcontracting
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 7e3597e..172ac9e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -572,9 +572,6 @@
},
is_subcontracted: function(frm) {
- if (frm.doc.is_subcontracted) {
- erpnext.buying.get_default_bom(frm);
- }
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
},
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 9f87c5a..181dcc3 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1365,7 +1365,7 @@
"width": "50px"
},
{
- "depends_on": "eval:doc.update_stock && doc.is_subcontracted",
+ "depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 23ad223..5c8ab64 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -513,7 +513,6 @@
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
- self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@@ -1416,7 +1415,6 @@
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
- self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel()
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 3c70e24..02ea3b6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -470,37 +470,6 @@
self.assertEqual(tax.tax_amount, expected_values[i][1])
self.assertEqual(tax.total, expected_values[i][2])
- def test_purchase_invoice_with_subcontracted_item(self):
- wrapper = frappe.copy_doc(test_records[0])
- wrapper.get("items")[0].item_code = "_Test FG Item"
- wrapper.insert()
- wrapper.load_from_db()
-
- expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]]
- for i, item in enumerate(wrapper.get("items")):
- self.assertEqual(item.item_code, expected_values[i][0])
- self.assertEqual(item.item_tax_amount, expected_values[i][1])
- self.assertEqual(item.valuation_rate, expected_values[i][2])
-
- self.assertEqual(wrapper.base_net_total, 1250)
-
- # tax amounts
- expected_values = [
- ["_Test Account Shipping Charges - _TC", 100, 1350],
- ["_Test Account Customs Duty - _TC", 125, 1350],
- ["_Test Account Excise Duty - _TC", 140, 1490],
- ["_Test Account Education Cess - _TC", 2.8, 1492.8],
- ["_Test Account S&H Education Cess - _TC", 1.4, 1494.2],
- ["_Test Account CST - _TC", 29.88, 1524.08],
- ["_Test Account VAT - _TC", 156.25, 1680.33],
- ["_Test Account Discount - _TC", 168.03, 1512.30],
- ]
-
- for i, tax in enumerate(wrapper.get("taxes")):
- self.assertEqual(tax.account_head, expected_values[i][0])
- self.assertEqual(tax.tax_amount, expected_values[i][1])
- self.assertEqual(tax.total, expected_values[i][2])
-
def test_purchase_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -961,30 +930,6 @@
pi.cancel()
self.assertEqual(actual_qty_0, get_qty_after_transaction())
- def test_subcontracting_via_purchase_invoice(self):
- from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
- from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-
- update_backflush_based_on("BOM")
- make_stock_entry(
- item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
- )
- make_stock_entry(
- item_code="_Test Item Home Desktop 100",
- target="_Test Warehouse 1 - _TC",
- qty=100,
- basic_rate=100,
- )
-
- pi = make_purchase_invoice(
- item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1
- )
-
- self.assertEqual(len(pi.get("supplied_items")), 2)
-
- rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
- self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
-
def test_rejected_serial_no(self):
pi = make_purchase_invoice(
item_code="_Test Serialized Item With Series",
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 6651195..dd62886 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -619,7 +619,8 @@
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
- "options": "BOM"
+ "options": "BOM",
+ "read_only": 1
},
{
"default": "0",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index da45610..6aba373 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -7,17 +7,6 @@
frappe.ui.form.on("Purchase Order", {
setup: function(frm) {
-
- frm.set_query("reserve_warehouse", "supplied_items", function() {
- return {
- filters: {
- "company": frm.doc.company,
- "name": ['!=', frm.doc.supplier_warehouse],
- "is_group": 0
- }
- }
- });
-
frm.set_indicator_formatter('item_code',
function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" })
@@ -28,6 +17,11 @@
}
});
+ frm.set_query("fg_item", "items", function() {
+ return {
+ filters: {'is_sub_contracted_item': 1}
+ }
+ });
},
company: function(frm) {
@@ -52,39 +46,6 @@
frm.set_value("tax_withholding_category", frm.supplier_tds);
}
},
-
- refresh: function(frm) {
- frm.trigger('get_materials_from_supplier');
- },
-
- get_materials_from_supplier: function(frm) {
- let po_details = [];
-
- if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
- frm.doc.supplied_items.forEach(d => {
- if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
- po_details.push(d.name)
- }
- });
- }
-
- if (po_details && po_details.length) {
- frm.add_custom_button(__('Return of Components'), () => {
- frm.call({
- method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
- freeze: true,
- freeze_message: __('Creating Stock Entry'),
- args: { purchase_order: frm.doc.name, po_details: po_details },
- callback: function(r) {
- if (r && r.message) {
- const doc = frappe.model.sync(r.message);
- frappe.set_route("Form", doc[0].doctype, doc[0].name);
- }
- }
- });
- }, __('Create'));
- }
- }
});
frappe.ui.form.on("Purchase Order Item", {
@@ -105,12 +66,11 @@
this.frm.custom_make_buttons = {
'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Purchase Invoice',
- 'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment',
+ 'Subcontracting Order': 'Subcontracting Order'
}
super.setup();
-
}
refresh(doc, cdt, cdn) {
@@ -177,9 +137,8 @@
if (doc.status != "On Hold") {
if(flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
- if(doc.is_subcontracted && me.has_unsupplied_items()) {
- cur_frm.add_custom_button(__('Material to Supplier'),
- function() { me.make_stock_entry(); }, __("Transfer"));
+ if (doc.is_subcontracted) {
+ cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create'));
}
}
if(flt(doc.per_billed) < 100)
@@ -247,142 +206,6 @@
set_schedule_date(this.frm);
}
- has_unsupplied_items() {
- return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
- }
-
- make_stock_entry() {
- var items = $.map(cur_frm.doc.items, function(d) { return d.bom ? d.item_code : false; });
- var me = this;
-
- if(items.length >= 1){
- me.raw_material_data = [];
- me.show_dialog = 1;
- let title = __('Transfer Material to Supplier');
- let fields = [
- {fieldtype:'Section Break', label: __('Raw Materials')},
- {fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'),
- fields: [
- {
- fieldtype:'Data',
- fieldname:'item_code',
- label: __('Item'),
- read_only:1,
- in_list_view:1
- },
- {
- fieldtype:'Data',
- fieldname:'rm_item_code',
- label: __('Raw Material'),
- read_only:1,
- in_list_view:1
- },
- {
- fieldtype:'Float',
- read_only:1,
- fieldname:'qty',
- label: __('Quantity'),
- read_only:1,
- in_list_view:1
- },
- {
- fieldtype:'Data',
- read_only:1,
- fieldname:'warehouse',
- label: __('Reserve Warehouse'),
- in_list_view:1
- },
- {
- fieldtype:'Float',
- read_only:1,
- fieldname:'rate',
- label: __('Rate'),
- hidden:1
- },
- {
- fieldtype:'Float',
- read_only:1,
- fieldname:'amount',
- label: __('Amount'),
- hidden:1
- },
- {
- fieldtype:'Link',
- read_only:1,
- fieldname:'uom',
- label: __('UOM'),
- hidden:1
- }
- ],
- data: me.raw_material_data,
- get_data: function() {
- return me.raw_material_data;
- }
- }
- ]
-
- me.dialog = new frappe.ui.Dialog({
- title: title, fields: fields
- });
-
- if (me.frm.doc['supplied_items']) {
- me.frm.doc['supplied_items'].forEach((item, index) => {
- if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) {
- me.raw_material_data.push ({
- 'name':item.name,
- 'item_code': item.main_item_code,
- 'rm_item_code': item.rm_item_code,
- 'item_name': item.rm_item_code,
- 'qty': item.required_qty - item.supplied_qty,
- 'warehouse':item.reserve_warehouse,
- 'rate':item.rate,
- 'amount':item.amount,
- 'stock_uom':item.stock_uom
- });
- me.dialog.fields_dict.sub_con_rm_items.grid.refresh();
- }
- })
- }
-
- me.dialog.get_field('sub_con_rm_items').check_all_rows()
-
- me.dialog.show()
- this.dialog.set_primary_action(__('Transfer'), function() {
- me.values = me.dialog.get_values();
- if(me.values) {
- me.values.sub_con_rm_items.map((row,i) => {
- if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
- let row_id = i+1;
- frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id]));
- }
- })
- me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
- me.dialog.hide()
- }
- });
- }
-
- me.dialog.get_close_btn().on('click', () => {
- me.dialog.hide();
- });
-
- }
-
- _make_rm_stock_entry(rm_items) {
- frappe.call({
- method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry",
- args: {
- purchase_order: cur_frm.doc.name,
- rm_items: rm_items
- }
- ,
- callback: function(r) {
- var doclist = frappe.model.sync(r.message);
- frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
- }
- });
- }
-
make_inter_company_order(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order",
@@ -405,6 +228,14 @@
})
}
+ make_subcontracting_order() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order",
+ frm: cur_frm,
+ freeze_message: __("Creating Subcontracting Order ...")
+ })
+ }
+
add_from_mappers() {
var me = this;
this.frm.add_custom_button(__('Material Request'),
@@ -613,18 +444,6 @@
}
}
-cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) {
- var d = locals[cdt][cdn]
- return {
- filters: [
- ['BOM', 'item', '=', d.item_code],
- ['BOM', 'is_active', '=', '1'],
- ['BOM', 'docstatus', '=', '1'],
- ['BOM', 'company', '=', doc.company]
- ]
- }
-}
-
function set_schedule_date(frm) {
if(frm.doc.schedule_date){
erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "items", "schedule_date");
@@ -632,9 +451,3 @@
}
frappe.provide("erpnext.buying");
-
-frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) {
- if (frm.doc.is_subcontracted) {
- erpnext.buying.get_default_bom(frm);
- }
-});
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index b365a83..07320d0 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -16,6 +16,8 @@
"supplier_name",
"apply_tds",
"tax_withholding_category",
+ "is_subcontracted",
+ "supplier_warehouse",
"column_break1",
"company",
"transaction_date",
@@ -55,10 +57,7 @@
"price_list_currency",
"plc_conversion_rate",
"ignore_pricing_rule",
- "sec_warehouse",
- "is_subcontracted",
- "col_break_warehouse",
- "supplier_warehouse",
+ "section_break_45",
"before_items_section",
"scan_barcode",
"items_col_break",
@@ -158,7 +157,8 @@
"hidden": 1,
"label": "Title",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "reqd": 1
},
{
"fieldname": "naming_series",
@@ -444,11 +444,6 @@
"print_hide": 1
},
{
- "fieldname": "sec_warehouse",
- "fieldtype": "Section Break",
- "label": "Subcontracting"
- },
- {
"description": "Sets 'Warehouse' in each row of the Items table.",
"fieldname": "set_warehouse",
"fieldtype": "Link",
@@ -464,7 +459,6 @@
"default": "0",
"fieldname": "is_subcontracted",
"fieldtype": "Check",
- "in_standard_filter": 1,
"label": "Is Subcontracted",
"print_hide": 1
},
@@ -1143,6 +1137,10 @@
"options": "Tax Withholding Category"
},
{
+ "fieldname": "section_break_45",
+ "fieldtype": "Section Break"
+ },
+ {
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -1169,7 +1167,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-04-26 12:16:38.694276",
+ "modified": "2022-04-26 18:46:58.863174",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 44426ba..e0cc43f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -24,7 +24,6 @@
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
-from erpnext.stock.utils import get_bin
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -69,8 +68,7 @@
self.validate_with_previous_doc()
self.validate_for_subcontracting()
self.validate_minimum_order_qty()
- self.validate_bom_for_subcontracting_items()
- self.create_raw_materials_supplied("supplied_items")
+ self.validate_fg_item_for_subcontracting()
self.set_received_qty_for_drop_ship_items()
validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_order_reference
@@ -193,12 +191,25 @@
).format(item_code, qty, itemwise_min_order_qty.get(item_code))
)
- def validate_bom_for_subcontracting_items(self):
+ def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted:
for item in self.items:
- if not item.bom:
+ if not item.fg_item:
frappe.throw(
- _("BOM is not specified for subcontracting item {0} at row {1}").format(
+ _("Finished Good Item is not specified for service item {0} at row {1}").format(
+ item.item_code, item.idx
+ )
+ )
+ else:
+ if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
+ frappe.throw(
+ _(
+ "Finished Good Item {0} must be a sub-contracted item for service item {1} at row {2}"
+ ).format(item.fg_item, item.item_code, item.idx)
+ )
+ if not item.fg_item_qty:
+ frappe.throw(
+ _("Finished Good Item Qty is not specified for service item {0} at row {1}").format(
item.item_code, item.idx
)
)
@@ -294,9 +305,6 @@
self.set_status(update=True, status=status)
self.update_requested_qty()
self.update_ordered_qty()
- if self.is_subcontracted:
- self.update_reserved_qty_for_subcontract()
-
self.notify_update()
clear_doctype_notifications(self)
@@ -311,9 +319,6 @@
self.update_ordered_qty()
self.validate_budget()
- if self.is_subcontracted:
- self.update_reserved_qty_for_subcontract()
-
frappe.get_doc("Authorization Control").validate_approving_authority(
self.doctype, self.company, self.base_grand_total
)
@@ -332,9 +337,6 @@
if self.has_drop_ship_item():
self.update_delivered_qty_in_sales_order()
- if self.is_subcontracted:
- self.update_reserved_qty_for_subcontract()
-
self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled")
@@ -404,12 +406,6 @@
if item.delivered_by_supplier == 1:
item.received_qty = item.qty
- def update_reserved_qty_for_subcontract(self):
- for d in self.supplied_items:
- if d.rm_item_code:
- stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse)
- stock_bin.update_reserved_qty_for_sub_contracting()
-
def update_receiving_percentage(self):
total_qty, received_qty = 0.0, 0.0
for item in self.items:
@@ -587,80 +583,6 @@
return doc
-@frappe.whitelist()
-def make_rm_stock_entry(purchase_order, rm_items):
- rm_items_list = rm_items
-
- if isinstance(rm_items, str):
- rm_items_list = json.loads(rm_items)
- elif not rm_items:
- frappe.throw(_("No Items available for transfer"))
-
- if rm_items_list:
- fg_items = list(set(d["item_code"] for d in rm_items_list))
- else:
- frappe.throw(_("No Items selected for transfer"))
-
- if purchase_order:
- purchase_order = frappe.get_doc("Purchase Order", purchase_order)
-
- if fg_items:
- items = tuple(set(d["rm_item_code"] for d in rm_items_list))
- item_wh = get_item_details(items)
-
- stock_entry = frappe.new_doc("Stock Entry")
- stock_entry.purpose = "Send to Subcontractor"
- stock_entry.purchase_order = purchase_order.name
- stock_entry.supplier = purchase_order.supplier
- stock_entry.supplier_name = purchase_order.supplier_name
- stock_entry.supplier_address = purchase_order.supplier_address
- stock_entry.address_display = purchase_order.address_display
- stock_entry.company = purchase_order.company
- stock_entry.to_warehouse = purchase_order.supplier_warehouse
- stock_entry.set_stock_entry_type()
-
- for item_code in fg_items:
- for rm_item_data in rm_items_list:
- if rm_item_data["item_code"] == item_code:
- rm_item_code = rm_item_data["rm_item_code"]
- items_dict = {
- rm_item_code: {
- "po_detail": rm_item_data.get("name"),
- "item_name": rm_item_data["item_name"],
- "description": item_wh.get(rm_item_code, {}).get("description", ""),
- "qty": rm_item_data["qty"],
- "from_warehouse": rm_item_data["warehouse"],
- "stock_uom": rm_item_data["stock_uom"],
- "serial_no": rm_item_data.get("serial_no"),
- "batch_no": rm_item_data.get("batch_no"),
- "main_item_code": rm_item_data["item_code"],
- "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
- }
- }
- stock_entry.add_to_stock_entry_detail(items_dict)
-
- stock_entry.set_missing_values()
- return stock_entry.as_dict()
- else:
- frappe.throw(_("No Items selected for transfer"))
- return purchase_order.name
-
-
-def get_item_details(items):
- item_details = {}
- for d in frappe.db.sql(
- """select item_code, description, allow_alternative_item from `tabItem`
- where name in ({0})""".format(
- ", ".join(["%s"] * len(items))
- ),
- items,
- as_dict=1,
- ):
- item_details[d.item_code] = d
-
- return item_details
-
-
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@@ -691,61 +613,40 @@
@frappe.whitelist()
-def get_materials_from_supplier(purchase_order, po_details):
- if isinstance(po_details, str):
- po_details = json.loads(po_details)
-
- doc = frappe.get_cached_doc("Purchase Order", purchase_order)
- doc.initialized_fields()
- doc.purchase_orders = [doc.name]
- doc.get_available_materials()
-
- if not doc.available_materials:
- frappe.throw(
- _("Materials are already received against the purchase order {0}").format(purchase_order)
- )
-
- return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
+def make_subcontracting_order(source_name, target_doc=None):
+ return get_mapped_subcontracting_order(source_name, target_doc)
-def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
- ste_doc = frappe.new_doc("Stock Entry")
- ste_doc.purpose = "Material Transfer"
- ste_doc.purchase_order = po_doc.name
- ste_doc.company = po_doc.company
- ste_doc.is_return = 1
+def get_mapped_subcontracting_order(source_name, target_doc=None):
- for key, value in available_materials.items():
- if not value.qty:
- continue
+ if target_doc and isinstance(target_doc, str):
+ target_doc = json.loads(target_doc)
+ for key in ["service_items", "items", "supplied_items"]:
+ if key in target_doc:
+ del target_doc[key]
+ target_doc = json.dumps(target_doc)
- if value.batch_no:
- for batch_no, qty in value.batch_no.items():
- if qty > 0:
- add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
- else:
- add_items_in_ste(ste_doc, value, value.qty, po_details)
-
- ste_doc.set_stock_entry_type()
- ste_doc.set_missing_values()
-
- return ste_doc
-
-
-def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
- item = ste_doc.append("items", row.item_details)
-
- po_detail = list(set(row.po_details).intersection(po_details))
- item.update(
+ target_doc = get_mapped_doc(
+ "Purchase Order",
+ source_name,
{
- "qty": qty,
- "batch_no": batch_no,
- "basic_rate": row.item_details["rate"],
- "po_detail": po_detail[0] if po_detail else "",
- "s_warehouse": row.item_details["t_warehouse"],
- "t_warehouse": row.item_details["s_warehouse"],
- "item_code": row.item_details["rm_item_code"],
- "subcontracted_item": row.item_details["main_item_code"],
- "serial_no": "\n".join(row.serial_no) if row.serial_no else "",
- }
+ "Purchase Order": {
+ "doctype": "Subcontracting Order",
+ "field_map": {},
+ "field_no_map": ["total_qty", "total", "net_total"],
+ "validation": {
+ "docstatus": ["=", 1],
+ },
+ },
+ "Purchase Order Item": {
+ "doctype": "Subcontracting Order Service Item",
+ "field_map": {},
+ "field_no_map": [],
+ },
+ },
+ target_doc,
)
+
+ target_doc.populate_items_table()
+
+ return target_doc
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
index 81f2010..0c38c3e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
@@ -22,6 +22,6 @@
"label": _("Reference"),
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
},
- {"label": _("Sub-contracting"), "items": ["Stock Entry"]},
+ {"label": _("Sub-contracting"), "items": ["Subcontracting Order"]},
],
}
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 1a7f2dd..0bebafe 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -13,9 +13,6 @@
make_purchase_invoice as make_pi_from_po,
)
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
-from erpnext.buying.doctype.purchase_order.purchase_order import (
- make_rm_stock_entry as make_subcontract_transfer_entry,
-)
from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.item.test_item import make_item
@@ -24,7 +21,6 @@
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
-from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPurchaseOrder(FrappeTestCase):
@@ -389,31 +385,6 @@
new_item_with_tax.delete()
frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
- def test_update_child_uom_conv_factor_change(self):
- po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
- total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
-
- trans_item = json.dumps(
- [
- {
- "item_code": po.get("items")[0].item_code,
- "rate": po.get("items")[0].rate,
- "qty": po.get("items")[0].qty,
- "uom": "_Test UOM 1",
- "conversion_factor": 2,
- "docname": po.get("items")[0].name,
- }
- ]
- )
- update_child_qty_rate("Purchase Order", trans_item, po.name)
- po.reload()
-
- total_reqd_qty_after_change = sum(
- d.get("required_qty") for d in po.as_dict().get("supplied_items")
- )
-
- self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
-
def test_update_qty(self):
po = create_purchase_order()
@@ -572,10 +543,6 @@
)
automatically_fetch_payment_terms(enable=0)
- def test_subcontracting(self):
- po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
- self.assertEqual(len(po.get("supplied_items")), 2)
-
def test_warehouse_company_validation(self):
from erpnext.stock.utils import InvalidWarehouseCompany
@@ -740,379 +707,6 @@
pi.insert()
self.assertTrue(pi.get("payment_schedule"))
- def test_reserved_qty_subcontract_po(self):
- # Make stock available for raw materials
- make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse 1 - _TC",
- item_code="_Test Item Home Desktop 100",
- qty=30,
- basic_rate=100,
- )
-
- bin1 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
- as_dict=1,
- )
-
- # Submit PO
- po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
-
- bin2 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
- as_dict=1,
- )
-
- self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
- self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
- self.assertNotEqual(bin1.modified, bin2.modified)
-
- # Create stock transfer
- rm_item = [
- {
- "item_code": "_Test FG Item",
- "rm_item_code": "_Test Item",
- "item_name": "_Test Item",
- "qty": 6,
- "warehouse": "_Test Warehouse - _TC",
- "rate": 100,
- "amount": 600,
- "stock_uom": "Nos",
- }
- ]
- rm_item_string = json.dumps(rm_item)
- se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
- se.to_warehouse = "_Test Warehouse 1 - _TC"
- se.save()
- se.submit()
-
- bin3 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
-
- # close PO
- po.update_status("Closed")
- bin4 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
-
- # Re-open PO
- po.update_status("Submitted")
- bin5 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
-
- make_stock_entry(
- target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse 1 - _TC",
- item_code="_Test Item Home Desktop 100",
- qty=40,
- basic_rate=100,
- )
-
- # make Purchase Receipt against PO
- pr = make_purchase_receipt(po.name)
- pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
- pr.save()
- pr.submit()
-
- bin6 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
-
- # Cancel PR
- pr.cancel()
- bin7 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
-
- # Make Purchase Invoice
- pi = make_pi_from_po(po.name)
- pi.update_stock = 1
- pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
- pi.insert()
- pi.submit()
- bin8 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
-
- # Cancel PR
- pi.cancel()
- bin9 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
-
- # Cancel Stock Entry
- se.cancel()
- bin10 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
-
- # Cancel PO
- po.reload()
- po.cancel()
- bin11 = frappe.db.get_value(
- "Bin",
- filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
- fieldname="reserved_qty_for_sub_contract",
- as_dict=1,
- )
-
- self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
-
- def test_exploded_items_in_subcontracted(self):
- item_code = "_Test Subcontracted FG Item 11"
- make_subcontracted_item(item_code=item_code)
-
- po = create_purchase_order(
- item_code=item_code,
- qty=1,
- is_subcontracted=1,
- supplier_warehouse="_Test Warehouse 1 - _TC",
- include_exploded_items=1,
- )
-
- name = frappe.db.get_value("BOM", {"item": item_code}, "name")
- bom = frappe.get_doc("BOM", name)
-
- exploded_items = sorted(
- [d.item_code for d in bom.exploded_items if not d.get("sourced_by_supplier")]
- )
- supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
- self.assertEqual(exploded_items, supplied_items)
-
- po1 = create_purchase_order(
- item_code=item_code,
- qty=1,
- is_subcontracted=1,
- supplier_warehouse="_Test Warehouse 1 - _TC",
- include_exploded_items=0,
- )
-
- supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items])
- bom_items = sorted([d.item_code for d in bom.items if not d.get("sourced_by_supplier")])
-
- self.assertEqual(supplied_items1, bom_items)
-
- def test_backflush_based_on_stock_entry(self):
- item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code=item_code)
- make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1})
-
- update_backflush_based_on("Material Transferred for Subcontract")
-
- order_qty = 5
- po = create_purchase_order(
- item_code=item_code,
- qty=order_qty,
- is_subcontracted=1,
- supplier_warehouse="_Test Warehouse 1 - _TC",
- )
-
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="Sub Contracted Raw Material 1",
- qty=10,
- basic_rate=100,
- )
-
- rm_items = [
- {
- "item_code": item_code,
- "rm_item_code": "Sub Contracted Raw Material 1",
- "item_name": "_Test Item",
- "qty": 10,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- },
- {
- "item_code": item_code,
- "rm_item_code": "_Test Item Home Desktop 100",
- "item_name": "_Test Item Home Desktop 100",
- "qty": 20,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- },
- {
- "item_code": item_code,
- "rm_item_code": "Test Extra Item 1",
- "item_name": "Test Extra Item 1",
- "qty": 10,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- },
- {
- "item_code": item_code,
- "rm_item_code": "Test Extra Item 2",
- "stock_uom": "Nos",
- "qty": 10,
- "warehouse": "_Test Warehouse - _TC",
- "item_name": "Test Extra Item 2",
- },
- ]
-
- rm_item_string = json.dumps(rm_items)
- se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
- se.submit()
-
- pr = make_purchase_receipt(po.name)
-
- received_qty = 2
- # partial receipt
- pr.get("items")[0].qty = received_qty
- pr.save()
- pr.submit()
-
- transferred_items = sorted(
- [d.item_code for d in se.get("items") if se.purchase_order == po.name]
- )
- issued_items = sorted([d.rm_item_code for d in pr.get("supplied_items")])
-
- self.assertEqual(transferred_items, issued_items)
- self.assertEqual(pr.get("items")[0].rm_supp_cost, 2000)
-
- transferred_rm_map = frappe._dict()
- for item in rm_items:
- transferred_rm_map[item.get("rm_item_code")] = item
-
- update_backflush_based_on("BOM")
-
- def test_supplied_qty_against_subcontracted_po(self):
- item_code = "_Test Subcontracted FG Item 5"
- make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1})
-
- make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
-
- update_backflush_based_on("Material Transferred for Subcontract")
-
- order_qty = 250
- po = create_purchase_order(
- item_code=item_code,
- qty=order_qty,
- is_subcontracted=1,
- supplier_warehouse="_Test Warehouse 1 - _TC",
- do_not_save=True,
- )
-
- # Add same subcontracted items multiple times
- po.append(
- "items",
- {
- "item_code": item_code,
- "qty": order_qty,
- "schedule_date": add_days(nowdate(), 1),
- "warehouse": "_Test Warehouse - _TC",
- },
- )
-
- po.set_missing_values()
- po.submit()
-
- # Material receipt entry for the raw materials which will be send to supplier
- make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="Sub Contracted Raw Material 4",
- qty=500,
- basic_rate=100,
- )
-
- rm_items = [
- {
- "item_code": item_code,
- "rm_item_code": "Sub Contracted Raw Material 4",
- "item_name": "_Test Item",
- "qty": 250,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- "name": po.supplied_items[0].name,
- },
- {
- "item_code": item_code,
- "rm_item_code": "Sub Contracted Raw Material 4",
- "item_name": "_Test Item",
- "qty": 250,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- },
- ]
-
- # Raw Materials transfer entry from stores to supplier's warehouse
- rm_item_string = json.dumps(rm_items)
- se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
- se.submit()
-
- # Test po_detail field has value or not
- for item_row in se.items:
- self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name)
-
- po_doc = frappe.get_doc("Purchase Order", po.name)
- for row in po_doc.supplied_items:
- # Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
- self.assertEqual(row.supplied_qty, 250.0)
-
- update_backflush_based_on("BOM")
-
def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -1211,50 +805,6 @@
return pr
-def make_subcontracted_item(**args):
- from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
-
- args = frappe._dict(args)
-
- if not frappe.db.exists("Item", args.item_code):
- make_item(
- args.item_code,
- {
- "is_stock_item": 1,
- "is_sub_contracted_item": 1,
- "has_batch_no": args.get("has_batch_no") or 0,
- },
- )
-
- if not args.raw_materials:
- if not frappe.db.exists("Item", "Test Extra Item 1"):
- make_item(
- "Test Extra Item 1",
- {
- "is_stock_item": 1,
- },
- )
-
- if not frappe.db.exists("Item", "Test Extra Item 2"):
- make_item(
- "Test Extra Item 2",
- {
- "is_stock_item": 1,
- },
- )
-
- args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
-
- if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
- make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
-
-
-def update_backflush_based_on(based_on):
- doc = frappe.get_doc("Buying Settings")
- doc.backflush_raw_materials_of_subcontract_based_on = based_on
- doc.save()
-
-
def get_same_items():
return [
{
diff --git a/erpnext/buying/doctype/purchase_order/test_records.json b/erpnext/buying/doctype/purchase_order/test_records.json
index 896050c..4df994a 100644
--- a/erpnext/buying/doctype/purchase_order/test_records.json
+++ b/erpnext/buying/doctype/purchase_order/test_records.json
@@ -8,40 +8,6 @@
"doctype": "Purchase Order",
"base_grand_total": 5000.0,
"grand_total": 5000.0,
- "is_subcontracted": 1,
- "naming_series": "_T-Purchase Order-",
- "base_net_total": 5000.0,
- "items": [
- {
- "base_amount": 5000.0,
- "conversion_factor": 1.0,
- "description": "_Test FG Item",
- "doctype": "Purchase Order Item",
- "item_code": "_Test FG Item",
- "item_name": "_Test FG Item",
- "parentfield": "items",
- "qty": 10.0,
- "rate": 500.0,
- "schedule_date": "2013-03-01",
- "stock_uom": "_Test UOM",
- "uom": "_Test UOM",
- "warehouse": "_Test Warehouse - _TC"
- }
- ],
- "supplier": "_Test Supplier",
- "supplier_name": "_Test Supplier",
- "transaction_date": "2013-02-12",
- "schedule_date": "2013-02-13"
- },
- {
- "advance_paid": 0.0,
- "buying_price_list": "_Test Price List",
- "company": "_Test Company",
- "conversion_rate": 1.0,
- "currency": "INR",
- "doctype": "Purchase Order",
- "base_grand_total": 5000.0,
- "grand_total": 5000.0,
"is_subcontracted": 0,
"naming_series": "_T-Purchase Order-",
"base_net_total": 5000.0,
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index f72c598..12eef79 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -11,6 +11,8 @@
"supplier_part_no",
"item_name",
"product_bundle",
+ "fg_item",
+ "fg_item_qty",
"column_break_4",
"schedule_date",
"expected_delivery_date",
@@ -572,18 +574,18 @@
"read_only": 1
},
{
- "depends_on": "eval:parent.is_subcontracted",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
"options": "BOM",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"default": "0",
- "depends_on": "eval:parent.is_subcontracted",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Include Exploded Items",
"print_hide": 1
},
@@ -845,13 +847,29 @@
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "depends_on": "eval:parent.is_subcontracted",
+ "fieldname": "fg_item",
+ "fieldtype": "Link",
+ "label": "Finished Good Item",
+ "mandatory_depends_on": "eval:parent.is_subcontracted",
+ "options": "Item"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:parent.is_subcontracted",
+ "fieldname": "fg_item_qty",
+ "fieldtype": "Float",
+ "label": "Finished Good Item Qty",
+ "mandatory_depends_on": "eval:parent.is_subcontracted"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-02-02 13:10:18.398976",
+ "modified": "2022-04-07 14:53:16.684010",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
index 6889322..57a41ad 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -14,32 +14,31 @@
},
{
label: __("From Date"),
- fieldname:"from_date",
+ fieldname: "from_date",
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
label: __("To Date"),
- fieldname:"to_date",
+ fieldname: "to_date",
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1
},
{
- label: __("Purchase Order"),
+ label: __("Subcontracting Order"),
fieldname: "name",
fieldtype: "Link",
- options: "Purchase Order",
- get_query: function() {
+ options: "Subcontracting Order",
+ get_query: function () {
return {
filters: {
docstatus: 1,
- is_subcontracted: 1,
company: frappe.query_report.get_filter_value('company')
}
}
}
}
]
-};
+};
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
index 526a8d8..7861e49 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
@@ -15,7 +15,7 @@
"name": "Subcontract Order Summary",
"owner": "Administrator",
"prepared_report": 0,
- "ref_doctype": "Purchase Order",
+ "ref_doctype": "Subcontracting Order",
"report_name": "Subcontract Order Summary",
"report_type": "Script Report",
"roles": [
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
index 3d66637..3750daa 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -20,34 +20,33 @@
if orders:
supplied_items = get_supplied_items(orders, report_filters)
- po_details = prepare_subcontracted_data(orders, supplied_items)
- get_subcontracted_data(po_details, data)
+ sco_details = prepare_subcontracted_data(orders, supplied_items)
+ get_subcontracted_data(sco_details, data)
return data
def get_subcontracted_orders(report_filters):
fields = [
- "`tabPurchase Order Item`.`parent` as po_id",
- "`tabPurchase Order Item`.`item_code`",
- "`tabPurchase Order Item`.`item_name`",
- "`tabPurchase Order Item`.`qty`",
- "`tabPurchase Order Item`.`name`",
- "`tabPurchase Order Item`.`received_qty`",
- "`tabPurchase Order`.`status`",
+ "`tabSubcontracting Order Item`.`parent` as sco_id",
+ "`tabSubcontracting Order Item`.`item_code`",
+ "`tabSubcontracting Order Item`.`item_name`",
+ "`tabSubcontracting Order Item`.`qty`",
+ "`tabSubcontracting Order Item`.`name`",
+ "`tabSubcontracting Order Item`.`received_qty`",
+ "`tabSubcontracting Order`.`status`",
]
filters = get_filters(report_filters)
- return frappe.get_all("Purchase Order", fields=fields, filters=filters) or []
+ return frappe.get_all("Subcontracting Order", fields=fields, filters=filters) or []
def get_filters(report_filters):
filters = [
- ["Purchase Order", "docstatus", "=", 1],
- ["Purchase Order", "is_subcontracted", "=", 1],
+ ["Subcontracting Order", "docstatus", "=", 1],
[
- "Purchase Order",
+ "Subcontracting Order",
"transaction_date",
"between",
(report_filters.from_date, report_filters.to_date),
@@ -56,7 +55,7 @@
for field in ["name", "company"]:
if report_filters.get(field):
- filters.append(["Purchase Order", field, "=", report_filters.get(field)])
+ filters.append(["Subcontracting Order", field, "=", report_filters.get(field)])
return filters
@@ -71,16 +70,15 @@
"rm_item_code",
"required_qty",
"supplied_qty",
- "returned_qty",
"total_supplied_qty",
"consumed_qty",
"reference_name",
]
- filters = {"parent": ("in", [d.po_id for d in orders]), "docstatus": 1}
+ filters = {"parent": ("in", [d.sco_id for d in orders]), "docstatus": 1}
supplied_items = {}
- for row in frappe.get_all("Purchase Order Item Supplied", fields=fields, filters=filters):
+ for row in frappe.get_all("Subcontracting Order Supplied Item", fields=fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
@@ -89,24 +87,24 @@
def prepare_subcontracted_data(orders, supplied_items):
- po_details = {}
+ sco_details = {}
for row in orders:
- key = (row.po_id, row.name, row.item_code)
- if key not in po_details:
- po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []}))
+ key = (row.sco_id, row.name, row.item_code)
+ if key not in sco_details:
+ sco_details.setdefault(key, frappe._dict({"sco_item": row, "supplied_items": []}))
- details = po_details[key]
+ details = sco_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details["supplied_items"].append(supplied_item)
- return po_details
+ return sco_details
-def get_subcontracted_data(po_details, data):
- for key, details in po_details.items():
- res = details.po_item
+def get_subcontracted_data(sco_details, data):
+ for key, details in sco_details.items():
+ res = details.sco_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
@@ -118,10 +116,10 @@
def get_columns():
return [
{
- "label": _("Purchase Order"),
- "fieldname": "po_id",
+ "label": _("Subcontracting Order"),
+ "fieldname": "sco_id",
"fieldtype": "Link",
- "options": "Purchase Order",
+ "options": "Subcontracting Order",
"width": 100,
},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80},
@@ -144,5 +142,4 @@
{"label": _("Required Qty"), "fieldname": "required_qty", "fieldtype": "Float", "width": 110},
{"label": _("Supplied Qty"), "fieldname": "supplied_qty", "fieldtype": "Float", "width": 110},
{"label": _("Consumed Qty"), "fieldname": "consumed_qty", "fieldtype": "Float", "width": 120},
- {"label": _("Returned Qty"), "fieldname": "returned_qty", "fieldtype": "Float", "width": 110},
]
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py
index 2e90de6..30f9dec 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.py
@@ -19,10 +19,10 @@
def get_columns():
return [
{
- "label": _("Purchase Order"),
+ "label": _("Subcontracting Order"),
"fieldtype": "Link",
- "fieldname": "purchase_order",
- "options": "Purchase Order",
+ "fieldname": "subcontracting_order",
+ "options": "Subcontracting Order",
"width": 150,
},
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150},
@@ -57,14 +57,14 @@
def get_data(data, filters):
- po = get_po(filters)
- po_name = [v.name for v in po]
- sub_items = get_purchase_order_item_supplied(po_name)
+ sco = get_sco(filters)
+ sco_name = [v.name for v in sco]
+ sub_items = get_subcontracting_order_item_supplied(sco_name)
for item in sub_items:
- for order in po:
+ for order in sco:
if order.name == item.parent and item.received_qty < item.qty:
row = {
- "purchase_order": item.parent,
+ "subcontracting_order": item.parent,
"date": order.transaction_date,
"supplier": order.supplier,
"fg_item_code": item.item_code,
@@ -76,22 +76,21 @@
data.append(row)
-def get_po(filters):
+def get_sco(filters):
record_filters = [
- ["is_subcontracted", "=", 1],
["supplier", "=", filters.supplier],
["transaction_date", "<=", filters.to_date],
["transaction_date", ">=", filters.from_date],
["docstatus", "=", 1],
]
return frappe.get_all(
- "Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]
+ "Subcontracting Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]
)
-def get_purchase_order_item_supplied(po):
+def get_subcontracting_order_item_supplied(sco):
return frappe.get_all(
- "Purchase Order Item",
- filters=[("parent", "IN", po)],
+ "Subcontracting Order Item",
+ filters=[("parent", "IN", sco)],
fields=["parent", "item_code", "item_name", "qty", "received_qty"],
)
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
index 57f8741..80fd657 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py
@@ -7,18 +7,35 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
-from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_to_be_received import (
execute,
)
+from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_subcontracting_order,
+ make_service_item,
+)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_subcontracting_receipt,
+)
class TestSubcontractedItemToBeReceived(FrappeTestCase):
def test_pending_and_received_qty(self):
- po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
- transfer_param = []
+ make_service_item("Subcontracted Service Item 1")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 500,
+ "fg_item": "_Test FG Item",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(
+ service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC"
+ )
make_stock_entry(
item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
)
@@ -28,28 +45,27 @@
qty=100,
basic_rate=100,
)
- make_purchase_receipt_against_po(po.name)
- po.reload()
+ make_subcontracting_receipt_against_sco(sco.name)
+ sco.reload()
col, data = execute(
filters=frappe._dict(
{
- "supplier": po.supplier,
+ "supplier": sco.supplier,
"from_date": frappe.utils.get_datetime(
- frappe.utils.add_to_date(po.transaction_date, days=-10)
+ frappe.utils.add_to_date(sco.transaction_date, days=-10)
),
- "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)),
+ "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)),
}
)
)
self.assertEqual(data[0]["pending_qty"], 5)
self.assertEqual(data[0]["received_qty"], 5)
- self.assertEqual(data[0]["purchase_order"], po.name)
- self.assertEqual(data[0]["supplier"], po.supplier)
+ self.assertEqual(data[0]["subcontracting_order"], sco.name)
+ self.assertEqual(data[0]["supplier"], sco.supplier)
-def make_purchase_receipt_against_po(po, quantity=5):
- pr = make_purchase_receipt(po)
- pr.items[0].qty = quantity
- pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
- pr.insert()
- pr.submit()
+def make_subcontracting_receipt_against_sco(sco, quantity=5):
+ scr = make_subcontracting_receipt(sco)
+ scr.items[0].qty = quantity
+ scr.insert()
+ scr.submit()
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
index 6b8a3b1..a837b24 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
@@ -46,10 +46,10 @@
def get_data(filters):
- po_rm_item_details = get_po_items_to_supply(filters)
+ sco_rm_item_details = get_sco_items_to_supply(filters)
data = []
- for row in po_rm_item_details:
+ for row in sco_rm_item_details:
transferred_qty = row.get("transferred_qty") or 0
if transferred_qty < row.get("reqd_qty", 0):
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
@@ -59,23 +59,22 @@
return data
-def get_po_items_to_supply(filters):
+def get_sco_items_to_supply(filters):
return frappe.db.get_all(
- "Purchase Order",
+ "Subcontracting Order",
fields=[
- "name as purchase_order",
+ "name as subcontracting_order",
"transaction_date as date",
"supplier as supplier",
- "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
- "`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
- "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty",
+ "`tabSubcontracting Order Supplied Item`.rm_item_code as rm_item_code",
+ "`tabSubcontracting Order Supplied Item`.required_qty as reqd_qty",
+ "`tabSubcontracting Order Supplied Item`.supplied_qty as transferred_qty",
],
filters=[
- ["Purchase Order", "per_received", "<", "100"],
- ["Purchase Order", "is_subcontracted", "=", 1],
- ["Purchase Order", "supplier", "=", filters.supplier],
- ["Purchase Order", "transaction_date", "<=", filters.to_date],
- ["Purchase Order", "transaction_date", ">=", filters.from_date],
- ["Purchase Order", "docstatus", "=", 1],
+ ["Subcontracting Order", "per_received", "<", "100"],
+ ["Subcontracting Order", "supplier", "=", filters.supplier],
+ ["Subcontracting Order", "transaction_date", "<=", filters.to_date],
+ ["Subcontracting Order", "transaction_date", ">=", filters.from_date],
+ ["Subcontracting Order", "docstatus", "=", 1],
],
)
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
index 2791a26..d29791c 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
@@ -3,24 +3,36 @@
# Compiled at: 2019-05-06 10:24:35
# Decompiled by https://python-decompiler.com
-import json
-
import frappe
from frappe.tests.utils import FrappeTestCase
-from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
-from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import (
execute,
)
+from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_subcontracting_order,
+ make_service_item,
+)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_rm_stock_entry,
+)
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
def test_pending_and_transferred_qty(self):
- po = create_purchase_order(
- item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
+ make_service_item("Subcontracted Service Item 1")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 500,
+ "fg_item": "_Test FG Item",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
# Material Receipt of RMs
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
@@ -28,50 +40,47 @@
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100
)
- se = transfer_subcontracted_raw_materials(po)
+ transfer_subcontracted_raw_materials(sco)
col, data = execute(
filters=frappe._dict(
{
- "supplier": po.supplier,
+ "supplier": sco.supplier,
"from_date": frappe.utils.get_datetime(
- frappe.utils.add_to_date(po.transaction_date, days=-10)
+ frappe.utils.add_to_date(sco.transaction_date, days=-10)
),
- "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)),
+ "to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)),
}
)
)
- po.reload()
+ sco.reload()
- po_data = [row for row in data if row.get("purchase_order") == po.name]
+ sco_data = [row for row in data if row.get("subcontracting_order") == sco.name]
# Alphabetically sort to be certain of order
- po_data = sorted(po_data, key=lambda i: i["rm_item_code"])
+ sco_data = sorted(sco_data, key=lambda i: i["rm_item_code"])
- self.assertEqual(len(po_data), 2)
- self.assertEqual(po_data[0]["purchase_order"], po.name)
+ self.assertEqual(len(sco_data), 2)
+ self.assertEqual(sco_data[0]["subcontracting_order"], sco.name)
- self.assertEqual(po_data[0]["rm_item_code"], "_Test Item")
- self.assertEqual(po_data[0]["p_qty"], 8)
- self.assertEqual(po_data[0]["transferred_qty"], 2)
+ self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item")
+ self.assertEqual(sco_data[0]["p_qty"], 8)
+ self.assertEqual(sco_data[0]["transferred_qty"], 2)
- self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
- self.assertEqual(po_data[1]["p_qty"], 19)
- self.assertEqual(po_data[1]["transferred_qty"], 1)
-
- se.cancel()
- po.cancel()
+ self.assertEqual(sco_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
+ self.assertEqual(sco_data[1]["p_qty"], 19)
+ self.assertEqual(sco_data[1]["transferred_qty"], 1)
-def transfer_subcontracted_raw_materials(po):
- # Order of supplied items fetched in PO is flaky
+def transfer_subcontracted_raw_materials(sco):
+ # Order of supplied items fetched in SCO is flaky
transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1}
- item_1 = po.supplied_items[0].rm_item_code
- item_2 = po.supplied_items[1].rm_item_code
+ item_1 = sco.supplied_items[0].rm_item_code
+ item_2 = sco.supplied_items[1].rm_item_code
- rm_item = [
+ rm_items = [
{
- "name": po.supplied_items[0].name,
+ "name": sco.supplied_items[0].name,
"item_code": item_1,
"rm_item_code": item_1,
"item_name": item_1,
@@ -82,7 +91,7 @@
"stock_uom": "Nos",
},
{
- "name": po.supplied_items[1].name,
+ "name": sco.supplied_items[1].name,
"item_code": item_2,
"rm_item_code": item_2,
"item_name": item_2,
@@ -93,8 +102,7 @@
"stock_uom": "Nos",
},
]
- rm_item_string = json.dumps(rm_item)
- se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
+ se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
se.from_warehouse = "_Test Warehouse - _TC"
se.to_warehouse = "_Test Warehouse - _TC"
se.stock_entry_type = "Send to Subcontractor"
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 854c0d0..7cb34fc 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2657,10 +2657,6 @@
parent.update_ordered_qty()
parent.update_ordered_and_reserved_qty()
parent.update_receiving_percentage()
- if parent.is_subcontracted:
- parent.update_reserved_qty_for_subcontract()
- parent.create_raw_materials_supplied("supplied_items")
- parent.save()
else: # Sales Order
parent.validate_warehouse()
parent.update_reserved_qty()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index f28de3b..fa091df 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -12,7 +12,6 @@
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
-from erpnext.controllers.subcontracting import Subcontracting
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
@@ -21,10 +20,7 @@
pass
-class BuyingController(StockController, Subcontracting):
- def __setup__(self):
- self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
-
+class BuyingController(StockController):
def get_feed(self):
if self.get("supplier_name"):
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
@@ -55,7 +51,6 @@
# sub-contracting
self.validate_for_subcontracting()
- self.create_raw_materials_supplied("supplied_items")
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
@@ -256,12 +251,8 @@
)
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
- item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = (
- item.base_net_amount
- + item.item_tax_amount
- + item.rm_supp_cost
- + flt(item.landed_cost_voucher_amount)
+ item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0
@@ -317,76 +308,15 @@
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
- def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
- supplied_items_cost = 0.0
- for d in self.get("supplied_items"):
- if d.reference_name == item_row_id:
- if reset_outgoing_rate and frappe.get_cached_value("Item", d.rm_item_code, "is_stock_item"):
- rate = get_incoming_rate(
- {
- "item_code": d.rm_item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * d.consumed_qty,
- "serial_no": d.serial_no,
- "batch_no": d.batch_no,
- }
- )
-
- if rate > 0:
- d.rate = rate
-
- d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
- supplied_items_cost += flt(d.amount)
-
- return supplied_items_cost
-
def validate_for_subcontracting(self):
if self.is_subcontracted:
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
-
- for item in self.get("items"):
- if item in self.sub_contracted_items and not item.bom:
- frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
-
- if self.doctype != "Purchase Order":
- return
-
- for row in self.get("supplied_items"):
- if not row.reserve_warehouse:
- msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
- frappe.throw(_(msg))
else:
for item in self.get("items"):
- if item.bom:
+ if item.get("bom"):
item.bom = None
- def create_raw_materials_supplied(self, raw_material_table):
- if self.is_subcontracted:
- self.set_materials_for_subcontracted_items(raw_material_table)
-
- elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- for item in self.get("items"):
- item.rm_supp_cost = 0.0
-
- if not self.is_subcontracted and self.get("supplied_items"):
- self.set("supplied_items", [])
-
- @property
- def sub_contracted_items(self):
- if not hasattr(self, "_sub_contracted_items"):
- self._sub_contracted_items = []
- item_codes = list(set(item.item_code for item in self.get("items")))
- if item_codes:
- items = frappe.get_all(
- "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1}
- )
- self._sub_contracted_items = [item.name for item in items]
-
- return self._sub_contracted_items
-
def set_qty_as_per_stock_uom(self):
for d in self.get("items"):
if d.meta.get_field("stock_qty"):
@@ -510,7 +440,7 @@
sle.update(
{
"incoming_rate": incoming_rate,
- "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0,
+ "recalculate_rate": 1 if (self.is_subcontracted and d.fg_item) or d.from_warehouse else 0,
}
)
sl_entries.append(sle)
@@ -538,7 +468,6 @@
)
)
- self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
allow_negative_stock=allow_negative_stock,
@@ -565,25 +494,6 @@
)
po_obj.update_ordered_qty(po_item_rows)
- if self.is_subcontracted:
- po_obj.update_reserved_qty_for_subcontract()
-
- def make_sl_entries_for_supplier_warehouse(self, sl_entries):
- if hasattr(self, "supplied_items"):
- for d in self.get("supplied_items"):
- # negative quantity is passed, as raw material qty has to be decreased
- # when PR is submitted and it has to be increased when PR is cancelled
- sl_entries.append(
- self.get_sl_entries(
- d,
- {
- "item_code": d.rm_item_code,
- "warehouse": self.supplier_warehouse,
- "actual_qty": -1 * flt(d.consumed_qty),
- "dependant_sle_voucher_detail_no": d.reference_name,
- },
- )
- )
def on_submit(self):
if self.get("is_return"):
@@ -808,10 +718,7 @@
if self.doctype == "Material Request":
return
- if hasattr(self, "is_subcontracted") and self.is_subcontracted:
- validate_item_type(self, "is_sub_contracted_item", "subcontracted")
- else:
- validate_item_type(self, "is_purchase_item", "purchase")
+ validate_item_type(self, "is_purchase_item", "purchase")
def get_asset_item_details(asset_items):
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index d24ac3f..04a0dfa 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -77,7 +77,7 @@
if doc.doctype != "Purchase Invoice":
select_fields += ",serial_no, batch_no"
- if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
+ if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
select_fields += ",rejected_qty, received_qty"
for d in frappe.db.sql(
@@ -161,7 +161,7 @@
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
fields = ["stock_qty"]
- if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
+ if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
fields.extend(["received_qty", "rejected_qty"])
already_returned_data = already_returned_items.get(args.item_code) or {}
@@ -224,7 +224,7 @@
if ref_item_row.get("rate", 0) > item_dict["rate"]:
item_dict["rate"] = ref_item_row.get("rate", 0)
- if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt"]:
+ if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
item_dict["received_qty"] += ref_item_row.received_qty
item_dict["rejected_qty"] += ref_item_row.rejected_qty
@@ -239,7 +239,7 @@
def get_already_returned_items(doc):
column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty"
- if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
+ if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty,
sum(abs(child.received_qty) * child.conversion_factor) as received_qty"""
@@ -281,17 +281,21 @@
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
- if doctype in ("Purchase Receipt", "Purchase Invoice"):
+ if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
party_type = "supplier"
else:
party_type = "customer"
fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
- "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype),
]
- if doctype in ("Purchase Receipt", "Purchase Invoice"):
+ if doctype != "Subcontracting Receipt":
+ fields += [
+ "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype),
+ ]
+
+ if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
fields += [
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype),
@@ -342,7 +346,7 @@
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note"))
- for tax in doc.get("taxes"):
+ for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":
tax.tax_amount = -1 * tax.tax_amount
@@ -381,8 +385,11 @@
for d in doc.get("packed_items"):
d.qty = d.qty * -1
- doc.discount_amount = -1 * source.discount_amount
- doc.run_method("calculate_taxes_and_totals")
+ if doc.get("discount_amount"):
+ doc.discount_amount = -1 * source.discount_amount
+
+ if doctype != "Subcontracting Receipt":
+ doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty
@@ -393,7 +400,7 @@
if serial_nos:
target_doc.serial_no = "\n".join(serial_nos)
- if doctype == "Purchase Receipt":
+ if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
)
@@ -405,15 +412,24 @@
)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0))
- target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0))
- target_doc.received_stock_qty = -1 * flt(
- source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0)
- )
+ if hasattr(target_doc, "stock_qty"):
+ target_doc.stock_qty = -1 * flt(
+ source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)
+ )
+ target_doc.received_stock_qty = -1 * flt(
+ source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0)
+ )
- target_doc.purchase_order = source_doc.purchase_order
- target_doc.purchase_order_item = source_doc.purchase_order_item
- target_doc.rejected_warehouse = source_doc.rejected_warehouse
- target_doc.purchase_receipt_item = source_doc.name
+ if doctype == "Subcontracting Receipt":
+ target_doc.subcontracting_order = source_doc.subcontracting_order
+ target_doc.subcontracting_order_item = source_doc.subcontracting_order_item
+ target_doc.rejected_warehouse = source_doc.rejected_warehouse
+ target_doc.subcontracting_receipt_item = source_doc.name
+ else:
+ target_doc.purchase_order = source_doc.purchase_order
+ target_doc.purchase_order_item = source_doc.purchase_order_item
+ target_doc.rejected_warehouse = source_doc.rejected_warehouse
+ target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row(
@@ -525,7 +541,7 @@
item_row,
)
- if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
select_field = "incoming_rate"
else:
select_field = "abs(stock_value_difference / actual_qty)"
@@ -560,6 +576,7 @@
"Purchase Invoice": "purchase_invoice_item",
"Delivery Note": "dn_detail",
"Sales Invoice": "sales_invoice_item",
+ "Subcontracting Receipt": "subcontracting_receipt_item",
}
return return_against_item_fields[voucher_type]
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
deleted file mode 100644
index 4bce06f..0000000
--- a/erpnext/controllers/subcontracting.py
+++ /dev/null
@@ -1,469 +0,0 @@
-import copy
-from collections import defaultdict
-
-import frappe
-from frappe import _
-from frappe.utils import cint, flt, get_link_to_form
-
-from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-
-
-class Subcontracting:
- def set_materials_for_subcontracted_items(self, raw_material_table):
- if self.doctype == "Purchase Invoice" and not self.update_stock:
- return
-
- self.raw_material_table = raw_material_table
- self.__identify_change_in_item_table()
- self.__prepare_supplied_items()
- self.__validate_supplied_items()
-
- def __prepare_supplied_items(self):
- self.initialized_fields()
- self.__get_purchase_orders()
- self.__get_pending_qty_to_receive()
- self.get_available_materials()
- self.__remove_changed_rows()
- self.__set_supplied_items()
-
- def initialized_fields(self):
- self.available_materials = frappe._dict()
- self.__transferred_items = frappe._dict()
- self.alternative_item_details = frappe._dict()
- self.__get_backflush_based_on()
-
- def __get_backflush_based_on(self):
- self.backflush_based_on = frappe.db.get_single_value(
- "Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
- )
-
- def __get_purchase_orders(self):
- self.purchase_orders = []
-
- if self.doctype == "Purchase Order":
- return
-
- self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
-
- def __identify_change_in_item_table(self):
- self.__changed_name = []
- self.__reference_name = []
-
- if self.doctype == "Purchase Order" or self.is_new():
- self.set(self.raw_material_table, [])
- return
-
- item_dict = self.__get_data_before_save()
- if not item_dict:
- return True
-
- for n_row in self.items:
- self.__reference_name.append(n_row.name)
- if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
- self.__changed_name.append(n_row.name)
-
- if item_dict.get(n_row.name):
- del item_dict[n_row.name]
-
- self.__changed_name.extend(item_dict.keys())
-
- def __get_data_before_save(self):
- item_dict = {}
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self._doc_before_save:
- for row in self._doc_before_save.get("items"):
- item_dict[row.name] = (row.item_code, row.qty)
-
- return item_dict
-
- def get_available_materials(self):
- """Get the available raw materials which has been transferred to the supplier.
- available_materials = {
- (item_code, subcontracted_item, purchase_order): {
- 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
- }
- }
- """
- if not self.purchase_orders:
- return
-
- for row in self.__get_transferred_items():
- key = (row.rm_item_code, row.main_item_code, row.purchase_order)
-
- if key not in self.available_materials:
- self.available_materials.setdefault(
- key,
- frappe._dict(
- {
- "qty": 0,
- "serial_no": [],
- "batch_no": defaultdict(float),
- "item_details": row,
- "po_details": [],
- }
- ),
- )
-
- details = self.available_materials[key]
- details.qty += row.qty
- details.po_details.append(row.po_detail)
-
- if row.serial_no:
- details.serial_no.extend(get_serial_nos(row.serial_no))
-
- if row.batch_no:
- details.batch_no[row.batch_no] += row.qty
-
- self.__set_alternative_item_details(row)
-
- self.__transferred_items = copy.deepcopy(self.available_materials)
- for doctype in ["Purchase Receipt", "Purchase Invoice"]:
- self.__update_consumed_materials(doctype)
-
- def __update_consumed_materials(self, doctype, return_consumed_items=False):
- """Deduct the consumed materials from the available materials."""
-
- pr_items = self.__get_received_items(doctype)
- if not pr_items:
- return ([], {}) if return_consumed_items else None
-
- pr_items = {d.name: d.get(self.get("po_field") or "purchase_order") for d in pr_items}
- consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
-
- if return_consumed_items:
- return (consumed_materials, pr_items)
-
- for row in consumed_materials:
- key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
- if not self.available_materials.get(key):
- continue
-
- self.available_materials[key]["qty"] -= row.consumed_qty
- if row.serial_no:
- self.available_materials[key]["serial_no"] = list(
- set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
- )
-
- if row.batch_no:
- self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
-
- def __get_transferred_items(self):
- fields = ["`tabStock Entry`.`purchase_order`"]
- alias_dict = {
- "item_code": "rm_item_code",
- "subcontracted_item": "main_item_code",
- "basic_rate": "rate",
- }
-
- child_table_fields = [
- "item_code",
- "item_name",
- "description",
- "qty",
- "basic_rate",
- "amount",
- "serial_no",
- "uom",
- "subcontracted_item",
- "stock_uom",
- "batch_no",
- "conversion_factor",
- "s_warehouse",
- "t_warehouse",
- "item_group",
- "po_detail",
- ]
-
- if self.backflush_based_on == "BOM":
- child_table_fields.append("original_item")
-
- for field in child_table_fields:
- fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
-
- filters = [
- ["Stock Entry", "docstatus", "=", 1],
- ["Stock Entry", "purpose", "=", "Send to Subcontractor"],
- ["Stock Entry", "purchase_order", "in", self.purchase_orders],
- ]
-
- return frappe.get_all("Stock Entry", fields=fields, filters=filters)
-
- def __get_received_items(self, doctype):
- fields = []
- self.po_field = "purchase_order"
-
- for field in ["name", self.po_field, "parent"]:
- fields.append(f"`tab{doctype} Item`.`{field}`")
-
- filters = [
- [doctype, "docstatus", "=", 1],
- [f"{doctype} Item", self.po_field, "in", self.purchase_orders],
- ]
- if doctype == "Purchase Invoice":
- filters.append(["Purchase Invoice", "update_stock", "=", 1])
-
- return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
-
- def __get_consumed_items(self, doctype, pr_items):
- return frappe.get_all(
- "Purchase Receipt Item Supplied",
- fields=[
- "serial_no",
- "rm_item_code",
- "reference_name",
- "batch_no",
- "consumed_qty",
- "main_item_code",
- ],
- filters={"docstatus": 1, "reference_name": ("in", list(pr_items)), "parenttype": doctype},
- )
-
- def __set_alternative_item_details(self, row):
- if row.get("original_item"):
- self.alternative_item_details[row.get("original_item")] = row
-
- def __get_pending_qty_to_receive(self):
- """Get qty to be received against the purchase order."""
-
- self.qty_to_be_received = defaultdict(float)
-
- if (
- self.doctype != "Purchase Order" and self.backflush_based_on != "BOM" and self.purchase_orders
- ):
- for row in frappe.get_all(
- "Purchase Order Item",
- fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
- filters={"docstatus": 1, "parent": ("in", self.purchase_orders)},
- ):
-
- self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
-
- def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
- doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
- fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
-
- alias_dict = {
- "item_code": "rm_item_code",
- "name": "bom_detail_no",
- "source_warehouse": "reserve_warehouse",
- }
- for field in [
- "item_code",
- "name",
- "rate",
- "stock_uom",
- "source_warehouse",
- "description",
- "item_name",
- "stock_uom",
- ]:
- fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
-
- filters = [
- [doctype, "parent", "=", bom_no],
- [doctype, "docstatus", "=", 1],
- ["BOM", "item", "=", item_code],
- [doctype, "sourced_by_supplier", "=", 0],
- ]
-
- return (
- frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
- )
-
- def __remove_changed_rows(self):
- if not self.__changed_name:
- return
-
- i = 1
- self.set(self.raw_material_table, [])
- for d in self._doc_before_save.supplied_items:
- if d.reference_name in self.__changed_name:
- continue
-
- if d.reference_name not in self.__reference_name:
- continue
-
- d.idx = i
- self.append("supplied_items", d)
-
- i += 1
-
- def __set_supplied_items(self):
- self.bom_items = {}
-
- has_supplied_items = True if self.get(self.raw_material_table) else False
- for row in self.items:
- if self.doctype != "Purchase Order" and (
- (self.__changed_name and row.name not in self.__changed_name)
- or (has_supplied_items and not self.__changed_name)
- ):
- continue
-
- if self.doctype == "Purchase Order" or self.backflush_based_on == "BOM":
- for bom_item in self.__get_materials_from_bom(
- row.item_code, row.bom, row.get("include_exploded_items")
- ):
- qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
- 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)
-
- elif self.backflush_based_on != "BOM":
- for key, transfer_item in self.available_materials.items():
- if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
- qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
- transfer_item.qty -= qty
- self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
-
- if self.qty_to_be_received:
- self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
-
- def __update_reserve_warehouse(self, row, item):
- if self.doctype == "Purchase Order":
- row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
-
- def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
- key = (item_row.item_code, item_row.purchase_order)
-
- if self.qty_to_be_received == item_row.qty:
- return transfer_item.qty
-
- if self.qty_to_be_received:
- qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
- transfer_item.item_details.required_qty = transfer_item.qty
-
- if transfer_item.serial_no or frappe.get_cached_value(
- "UOM", transfer_item.item_details.stock_uom, "must_be_whole_number"
- ):
- return frappe.utils.ceil(qty)
-
- return qty
-
- def __set_alternative_item(self, bom_item):
- if self.alternative_item_details.get(bom_item.rm_item_code):
- bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
-
- def __add_supplied_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)
- rm_obj.reference_name = item_row.name
-
- if self.doctype == "Purchase Order":
- rm_obj.required_qty = qty
- else:
- rm_obj.consumed_qty = 0
- rm_obj.purchase_order = item_row.purchase_order
- self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
-
- def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
- key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
-
- if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
- new_rm_obj = None
- for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
- if batch_qty >= qty:
- self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
- self.available_materials[key]["batch_no"][batch_no] -= qty
- return
-
- elif qty > 0 and batch_qty > 0:
- qty -= batch_qty
- new_rm_obj = self.append(self.raw_material_table, bom_item)
- new_rm_obj.reference_name = item_row.name
- self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
- self.available_materials[key]["batch_no"][batch_no] = 0
-
- if abs(qty) > 0 and not new_rm_obj:
- self.__set_consumed_qty(rm_obj, qty)
- else:
- self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
- self.__set_serial_nos(item_row, rm_obj)
-
- def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
- rm_obj.required_qty = required_qty
- rm_obj.consumed_qty = consumed_qty
-
- def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
- rm_obj.update(
- {
- "consumed_qty": qty,
- "batch_no": batch_no,
- "required_qty": qty,
- "purchase_order": item_row.purchase_order,
- }
- )
-
- self.__set_serial_nos(item_row, rm_obj)
-
- def __set_serial_nos(self, item_row, rm_obj):
- key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
- if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
- used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
- rm_obj.serial_no = "\n".join(used_serial_nos)
-
- # Removed the used serial nos from the list
- for sn in used_serial_nos:
- self.available_materials[key]["serial_no"].remove(sn)
-
- def set_consumed_qty_in_po(self):
- # Update consumed qty back in the purchase order
- if not self.is_subcontracted:
- return
-
- self.__get_purchase_orders()
- itemwise_consumed_qty = defaultdict(float)
- for doctype in ["Purchase Receipt", "Purchase Invoice"]:
- consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
-
- for row in consumed_items:
- key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
- itemwise_consumed_qty[key] += row.consumed_qty
-
- self.__update_consumed_qty_in_po(itemwise_consumed_qty)
-
- def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
- fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
- filters = {"docstatus": 1, "parent": ("in", self.purchase_orders)}
-
- for row in frappe.get_all(
- "Purchase Order Item Supplied", fields=fields, filters=filters, order_by="idx"
- ):
- key = (row.rm_item_code, row.main_item_code, row.parent)
- consumed_qty = itemwise_consumed_qty.get(key, 0)
-
- if row.supplied_qty < consumed_qty:
- consumed_qty = row.supplied_qty
-
- itemwise_consumed_qty[key] -= consumed_qty
- frappe.db.set_value("Purchase Order Item Supplied", row.name, "consumed_qty", consumed_qty)
-
- def __validate_supplied_items(self):
- if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
- return
-
- for row in self.get(self.raw_material_table):
- key = (row.rm_item_code, row.main_item_code, row.purchase_order)
- if not self.__transferred_items or not self.__transferred_items.get(key):
- return
-
- self.__validate_batch_no(row, key)
- self.__validate_serial_no(row, key)
-
- def __validate_batch_no(self, row, key):
- if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
- "batch_no"
- ):
- link = get_link_to_form("Purchase Order", row.purchase_order)
- msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
- frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
-
- def __validate_serial_no(self, row, key):
- if row.get("serial_no"):
- serial_nos = get_serial_nos(row.get("serial_no"))
- incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
-
- if incorrect_sn:
- incorrect_sn = "\n".join(incorrect_sn)
- link = get_link_to_form("Purchase Order", row.purchase_order)
- msg = f"The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}"
- frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
new file mode 100644
index 0000000..4e0d911
--- /dev/null
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -0,0 +1,652 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import copy
+from collections import defaultdict
+
+import frappe
+from frappe import _
+from frappe.utils import cint, cstr, flt, get_link_to_form
+
+from erpnext.controllers.stock_controller import StockController
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.utils import get_incoming_rate
+
+
+class SubcontractingController(StockController):
+ def before_validate(self):
+ self.remove_empty_rows()
+ self.set_items_conversion_factor()
+
+ def validate(self):
+ self.validate_items()
+ self.create_raw_materials_supplied()
+
+ def remove_empty_rows(self):
+ for key in ["service_items", "items", "supplied_items"]:
+ if self.get(key):
+ idx = 1
+ for item in self.get(key)[:]:
+ if not (item.get("item_code") or item.get("main_item_code")):
+ self.get(key).remove(item)
+ else:
+ item.idx = idx
+ idx += 1
+
+ def set_items_conversion_factor(self):
+ for item in self.get("items"):
+ if not item.conversion_factor:
+ item.conversion_factor = 1
+
+ def validate_items(self):
+ for item in self.items:
+ if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"):
+ msg = f"Item {item.item_name} must be a subcontracted item."
+ frappe.throw(_(msg))
+ if item.bom:
+ bom = frappe.get_doc("BOM", item.bom)
+ if not bom.is_active:
+ msg = f"Please select an active BOM for Item {item.item_name}."
+ frappe.throw(_(msg))
+ if bom.item != item.item_code:
+ msg = f"Please select an valid BOM for Item {item.item_name}."
+ frappe.throw(_(msg))
+
+ def __get_data_before_save(self):
+ item_dict = {}
+ if self.doctype == "Subcontracting Receipt" and self._doc_before_save:
+ for row in self._doc_before_save.get("items"):
+ item_dict[row.name] = (row.item_code, row.qty)
+
+ return item_dict
+
+ def __identify_change_in_item_table(self):
+ self.__changed_name = []
+ self.__reference_name = []
+
+ if self.doctype == "Subcontracting Order" or self.is_new():
+ self.set(self.raw_material_table, [])
+ return
+
+ item_dict = self.__get_data_before_save()
+ if not item_dict:
+ return True
+
+ for row in self.items:
+ self.__reference_name.append(row.name)
+ if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]:
+ self.__changed_name.append(row.name)
+
+ if item_dict.get(row.name):
+ del item_dict[row.name]
+
+ 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"
+ )
+
+ def initialized_fields(self):
+ self.available_materials = frappe._dict()
+ self.__transferred_items = frappe._dict()
+ self.alternative_item_details = frappe._dict()
+ self.__get_backflush_based_on()
+
+ def __get_subcontracting_orders(self):
+ self.subcontracting_orders = []
+
+ if self.doctype == "Subcontracting Order":
+ return
+
+ self.subcontracting_orders = [
+ item.subcontracting_order for item in self.items if item.subcontracting_order
+ ]
+
+ def __get_pending_qty_to_receive(self):
+ """Get qty to be received against the subcontracting order."""
+
+ self.qty_to_be_received = defaultdict(float)
+
+ if (
+ self.doctype != "Subcontracting Order"
+ and self.backflush_based_on != "BOM"
+ and self.subcontracting_orders
+ ):
+ for row in frappe.get_all(
+ "Subcontracting Order Item",
+ fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
+ filters={"docstatus": 1, "parent": ("in", self.subcontracting_orders)},
+ ):
+
+ self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
+
+ def __get_transferred_items(self):
+ fields = ["`tabStock Entry`.`subcontracting_order`"]
+ alias_dict = {
+ "item_code": "rm_item_code",
+ "subcontracted_item": "main_item_code",
+ "basic_rate": "rate",
+ }
+
+ child_table_fields = [
+ "item_code",
+ "item_name",
+ "description",
+ "qty",
+ "basic_rate",
+ "amount",
+ "serial_no",
+ "uom",
+ "subcontracted_item",
+ "stock_uom",
+ "batch_no",
+ "conversion_factor",
+ "s_warehouse",
+ "t_warehouse",
+ "item_group",
+ "sco_rm_detail",
+ ]
+
+ if self.backflush_based_on == "BOM":
+ child_table_fields.append("original_item")
+
+ for field in child_table_fields:
+ fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
+
+ filters = [
+ ["Stock Entry", "docstatus", "=", 1],
+ ["Stock Entry", "purpose", "=", "Send to Subcontractor"],
+ ["Stock Entry", "subcontracting_order", "in", self.subcontracting_orders],
+ ]
+
+ return frappe.get_all("Stock Entry", fields=fields, filters=filters)
+
+ def __set_alternative_item_details(self, row):
+ if row.get("original_item"):
+ self.alternative_item_details[row.get("original_item")] = row
+
+ def __get_received_items(self, doctype):
+ fields = []
+ self.sco_field = "subcontracting_order"
+
+ for field in ["name", self.sco_field, "parent"]:
+ fields.append(f"`tab{doctype} Item`.`{field}`")
+
+ filters = [
+ [doctype, "docstatus", "=", 1],
+ [f"{doctype} Item", self.sco_field, "in", self.subcontracting_orders],
+ ]
+
+ return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
+
+ def __get_consumed_items(self, doctype, scr_items):
+ return frappe.get_all(
+ "Subcontracting Receipt Supplied Item",
+ fields=[
+ "serial_no",
+ "rm_item_code",
+ "reference_name",
+ "batch_no",
+ "consumed_qty",
+ "main_item_code",
+ ],
+ filters={"docstatus": 1, "reference_name": ("in", list(scr_items)), "parenttype": doctype},
+ )
+
+ def __update_consumed_materials(self, doctype, return_consumed_items=False):
+ """Deduct the consumed materials from the available materials."""
+
+ scr_items = self.__get_received_items(doctype)
+ if not scr_items:
+ return ([], {}) if return_consumed_items else None
+
+ scr_items = {
+ item.name: item.get(self.get("sco_field") or "subcontracting_order") for item in scr_items
+ }
+ consumed_materials = self.__get_consumed_items(doctype, scr_items.keys())
+
+ if return_consumed_items:
+ return (consumed_materials, scr_items)
+
+ for row in consumed_materials:
+ key = (row.rm_item_code, row.main_item_code, scr_items.get(row.reference_name))
+ if not self.available_materials.get(key):
+ continue
+
+ self.available_materials[key]["qty"] -= row.consumed_qty
+ if row.serial_no:
+ self.available_materials[key]["serial_no"] = list(
+ set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
+ )
+
+ if row.batch_no:
+ self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
+
+ def get_available_materials(self):
+ """Get the available raw materials which has been transferred to the supplier.
+ available_materials = {
+ (item_code, subcontracted_item, subcontracting_order): {
+ 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
+ }
+ }
+ """
+ if not self.subcontracting_orders:
+ return
+
+ for row in self.__get_transferred_items():
+ key = (row.rm_item_code, row.main_item_code, row.subcontracting_order)
+
+ if key not in self.available_materials:
+ self.available_materials.setdefault(
+ key,
+ frappe._dict(
+ {
+ "qty": 0,
+ "serial_no": [],
+ "batch_no": defaultdict(float),
+ "item_details": row,
+ "sco_rm_details": [],
+ }
+ ),
+ )
+
+ details = self.available_materials[key]
+ details.qty += row.qty
+ details.sco_rm_details.append(row.sco_rm_detail)
+
+ if row.serial_no:
+ details.serial_no.extend(get_serial_nos(row.serial_no))
+
+ if row.batch_no:
+ details.batch_no[row.batch_no] += row.qty
+
+ self.__set_alternative_item_details(row)
+
+ self.__transferred_items = copy.deepcopy(self.available_materials)
+ self.__update_consumed_materials("Subcontracting Receipt")
+
+ def __remove_changed_rows(self):
+ if not self.__changed_name:
+ return
+
+ i = 1
+ self.set(self.raw_material_table, [])
+ for item in self._doc_before_save.supplied_items:
+ if item.reference_name in self.__changed_name:
+ continue
+
+ if item.reference_name not in self.__reference_name:
+ continue
+
+ item.idx = i
+ self.append("supplied_items", item)
+
+ i += 1
+
+ def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
+ doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
+ fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
+
+ alias_dict = {
+ "item_code": "rm_item_code",
+ "name": "bom_detail_no",
+ "source_warehouse": "reserve_warehouse",
+ }
+ for field in [
+ "item_code",
+ "name",
+ "rate",
+ "stock_uom",
+ "source_warehouse",
+ "description",
+ "item_name",
+ "stock_uom",
+ ]:
+ fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
+
+ filters = [
+ [doctype, "parent", "=", bom_no],
+ [doctype, "docstatus", "=", 1],
+ ["BOM", "item", "=", item_code],
+ [doctype, "sourced_by_supplier", "=", 0],
+ ]
+
+ 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 == "Subcontracting Order":
+ row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
+
+ def __set_alternative_item(self, bom_item):
+ if self.alternative_item_details.get(bom_item.rm_item_code):
+ bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
+
+ def __set_serial_nos(self, item_row, rm_obj):
+ key = (rm_obj.rm_item_code, item_row.item_code, item_row.subcontracting_order)
+ if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
+ used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
+ rm_obj.serial_no = "\n".join(used_serial_nos)
+
+ # Removed the used serial nos from the list
+ for sn in used_serial_nos:
+ self.available_materials[key]["serial_no"].remove(sn)
+
+ def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
+ rm_obj.update(
+ {
+ "consumed_qty": qty,
+ "batch_no": batch_no,
+ "required_qty": qty,
+ "subcontracting_order": item_row.subcontracting_order,
+ }
+ )
+
+ self.__set_serial_nos(item_row, rm_obj)
+
+ def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
+ rm_obj.required_qty = required_qty
+ rm_obj.consumed_qty = consumed_qty
+
+ def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
+ key = (rm_obj.rm_item_code, item_row.item_code, item_row.subcontracting_order)
+
+ if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
+ new_rm_obj = None
+ for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
+ if batch_qty >= qty:
+ self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
+ self.available_materials[key]["batch_no"][batch_no] -= qty
+ return
+
+ elif qty > 0 and batch_qty > 0:
+ qty -= batch_qty
+ new_rm_obj = self.append(self.raw_material_table, bom_item)
+ new_rm_obj.reference_name = item_row.name
+ self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
+ self.available_materials[key]["batch_no"][batch_no] = 0
+
+ if abs(qty) > 0 and not new_rm_obj:
+ self.__set_consumed_qty(rm_obj, qty)
+ else:
+ self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
+ self.__set_serial_nos(item_row, rm_obj)
+
+ def __add_supplied_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)
+ rm_obj.reference_name = item_row.name
+
+ if self.doctype == "Subcontracting Receipt":
+ args = frappe._dict(
+ {
+ "item_code": rm_obj.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1 * flt(rm_obj.consumed_qty),
+ "serial_no": rm_obj.serial_no,
+ "batch_no": rm_obj.batch_no,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "company": self.company,
+ "allow_zero_valuation": 1,
+ }
+ )
+ rm_obj.rate = get_incoming_rate(args)
+
+ if self.doctype == "Subcontracting Order":
+ rm_obj.required_qty = qty
+ rm_obj.amount = rm_obj.required_qty * rm_obj.rate
+ else:
+ rm_obj.consumed_qty = 0
+ rm_obj.subcontracting_order = item_row.subcontracting_order
+ self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
+
+ def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
+ key = (item_row.item_code, item_row.subcontracting_order)
+
+ if self.qty_to_be_received == item_row.qty:
+ return transfer_item.qty
+
+ if self.qty_to_be_received:
+ qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
+ transfer_item.item_details.required_qty = transfer_item.qty
+
+ if transfer_item.serial_no or frappe.get_cached_value(
+ "UOM", transfer_item.item_details.stock_uom, "must_be_whole_number"
+ ):
+ return frappe.utils.ceil(qty)
+
+ return qty
+
+ def __set_supplied_items(self):
+ self.bom_items = {}
+
+ has_supplied_items = True if self.get(self.raw_material_table) else False
+ for row in self.items:
+ if self.doctype != "Subcontracting Order" and (
+ (self.__changed_name and row.name not in self.__changed_name)
+ or (has_supplied_items and not self.__changed_name)
+ ):
+ continue
+
+ if self.doctype == "Subcontracting Order" or self.backflush_based_on == "BOM":
+ for bom_item in self.__get_materials_from_bom(
+ row.item_code, row.bom, row.get("include_exploded_items")
+ ):
+ qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
+ 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)
+
+ elif self.backflush_based_on != "BOM":
+ for key, transfer_item in self.available_materials.items():
+ if (key[1], key[2]) == (row.item_code, row.subcontracting_order) and transfer_item.qty > 0:
+ qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
+ transfer_item.qty -= qty
+ self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
+
+ if self.qty_to_be_received:
+ self.qty_to_be_received[(row.item_code, row.subcontracting_order)] -= row.qty
+
+ def __prepare_supplied_items(self):
+ self.initialized_fields()
+ self.__get_subcontracting_orders()
+ self.__get_pending_qty_to_receive()
+ self.get_available_materials()
+ self.__remove_changed_rows()
+ self.__set_supplied_items()
+
+ def __validate_batch_no(self, row, key):
+ if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
+ "batch_no"
+ ):
+ link = get_link_to_form("Subcontracting Order", row.subcontracting_order)
+ msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Subcontracting Order {link}'
+ frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
+
+ def __validate_serial_no(self, row, key):
+ if row.get("serial_no"):
+ serial_nos = get_serial_nos(row.get("serial_no"))
+ incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
+
+ if incorrect_sn:
+ incorrect_sn = "\n".join(incorrect_sn)
+ link = get_link_to_form("Subcontracting Order", row.subcontracting_order)
+ msg = f"The Serial Nos {incorrect_sn} has not supplied against the Subcontracting Order {link}"
+ frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
+
+ def __validate_supplied_items(self):
+ if self.doctype != "Subcontracting Receipt":
+ return
+
+ for row in self.get(self.raw_material_table):
+ key = (row.rm_item_code, row.main_item_code, row.subcontracting_order)
+ if not self.__transferred_items or not self.__transferred_items.get(key):
+ return
+
+ self.__validate_batch_no(row, key)
+ self.__validate_serial_no(row, key)
+
+ def set_materials_for_subcontracted_items(self, raw_material_table):
+ self.raw_material_table = raw_material_table
+ self.__identify_change_in_item_table()
+ self.__prepare_supplied_items()
+ self.__validate_supplied_items()
+
+ def create_raw_materials_supplied(self, raw_material_table="supplied_items"):
+ self.set_materials_for_subcontracted_items(raw_material_table)
+
+ if self.doctype == "Subcontracting Receipt":
+ for item in self.get("items"):
+ item.rm_supp_cost = 0.0
+
+ def __update_consumed_qty_in_sco(self, itemwise_consumed_qty):
+ fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
+ filters = {"docstatus": 1, "parent": ("in", self.subcontracting_orders)}
+
+ for row in frappe.get_all(
+ "Subcontracting Order Supplied Item", fields=fields, filters=filters, order_by="idx"
+ ):
+ key = (row.rm_item_code, row.main_item_code, row.parent)
+ consumed_qty = itemwise_consumed_qty.get(key, 0)
+
+ if row.supplied_qty < consumed_qty:
+ consumed_qty = row.supplied_qty
+
+ itemwise_consumed_qty[key] -= consumed_qty
+ frappe.db.set_value(
+ "Subcontracting Order Supplied Item", row.name, "consumed_qty", consumed_qty
+ )
+
+ def set_consumed_qty_in_sco(self):
+ # Update consumed qty back in the subcontracting order
+ self.__get_subcontracting_orders()
+ itemwise_consumed_qty = defaultdict(float)
+ consumed_items, scr_items = self.__update_consumed_materials(
+ "Subcontracting Receipt", return_consumed_items=True
+ )
+
+ for row in consumed_items:
+ key = (row.rm_item_code, row.main_item_code, scr_items.get(row.reference_name))
+ itemwise_consumed_qty[key] += row.consumed_qty
+
+ self.__update_consumed_qty_in_sco(itemwise_consumed_qty)
+
+ def update_ordered_and_reserved_qty(self):
+ sco_map = {}
+ for item in self.get("items"):
+ if self.doctype == "Subcontracting Receipt" and item.subcontracting_order:
+ sco_map.setdefault(item.subcontracting_order, []).append(item.subcontracting_order_item)
+
+ for sco, sco_item_rows in sco_map.items():
+ if sco and sco_item_rows:
+ sco_doc = frappe.get_doc("Subcontracting Order", sco)
+
+ if sco_doc.status in ["Closed", "Cancelled"]:
+ frappe.throw(
+ _("{0} {1} is cancelled or closed").format(_("Subcontracting Order"), sco),
+ frappe.InvalidStatusError,
+ )
+
+ sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows)
+ sco_doc.update_reserved_qty_for_subcontracting()
+
+ def make_sl_entries_for_supplier_warehouse(self, sl_entries):
+ if hasattr(self, "supplied_items"):
+ for item in self.get("supplied_items"):
+ # negative quantity is passed, as raw material qty has to be decreased
+ # when SCR is submitted and it has to be increased when SCR is cancelled
+ sl_entries.append(
+ self.get_sl_entries(
+ item,
+ {
+ "item_code": item.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "actual_qty": -1 * flt(item.consumed_qty),
+ "dependant_sle_voucher_detail_no": item.reference_name,
+ },
+ )
+ )
+
+ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
+ self.update_ordered_and_reserved_qty()
+
+ sl_entries = []
+ stock_items = self.get_stock_items()
+
+ for item in self.get("items"):
+ if item.item_code in stock_items and item.warehouse:
+ scr_qty = flt(item.qty) * flt(item.conversion_factor)
+
+ if scr_qty:
+ sle = self.get_sl_entries(
+ item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
+ )
+ rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
+ incoming_rate = flt(item.rate, rate_db_precision)
+ sle.update(
+ {
+ "incoming_rate": incoming_rate,
+ "recalculate_rate": 1,
+ }
+ )
+ sl_entries.append(sle)
+
+ if flt(item.rejected_qty) != 0:
+ sl_entries.append(
+ self.get_sl_entries(
+ item,
+ {
+ "warehouse": item.rejected_warehouse,
+ "actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
+ "serial_no": cstr(item.rejected_serial_no).strip(),
+ "incoming_rate": 0.0,
+ "recalculate_rate": 1,
+ },
+ )
+ )
+
+ self.make_sl_entries_for_supplier_warehouse(sl_entries)
+ self.make_sl_entries(
+ sl_entries,
+ allow_negative_stock=allow_negative_stock,
+ via_landed_cost_voucher=via_landed_cost_voucher,
+ )
+
+ def get_supplied_items_cost(self, item_row_id):
+ supplied_items_cost = 0.0
+ for item in self.get("supplied_items"):
+ if item.reference_name == item_row_id:
+ item.amount = flt(flt(item.consumed_qty) * flt(item.rate), item.precision("amount"))
+ supplied_items_cost += item.amount
+
+ return supplied_items_cost
+
+ def set_subcontracting_order_status(self):
+ if self.doctype == "Subcontracting Order":
+ self.update_status()
+ elif self.doctype == "Subcontracting Receipt":
+ self.__get_subcontracting_orders
+
+ if self.subcontracting_orders:
+ for sco in set(self.subcontracting_orders):
+ sco_doc = frappe.get_doc("Subcontracting Order", sco)
+ sco_doc.update_status()
+
+ @property
+ def sub_contracted_items(self):
+ if not hasattr(self, "_sub_contracted_items"):
+ self._sub_contracted_items = []
+ item_codes = list(set(item.item_code for item in self.get("items")))
+ if item_codes:
+ items = frappe.get_all(
+ "Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1}
+ )
+ self._sub_contracted_items = [item.name for item in items]
+
+ return self._sub_contracted_items
diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py
new file mode 100644
index 0000000..cbdd191
--- /dev/null
+++ b/erpnext/controllers/tests/test_subcontracting_controller.py
@@ -0,0 +1,1024 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import copy
+from collections import defaultdict
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import cint
+
+from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ get_materials_from_supplier,
+ make_rm_stock_entry,
+ make_subcontracting_receipt,
+)
+
+
+class TestSubcontractingController(FrappeTestCase):
+ def setUp(self):
+ make_subcontracted_items()
+ make_raw_materials()
+ make_service_items()
+ make_bom_for_subcontracted_items()
+
+ def test_remove_empty_rows(self):
+ sco = get_subcontracting_order()
+ len_before = len(sco.service_items)
+ sco.service_items[0].item_code = None
+ sco.remove_empty_rows()
+ self.assertEqual((len_before - 1), len(sco.service_items))
+
+ def test_create_raw_materials_supplied(self):
+ sco = get_subcontracting_order()
+ sco.supplied_items = None
+ sco.create_raw_materials_supplied()
+ self.assertIsNotNone(sco.supplied_items)
+
+ def test_sco_with_bom(self):
+ """
+ - Set backflush based on BOM.
+ - Create SCO for the item Subcontracted Item SA1 and add same item two times.
+ - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
+ - Create SCR against the SCO and check serial nos and batch no.
+ """
+
+ set_backflush_based_on("BOM")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA1",
+ "fg_item_qty": 5,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 6,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA1",
+ "fg_item_qty": 6,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ scr.submit()
+
+ for key, value in get_supplied_items(scr).items():
+ transferred_detais = itemwise_details.get(key)
+
+ for field in ["qty", "serial_no", "batch_no"]:
+ if value.get(field):
+ transfer, consumed = (transferred_detais.get(field), value.get(field))
+ if field == "serial_no":
+ transfer, consumed = (sorted(transfer), sorted(consumed))
+
+ self.assertEqual(transfer, consumed)
+
+ def test_sco_with_material_transfer(self):
+ """
+ - Set backflush based on Material Transfer.
+ - Create SCO for the item Subcontracted Item SA1 and Subcontracted Item SA5.
+ - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
+ - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5.
+ - Create partial SCR against the SCO and check serial nos and batch no.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA1",
+ "fg_item_qty": 5,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 5",
+ "qty": 6,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA5",
+ "fg_item_qty": 6,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ rm_items.append(
+ {
+ "main_item_code": "Subcontracted Item SA5",
+ "item_code": "Subcontracted SRM Item 4",
+ "qty": 6,
+ }
+ )
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.remove(scr1.items[1])
+ scr1.save()
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ transferred_detais = itemwise_details.get(key)
+
+ for field in ["qty", "serial_no", "batch_no"]:
+ if value.get(field):
+ self.assertEqual(value.get(field), transferred_detais.get(field))
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.save()
+ scr2.submit()
+
+ for key, value in get_supplied_items(scr2).items():
+ transferred_detais = itemwise_details.get(key)
+
+ for field in ["qty", "serial_no", "batch_no"]:
+ if value.get(field):
+ self.assertEqual(value.get(field), transferred_detais.get(field))
+
+ def test_subcontracting_with_same_components_different_fg(self):
+ """
+ - Set backflush based on Material Transfer.
+ - Create SCO for the item Subcontracted Item SA2 and Subcontracted Item SA3.
+ - Transfer the components from Stores to Supplier warehouse with serial nos.
+ - Transfer extra qty of components for the item Subcontracted Item SA2.
+ - Create partial SCR against the SCO and check serial nos.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 2",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA2",
+ "fg_item_qty": 5,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 3",
+ "qty": 6,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA3",
+ "fg_item_qty": 6,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ rm_items[0]["qty"] += 1
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.items[0].qty = 3
+ scr1.remove(scr1.items[1])
+ scr1.save()
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ transferred_detais = itemwise_details.get(key)
+
+ self.assertEqual(value.qty, 4)
+ self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4]))
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.items[0].qty = 2
+ scr2.remove(scr2.items[1])
+ scr2.save()
+ scr2.submit()
+
+ for key, value in get_supplied_items(scr2).items():
+ transferred_detais = itemwise_details.get(key)
+
+ self.assertEqual(value.qty, 2)
+ self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6]))
+
+ scr3 = make_subcontracting_receipt(sco.name)
+ scr3.save()
+ scr3.submit()
+
+ for key, value in get_supplied_items(scr3).items():
+ transferred_detais = itemwise_details.get(key)
+
+ self.assertEqual(value.qty, 6)
+ self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12]))
+
+ def test_return_non_consumed_materials(self):
+ """
+ - Set backflush based on Material Transfer.
+ - Create SCO for item Subcontracted Item SA2.
+ - Transfer the components from Stores to Supplier warehouse with serial nos.
+ - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
+ - Create SCR for full qty against the SCO and change the qty of raw material.
+ - After that return the non consumed material back to the store from supplier's warehouse.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 2",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA2",
+ "fg_item_qty": 5,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ rm_items[0]["qty"] += 1
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+ scr1.supplied_items[0].consumed_qty = 5
+ scr1.supplied_items[0].serial_no = "\n".join(
+ sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5])
+ )
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ transferred_detais = itemwise_details.get(key)
+ self.assertEqual(value.qty, 5)
+ self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5]))
+
+ sco.load_from_db()
+ self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
+ doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
+ self.assertEqual(doc.items[0].qty, 1)
+ self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
+ self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
+ self.assertEqual(
+ get_serial_nos(doc.items[0].serial_no),
+ itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6],
+ )
+
+ def test_item_with_batch_based_on_bom(self):
+ """
+ - Set backflush based on BOM.
+ - Create SCO for item Subcontracted Item SA4 (has batch no).
+ - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
+ - Transfer the components in multiple batches.
+ - Create the 3 SCR against the SCO and split Subcontracted Items into two batches.
+ - Keep the qty as 2 for Subcontracted Item in the SCR.
+ """
+
+ set_backflush_based_on("BOM")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 4",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA4",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = [
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 1",
+ "qty": 10.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 2",
+ "qty": 10.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 1.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ ]
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.items[0].qty = 2
+ add_second_row_in_scr(scr1)
+ scr1.flags.ignore_mandatory = True
+ scr1.save()
+ scr1.set_missing_values()
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ self.assertEqual(value.qty, 4)
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.items[0].qty = 2
+ add_second_row_in_scr(scr2)
+ scr2.flags.ignore_mandatory = True
+ scr2.save()
+ scr2.set_missing_values()
+ scr2.submit()
+
+ for key, value in get_supplied_items(scr2).items():
+ self.assertEqual(value.qty, 4)
+
+ scr3 = make_subcontracting_receipt(sco.name)
+ scr3.items[0].qty = 2
+ scr3.flags.ignore_mandatory = True
+ scr3.save()
+ scr3.set_missing_values()
+ scr3.submit()
+
+ for key, value in get_supplied_items(scr3).items():
+ self.assertEqual(value.qty, 2)
+
+ def test_item_with_batch_based_on_material_transfer(self):
+ """
+ - Set backflush based on Material Transferred for Subcontract.
+ - Create SCO for item Subcontracted Item SA4 (has batch no).
+ - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
+ - Transfer the components in multiple batches with extra 2 qty for the batched item.
+ - Create the 3 SCR against the SCO and split Subcontracted Items into two batches.
+ - Keep the qty as 2 for Subcontracted Item in the SCR.
+ - In the first SCR the batched raw materials will be consumed 2 extra qty.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 4",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA4",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = [
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 1",
+ "qty": 10.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 2",
+ "qty": 10.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ {
+ "main_item_code": "Subcontracted Item SA4",
+ "item_code": "Subcontracted SRM Item 3",
+ "qty": 3.0,
+ "rate": 100.0,
+ "stock_uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ },
+ ]
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.items[0].qty = 2
+ add_second_row_in_scr(scr1)
+ scr1.flags.ignore_mandatory = True
+ scr1.save()
+ scr1.set_missing_values()
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ qty = 4 if key != "Subcontracted SRM Item 3" else 6
+ self.assertEqual(value.qty, qty)
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.items[0].qty = 2
+ add_second_row_in_scr(scr2)
+ scr2.flags.ignore_mandatory = True
+ scr2.save()
+ scr2.set_missing_values()
+ scr2.submit()
+
+ for key, value in get_supplied_items(scr2).items():
+ self.assertEqual(value.qty, 4)
+
+ scr3 = make_subcontracting_receipt(sco.name)
+ scr3.items[0].qty = 2
+ scr3.flags.ignore_mandatory = True
+ scr3.save()
+ scr3.set_missing_values()
+ scr3.submit()
+
+ for key, value in get_supplied_items(scr3).items():
+ self.assertEqual(value.qty, 1)
+
+ def test_partial_transfer_serial_no_components_based_on_material_transfer(self):
+ """
+ - Set backflush based on Material Transferred for Subcontract.
+ - Create SCO for the item Subcontracted Item SA2.
+ - Transfer the partial components from Stores to Supplier warehouse with serial nos.
+ - Create partial SCR against the SCO and change the qty manually.
+ - Transfer the remaining components from Stores to Supplier warehouse with serial nos.
+ - Create SCR for remaining qty against the SCO and change the qty manually.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 2",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA2",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ rm_items[0]["qty"] = 5
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.items[0].qty = 5
+ scr1.flags.ignore_mandatory = True
+ scr1.save()
+ scr1.set_missing_values()
+
+ for key, value in get_supplied_items(scr1).items():
+ details = itemwise_details.get(key)
+ self.assertEqual(value.qty, 3)
+ self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
+
+ scr1.load_from_db()
+ scr1.supplied_items[0].consumed_qty = 5
+ scr1.supplied_items[0].serial_no = "\n".join(
+ itemwise_details[scr1.supplied_items[0].rm_item_code]["serial_no"]
+ )
+ scr1.save()
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ details = itemwise_details.get(key)
+ self.assertEqual(value.qty, details.qty)
+ self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
+
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.submit()
+
+ for key, value in get_supplied_items(scr2).items():
+ details = itemwise_details.get(key)
+ self.assertEqual(value.qty, details.qty)
+ self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
+
+ def test_incorrect_serial_no_components_based_on_material_transfer(self):
+ """
+ - Set backflush based on Material Transferred for Subcontract.
+ - Create SCO for the item Subcontracted Item SA2.
+ - Transfer the serialized componenets to the supplier.
+ - Create SCR and change the serial no which is not transferred.
+ - System should throw the error and not allowed to save the SCR.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 2",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA2",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+ scr1.supplied_items[0].serial_no = "ABCD"
+ self.assertRaises(frappe.ValidationError, scr1.save)
+ scr1.delete()
+
+ def test_partial_transfer_batch_based_on_material_transfer(self):
+ """
+ - Set backflush based on Material Transferred for Subcontract.
+ - Create SCO for the item Subcontracted Item SA6.
+ - Transfer the partial components from Stores to Supplier warehouse with batch.
+ - Create partial SCR against the SCO and change the qty manually.
+ - Transfer the remaining components from Stores to Supplier warehouse with batch.
+ - Create SCR for remaining qty against the SCO and change the qty manually.
+ """
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 6",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA6",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ rm_items[0]["qty"] = 5
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.items[0].qty = 5
+ scr1.save()
+
+ transferred_batch_no = ""
+ for key, value in get_supplied_items(scr1).items():
+ details = itemwise_details.get(key)
+ self.assertEqual(value.qty, 3)
+ transferred_batch_no = details.batch_no
+ self.assertEqual(value.batch_no, details.batch_no)
+
+ scr1.load_from_db()
+ scr1.supplied_items[0].consumed_qty = 5
+ scr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
+ scr1.save()
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ details = itemwise_details.get(key)
+ self.assertEqual(value.qty, details.qty)
+ self.assertEqual(value.batch_no, details.batch_no)
+
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name
+
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.submit()
+
+ for key, value in get_supplied_items(scr1).items():
+ details = itemwise_details.get(key)
+ self.assertEqual(value.qty, details.qty)
+ self.assertEqual(value.batch_no, details.batch_no)
+
+
+def add_second_row_in_scr(scr):
+ item_dict = {}
+ for column in [
+ "item_code",
+ "item_name",
+ "qty",
+ "uom",
+ "warehouse",
+ "stock_uom",
+ "subcontracting_order",
+ "subcontracting_order_finished_good_item",
+ "conversion_factor",
+ "rate",
+ "expense_account",
+ "sco_rm_detail",
+ ]:
+ item_dict[column] = scr.items[0].get(column)
+
+ scr.append("items", item_dict)
+
+
+def get_supplied_items(scr_doc):
+ supplied_items = {}
+ for row in scr_doc.get("supplied_items"):
+ if row.rm_item_code not in supplied_items:
+ supplied_items.setdefault(
+ row.rm_item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)})
+ )
+
+ details = supplied_items[row.rm_item_code]
+ update_item_details(row, details)
+
+ return supplied_items
+
+
+def make_stock_in_entry(**args):
+ args = frappe._dict(args)
+
+ items = {}
+ for row in args.rm_items:
+ row = frappe._dict(row)
+
+ doc = make_stock_entry(
+ target=row.warehouse or "_Test Warehouse - _TC",
+ item_code=row.item_code,
+ qty=row.qty or 1,
+ basic_rate=row.rate or 100,
+ )
+
+ if row.item_code not in items:
+ items.setdefault(
+ row.item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)})
+ )
+
+ child_row = doc.items[0]
+ details = items[child_row.item_code]
+ update_item_details(child_row, details)
+
+ return items
+
+
+def update_item_details(child_row, details):
+ details.qty += (
+ child_row.get("qty")
+ if child_row.doctype == "Stock Entry Detail"
+ else child_row.get("consumed_qty")
+ )
+
+ if child_row.serial_no:
+ details.serial_no.extend(get_serial_nos(child_row.serial_no))
+
+ if child_row.batch_no:
+ details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
+
+
+def make_stock_transfer_entry(**args):
+ args = frappe._dict(args)
+
+ items = []
+ for row in args.rm_items:
+ row = frappe._dict(row)
+
+ item = {
+ "item_code": row.main_item_code or args.main_item_code,
+ "rm_item_code": row.item_code,
+ "qty": row.qty or 1,
+ "item_name": row.item_code,
+ "rate": row.rate or 100,
+ "stock_uom": row.stock_uom or "Nos",
+ "warehouse": row.warehuose or "_Test Warehouse - _TC",
+ }
+
+ item_details = args.itemwise_details.get(row.item_code)
+
+ if item_details and item_details.serial_no:
+ serial_nos = item_details.serial_no[0 : cint(row.qty)]
+ item["serial_no"] = "\n".join(serial_nos)
+ item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
+
+ if item_details and item_details.batch_no:
+ for batch_no, batch_qty in item_details.batch_no.items():
+ if batch_qty >= row.qty:
+ item["batch_no"] = batch_no
+ item_details.batch_no[batch_no] -= row.qty
+ break
+
+ items.append(item)
+
+ ste_dict = make_rm_stock_entry(args.sco_no, items)
+ doc = frappe.get_doc(ste_dict)
+ doc.insert()
+ doc.submit()
+
+ return doc
+
+
+def make_subcontracted_items():
+ sub_contracted_items = {
+ "Subcontracted Item SA1": {},
+ "Subcontracted Item SA2": {},
+ "Subcontracted Item SA3": {},
+ "Subcontracted Item SA4": {
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "SBAT.####",
+ },
+ "Subcontracted Item SA5": {},
+ "Subcontracted Item SA6": {},
+ "Subcontracted Item SA7": {},
+ }
+
+ 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():
+ raw_materials = {
+ "Subcontracted SRM Item 1": {},
+ "Subcontracted SRM Item 2": {"has_serial_no": 1, "serial_no_series": "SRI.####"},
+ "Subcontracted SRM Item 3": {
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "BAT.####",
+ },
+ "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
+ "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
+ }
+
+ for item, properties in raw_materials.items():
+ if not frappe.db.exists("Item", item):
+ properties.update({"is_stock_item": 1})
+ make_item(item, properties)
+
+
+def make_service_item(item, properties={}):
+ if not frappe.db.exists("Item", item):
+ properties.update({"is_stock_item": 0})
+ make_item(item, properties)
+
+
+def make_service_items():
+ service_items = {
+ "Subcontracted Service Item 1": {},
+ "Subcontracted Service Item 2": {},
+ "Subcontracted Service Item 3": {},
+ "Subcontracted Service Item 4": {},
+ "Subcontracted Service Item 5": {},
+ "Subcontracted Service Item 6": {},
+ "Subcontracted Service Item 7": {},
+ }
+
+ for item, properties in service_items.items():
+ make_service_item(item, properties)
+
+
+def make_bom_for_subcontracted_items():
+ boms = {
+ "Subcontracted Item SA1": [
+ "Subcontracted SRM Item 1",
+ "Subcontracted SRM Item 2",
+ "Subcontracted SRM Item 3",
+ ],
+ "Subcontracted Item SA2": ["Subcontracted SRM Item 2"],
+ "Subcontracted Item SA3": ["Subcontracted SRM Item 2"],
+ "Subcontracted Item SA4": [
+ "Subcontracted SRM Item 1",
+ "Subcontracted SRM Item 2",
+ "Subcontracted SRM Item 3",
+ ],
+ "Subcontracted Item SA5": ["Subcontracted SRM Item 5"],
+ "Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
+ "Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
+ }
+
+ 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)
+
+
+def set_backflush_based_on(based_on):
+ frappe.db.set_value(
+ "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on
+ )
+
+
+def get_subcontracting_order(**args):
+ from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import (
+ create_subcontracting_order,
+ )
+
+ args = frappe._dict(args)
+
+ if args.get("po_name"):
+ po = frappe.get_doc("Purchase Order", args.get("po_name"))
+
+ if po.is_subcontracted:
+ return create_subcontracting_order(po_name=po.name, **args)
+
+ if not args.service_items:
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 7",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA7",
+ "fg_item_qty": 10,
+ },
+ ]
+ else:
+ service_items = args.service_items
+
+ po = create_purchase_order(
+ rm_items=service_items,
+ is_subcontracted=1,
+ supplier_warehouse=args.supplier_warehouse or "_Test Warehouse 1 - _TC",
+ )
+
+ return create_subcontracting_order(po_name=po.name, **args)
+
+
+def get_rm_items(supplied_items):
+ rm_items = []
+
+ for item in supplied_items:
+ rm_items.append(
+ {
+ "main_item_code": item.main_item_code,
+ "item_code": item.rm_item_code,
+ "qty": item.required_qty,
+ "rate": item.rate,
+ "stock_uom": item.stock_uom,
+ "warehouse": item.reserve_warehouse,
+ }
+ )
+
+ return rm_items
+
+
+def make_subcontracted_item(**args):
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ args = frappe._dict(args)
+
+ if not frappe.db.exists("Item", args.item_code):
+ make_item(
+ args.item_code,
+ {
+ "is_stock_item": 1,
+ "is_sub_contracted_item": 1,
+ "has_batch_no": args.get("has_batch_no") or 0,
+ },
+ )
+
+ if not args.raw_materials:
+ if not frappe.db.exists("Item", "Test Extra Item 1"):
+ make_item(
+ "Test Extra Item 1",
+ {
+ "is_stock_item": 1,
+ },
+ )
+
+ if not frappe.db.exists("Item", "Test Extra Item 2"):
+ make_item(
+ "Test Extra Item 2",
+ {
+ "is_stock_item": 1,
+ },
+ )
+
+ args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
+
+ if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
+ make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 182a20c..9574158 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -9,7 +9,7 @@
from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt
-from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.controllers.tests.test_subcontracting_controller import set_backflush_based_on
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test,
@@ -18,7 +18,6 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
-from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records("BOM")
test_dependencies = ["Item", "Quality Inspection Template"]
@@ -256,12 +255,29 @@
bom.submit()
# test that sourced_by_supplier rate is zero even after updating cost
self.assertEqual(bom.items[2].rate, 0)
- # test in Purchase Order sourced_by_supplier is not added to Supplied Item
- po = create_purchase_order(
- item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
+
+ from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_subcontracting_order,
+ make_service_item,
+ )
+
+ make_service_item("Subcontracted Service Item 1")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 100,
+ "fg_item": item_code,
+ "fg_item_qty": 1,
+ },
+ ]
+ # test in Subcontracting Order sourced_by_supplier is not added to Supplied Item
+ sco = get_subcontracting_order(
+ service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC"
)
bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1])
- supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
+ supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
self.assertEqual(bom_items, supplied_items)
def test_bom_tree_representation(self):
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 9ca05b9..132e1eb 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -506,7 +506,7 @@
po.is_subcontracted = 1
for row in po_list:
po_data = {
- "item_code": row.production_item,
+ "fg_item": row.production_item,
"warehouse": row.fg_warehouse,
"production_plan_sub_assembly_item": row.name,
"bom": row.bom_no,
@@ -516,9 +516,6 @@
for field in [
"schedule_date",
"qty",
- "uom",
- "stock_uom",
- "item_name",
"description",
"production_plan_item",
]:
diff --git a/erpnext/modules.txt b/erpnext/modules.txt
index 869166b..01ebed7 100644
--- a/erpnext/modules.txt
+++ b/erpnext/modules.txt
@@ -21,3 +21,4 @@
Telephony
Bulk Transaction
E-commerce
+Subcontracting
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
index f6427ca..75a5477 100644
--- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
+++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
@@ -15,6 +15,8 @@
("accounts", "sales_invoice_item"),
("accounts", "purchase_invoice_item"),
("buying", "purchase_receipt_item_supplied"),
+ ("subcontracting", "subcontracting_receipt_item"),
+ ("subcontracting", "subcontracting_receipt_supplied_item"),
]
for module, doctype in doctypes_to_reload:
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index a5b7699..91e0771 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -85,7 +85,7 @@
if (me.frm.doc.is_subcontracted) {
return{
query: "erpnext.controllers.queries.item_query",
- filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 }
+ filters:{ 'supplier': me.frm.doc.supplier, 'is_stock_item': 0 }
}
}
else {
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index bc235d9..448b049 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -43,20 +43,18 @@
def update_reserved_qty_for_sub_contracting(self):
# reserved qty
- po = frappe.qb.DocType("Purchase Order")
- supplied_item = frappe.qb.DocType("Purchase Order Item Supplied")
+ sco = frappe.qb.DocType("Subcontracting Order")
+ supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item")
reserved_qty_for_sub_contract = (
- frappe.qb.from_(po)
+ frappe.qb.from_(sco)
.from_(supplied_item)
.select(Sum(Coalesce(supplied_item.required_qty, 0)))
.where(
(supplied_item.rm_item_code == self.item_code)
- & (po.name == supplied_item.parent)
- & (po.docstatus == 1)
- & (po.is_subcontracted)
- & (po.status != "Closed")
- & (po.per_received < 100)
+ & (sco.name == supplied_item.parent)
+ & (sco.docstatus == 1)
+ & (sco.per_received < 100)
& (supplied_item.reserve_warehouse == self.warehouse)
)
).run()[0][0] or 0.0
@@ -74,19 +72,19 @@
materials_transferred = (
frappe.qb.from_(se)
.from_(se_item)
- .from_(po)
- .select(Sum(qty_field))
+ .from_(sco)
+ .select(
+ Sum(Case().when(se.is_return == 1, se_item.transfer_qty * -1).else_(se_item.transfer_qty))
+ )
.where(
(se.docstatus == 1)
& (se.purpose == "Send to Subcontractor")
- & (Coalesce(se.purchase_order, "") != "")
+ & (Coalesce(se.subcontracting_order, "") != "")
& ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code))
& (se.name == se_item.parent)
- & (po.name == se.purchase_order)
- & (po.docstatus == 1)
- & (po.is_subcontracted == 1)
- & (po.status != "Closed")
- & (po.per_received < 100)
+ & (sco.name == se.subcontracting_order)
+ & (sco.docstatus == 1)
+ & (sco.per_received < 100)
)
).run()[0][0] or 0.0
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index b8f4803..49530b4 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -1,17 +1,15 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import json
-
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt
-from erpnext.buying.doctype.purchase_order.purchase_order import (
- make_purchase_receipt,
- make_rm_stock_entry,
+from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_subcontracting_order,
+ make_service_item,
+ set_backflush_based_on,
)
-from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
@@ -22,6 +20,10 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_rm_stock_entry,
+ make_subcontracting_receipt,
+)
class TestItemAlternative(FrappeTestCase):
@@ -30,9 +32,7 @@
make_items()
def test_alternative_item_for_subcontract_rm(self):
- frappe.db.set_value(
- "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
- )
+ set_backflush_based_on("BOM")
create_stock_reconciliation(
item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
@@ -42,15 +42,22 @@
)
supplier_warehouse = "Test Supplier Warehouse - _TC"
- po = create_purchase_order(
- item="Test Finished Goods - A",
- is_subcontracted=1,
- qty=5,
- rate=3000,
- supplier_warehouse=supplier_warehouse,
- )
- rm_item = [
+ make_service_item("Subcontracted Service Item 1")
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 5,
+ "rate": 3000,
+ "fg_item": "Test Finished Goods - A",
+ "fg_item_qty": 5,
+ },
+ ]
+ sco = get_subcontracting_order(
+ service_items=service_items, supplier_warehouse=supplier_warehouse
+ )
+ rm_items = [
{
"item_code": "Test Finished Goods - A",
"rm_item_code": "Test FG A RW 1",
@@ -73,14 +80,13 @@
},
]
- rm_item_string = json.dumps(rm_item)
reserved_qty_for_sub_contract = frappe.db.get_value(
"Bin",
{"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
"reserved_qty_for_sub_contract",
)
- se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
+ se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
se.to_warehouse = supplier_warehouse
se.insert()
@@ -104,22 +110,17 @@
after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)
)
- pr = make_purchase_receipt(po.name)
- pr.save()
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
- pr = frappe.get_doc("Purchase Receipt", pr.name)
+ scr = frappe.get_doc("Subcontracting Receipt", scr.name)
status = False
- for d in pr.supplied_items:
- if d.rm_item_code == "Alternate Item For A RW 1":
+ for item in scr.supplied_items:
+ if item.rm_item_code == "Alternate Item For A RW 1":
status = True
self.assertEqual(status, True)
- frappe.db.set_value(
- "Buying Settings",
- None,
- "backflush_raw_materials_of_subcontract_based_on",
- "Material Transferred for Subcontract",
- )
+ set_backflush_based_on("Material Transferred for Subcontract")
def test_alternative_item_for_production_rm(self):
create_stock_reconciliation(
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 754404b..74db616 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -296,9 +296,6 @@
frappe.provide("erpnext.buying");
frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) {
- if (frm.doc.is_subcontracted) {
- erpnext.buying.get_default_bom(frm);
- }
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
});
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index fcf0cd1..ff4e0a1 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -234,7 +234,6 @@
self.make_gl_entries()
self.repost_future_sle_and_gle()
- self.set_consumed_qty_in_po()
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
@@ -270,18 +269,6 @@
self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.delete_auto_created_batches()
- self.set_consumed_qty_in_po()
-
- @frappe.whitelist()
- def get_current_stock(self):
- for d in self.get("supplied_items"):
- if self.supplier_warehouse:
- bin = frappe.db.sql(
- "select actual_qty from `tabBin` where item_code = %s and warehouse = %s",
- (d.rm_item_code, self.supplier_warehouse),
- as_dict=1,
- )
- d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 7fbfa62..f500187 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2,10 +2,6 @@
# License: GNU General Public License v3. See license.txt
-import json
-import unittest
-from collections import defaultdict
-
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
@@ -311,142 +307,6 @@
pr.cancel()
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
- def test_subcontracting(self):
- from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-
- frappe.db.set_value(
- "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
- )
-
- make_stock_entry(
- item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
- )
- make_stock_entry(
- item_code="_Test Item Home Desktop 100",
- qty=100,
- target="_Test Warehouse 1 - _TC",
- basic_rate=100,
- )
- pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted=1)
- self.assertEqual(len(pr.get("supplied_items")), 2)
-
- rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
- self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
-
- pr.cancel()
-
- def test_subcontracting_gle_fg_item_rate_zero(self):
- from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-
- frappe.db.set_value(
- "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
- )
-
- se1 = make_stock_entry(
- item_code="_Test Item",
- target="Work In Progress - TCP1",
- qty=100,
- basic_rate=100,
- company="_Test Company with perpetual inventory",
- )
-
- se2 = make_stock_entry(
- item_code="_Test Item Home Desktop 100",
- target="Work In Progress - TCP1",
- qty=100,
- basic_rate=100,
- company="_Test Company with perpetual inventory",
- )
-
- pr = make_purchase_receipt(
- item_code="_Test FG Item",
- qty=10,
- rate=0,
- is_subcontracted=1,
- company="_Test Company with perpetual inventory",
- warehouse="Stores - TCP1",
- supplier_warehouse="Work In Progress - TCP1",
- )
-
- gl_entries = get_gl_entries("Purchase Receipt", pr.name)
-
- self.assertFalse(gl_entries)
-
- pr.cancel()
- se1.cancel()
- se2.cancel()
-
- def test_subcontracting_over_receipt(self):
- """
- Behaviour: Raise multiple PRs against one PO that in total
- receive more than the required qty in the PO.
- Expected Result: Error Raised for Over Receipt against PO.
- """
- from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
- from erpnext.buying.doctype.purchase_order.purchase_order import (
- make_rm_stock_entry as make_subcontract_transfer_entry,
- )
- from erpnext.buying.doctype.purchase_order.test_purchase_order import (
- create_purchase_order,
- make_subcontracted_item,
- update_backflush_based_on,
- )
- from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-
- update_backflush_based_on("Material Transferred for Subcontract")
- item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code=item_code)
-
- po = create_purchase_order(
- item_code=item_code,
- qty=1,
- include_exploded_items=0,
- is_subcontracted=1,
- supplier_warehouse="_Test Warehouse 1 - _TC",
- )
-
- # stock raw materials in a warehouse before transfer
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100
- )
- make_stock_entry(
- target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100
- )
-
- rm_items = [
- {
- "item_code": item_code,
- "rm_item_code": po.supplied_items[0].rm_item_code,
- "item_name": "_Test FG Item",
- "qty": po.supplied_items[0].required_qty,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- },
- {
- "item_code": item_code,
- "rm_item_code": po.supplied_items[1].rm_item_code,
- "item_name": "Test Extra Item 1",
- "qty": po.supplied_items[1].required_qty,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- },
- ]
- rm_item_string = json.dumps(rm_items)
- se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
- se.to_warehouse = "_Test Warehouse 1 - _TC"
- se.save()
- se.submit()
-
- pr1 = make_purchase_receipt(po.name)
- pr2 = make_purchase_receipt(po.name)
-
- pr1.submit()
- self.assertRaises(frappe.ValidationError, pr2.submit)
- frappe.db.rollback()
-
def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
pr_row_1_serial_no = pr.get("items")[0].serial_no
@@ -1133,103 +993,6 @@
pr.cancel()
pr1.cancel()
- def test_subcontracted_pr_for_multi_transfer_batches(self):
- from erpnext.buying.doctype.purchase_order.purchase_order import (
- make_purchase_receipt,
- make_rm_stock_entry,
- )
- from erpnext.buying.doctype.purchase_order.test_purchase_order import (
- create_purchase_order,
- update_backflush_based_on,
- )
- from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-
- update_backflush_based_on("Material Transferred for Subcontract")
- item_code = "_Test Subcontracted FG Item 3"
-
- make_item(
- "Sub Contracted Raw Material 3",
- {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1},
- )
-
- create_subcontracted_item(
- item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"]
- )
-
- order_qty = 500
- po = create_purchase_order(
- item_code=item_code,
- qty=order_qty,
- is_subcontracted=1,
- supplier_warehouse="_Test Warehouse 1 - _TC",
- )
-
- ste1 = make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="Sub Contracted Raw Material 3",
- qty=300,
- basic_rate=100,
- )
- ste2 = make_stock_entry(
- target="_Test Warehouse - _TC",
- item_code="Sub Contracted Raw Material 3",
- qty=200,
- basic_rate=100,
- )
-
- transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200}
-
- rm_items = [
- {
- "item_code": item_code,
- "rm_item_code": "Sub Contracted Raw Material 3",
- "item_name": "_Test Item",
- "qty": 300,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- "name": po.supplied_items[0].name,
- },
- {
- "item_code": item_code,
- "rm_item_code": "Sub Contracted Raw Material 3",
- "item_name": "_Test Item",
- "qty": 200,
- "warehouse": "_Test Warehouse - _TC",
- "stock_uom": "Nos",
- "name": po.supplied_items[0].name,
- },
- ]
-
- rm_item_string = json.dumps(rm_items)
- se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
- self.assertEqual(len(se.items), 2)
- se.items[0].batch_no = ste1.items[0].batch_no
- se.items[1].batch_no = ste2.items[0].batch_no
- se.submit()
-
- supplied_qty = frappe.db.get_value(
- "Purchase Order Item Supplied",
- {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"},
- "supplied_qty",
- )
-
- self.assertEqual(supplied_qty, 500.00)
-
- pr = make_purchase_receipt(po.name)
- pr.save()
- self.assertEqual(len(pr.supplied_items), 2)
-
- for row in pr.supplied_items:
- self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty)
-
- update_backflush_based_on("BOM")
-
- pr.delete()
- se.cancel()
- ste2.cancel()
- ste1.cancel()
- po.cancel()
-
def test_po_to_pi_and_po_to_pr_worflow_full(self):
"""Test following behaviour:
- Create PO
@@ -1568,43 +1331,5 @@
return pr
-def create_subcontracted_item(**args):
- from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
-
- args = frappe._dict(args)
-
- if not frappe.db.exists("Item", args.item_code):
- make_item(
- args.item_code,
- {
- "is_stock_item": 1,
- "is_sub_contracted_item": 1,
- "has_batch_no": args.get("has_batch_no") or 0,
- },
- )
-
- if not args.raw_materials:
- if not frappe.db.exists("Item", "Test Extra Item 1"):
- make_item(
- "Test Extra Item 1",
- {
- "is_stock_item": 1,
- },
- )
-
- if not frappe.db.exists("Item", "Test Extra Item 2"):
- make_item(
- "Test Extra Item 2",
- {
- "is_stock_item": 1,
- },
- )
-
- args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
-
- if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
- make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
-
-
test_dependencies = ["BOM", "Item Price", "Location"]
test_records = frappe.get_test_records("Purchase Receipt")
diff --git a/erpnext/stock/doctype/purchase_receipt/test_records.json b/erpnext/stock/doctype/purchase_receipt/test_records.json
index 990ad12..e7ea9af 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_records.json
+++ b/erpnext/stock/doctype/purchase_receipt/test_records.json
@@ -83,37 +83,5 @@
}
],
"supplier": "_Test Supplier"
- },
-
- {
- "buying_price_list": "_Test Price List",
- "company": "_Test Company",
- "conversion_rate": 1.0,
- "currency": "INR",
- "doctype": "Purchase Receipt",
- "base_grand_total": 5000.0,
- "is_subcontracted": 1,
- "base_net_total": 5000.0,
- "items": [
- {
- "base_amount": 5000.0,
- "conversion_factor": 1.0,
- "description": "_Test FG Item",
- "doctype": "Purchase Receipt Item",
- "item_code": "_Test FG Item",
- "item_name": "_Test FG Item",
- "parentfield": "items",
- "qty": 10.0,
- "rate": 500.0,
- "received_qty": 10.0,
- "rejected_qty": 0.0,
- "stock_uom": "_Test UOM",
- "uom": "_Test UOM",
- "warehouse": "_Test Warehouse - _TC",
- "cost_center": "Main - _TC"
- }
- ],
- "supplier": "_Test Supplier",
- "supplier_warehouse": "_Test Warehouse - _TC"
}
]
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 1c65ac8..625a303 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -647,7 +647,8 @@
"label": "BOM",
"no_copy": 1,
"options": "BOM",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"default": "0",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 7101190..6042ed4 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -687,7 +687,10 @@
update_rejected_serial_nos = (
True
- if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty)
+ if (
+ controller.doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt")
+ and d.rejected_qty
+ )
else False
)
accepted_serial_nos_updated = False
@@ -700,7 +703,11 @@
qty = d.stock_qty
else:
warehouse = d.warehouse
- qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty
+ qty = (
+ d.qty
+ if controller.doctype in ["Stock Reconciliation", "Subcontracting Receipt"]
+ else d.stock_qty
+ )
for sle in stock_ledger_entries:
if sle.voucher_detail_no == d.name:
if (
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 540ad18..a838236 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -613,7 +613,21 @@
apply_putaway_rule: function (frm) {
if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose);
- }
+ },
+
+ subcontracting_order: (frm) => {
+ if (frm.doc.subcontracting_order) {
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order',
+ source_name: frm.doc.subcontracting_order,
+ target_doc: frm,
+ freeze: true,
+ });
+ }
+ else {
+ frm.set_value("items", []);
+ }
+ },
});
frappe.ui.form.on('Stock Entry Detail', {
@@ -776,11 +790,10 @@
return erpnext.queries.item({is_stock_item: 1});
};
- this.frm.set_query("purchase_order", function() {
+ this.frm.set_query("subcontracting_order", function() {
return {
"filters": {
"docstatus": 1,
- "is_subcontracted": 1,
"company": me.frm.doc.company
}
};
@@ -801,7 +814,7 @@
}
}
- this.frm.add_fetch("purchase_order", "supplier", "supplier");
+ this.frm.add_fetch("subcontracting_order", "supplier", "supplier");
frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' }
this.frm.set_query("supplier_address", erpnext.queries.address_query)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index f56e059..86f1b6a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -15,6 +15,7 @@
"add_to_transit",
"work_order",
"purchase_order",
+ "subcontracting_order",
"delivery_note_no",
"sales_invoice_no",
"pick_list",
@@ -147,13 +148,20 @@
"search_index": 1
},
{
- "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
- "fieldname": "purchase_order",
- "fieldtype": "Link",
- "label": "Purchase Order",
- "options": "Purchase Order"
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "label": "Purchase Order",
+ "options": "Purchase Order",
+ "read_only": 1
},
{
+ "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
+ "fieldname": "subcontracting_order",
+ "fieldtype": "Link",
+ "label": "Subcontracting Order",
+ "options": "Subcontracting Order"
+ },
+ {
"depends_on": "eval:doc.purpose==\"Sales Return\"",
"fieldname": "delivery_note_no",
"fieldtype": "Link",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index f1df54d..19f0561 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -134,8 +134,9 @@
update_serial_nos_after_submit(self, "items")
self.update_work_order()
- self.validate_purchase_order()
- self.update_purchase_order_supplied_items()
+ self.validate_subcontracting_order()
+ self.update_subcontracting_order_supplied_items()
+ self.update_subcontracting_order_status()
self.make_gl_entries()
@@ -154,7 +155,8 @@
self.set_material_request_transfer_status("Completed")
def on_cancel(self):
- self.update_purchase_order_supplied_items()
+ self.update_subcontracting_order_supplied_items()
+ self.update_subcontracting_order_status()
if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status()
@@ -807,8 +809,8 @@
serial_nos.append(sn)
- def validate_purchase_order(self):
- """Throw exception if more raw material is transferred against Purchase Order than in
+ def validate_subcontracting_order(self):
+ """Throw exception if more raw material is transferred against Subcontracting Order than in
the raw materials supplied table"""
backflush_raw_materials_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
@@ -816,24 +818,28 @@
qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"))
- if not (self.purpose == "Send to Subcontractor" and self.purchase_order):
+ if not (self.purpose == "Send to Subcontractor" and self.subcontracting_order):
return
if backflush_raw_materials_based_on == "BOM":
- purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
+ subcontracting_order = frappe.get_doc("Subcontracting Order", self.subcontracting_order)
for se_item in self.items:
item_code = se_item.original_item or se_item.item_code
precision = cint(frappe.db.get_default("float_precision")) or 3
required_qty = sum(
- [flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code]
+ [
+ flt(d.required_qty)
+ for d in subcontracting_order.supplied_items
+ if d.rm_item_code == item_code
+ ]
)
total_allowed = required_qty + (required_qty * (qty_allowance / 100))
if not required_qty:
bom_no = frappe.db.get_value(
- "Purchase Order Item",
- {"parent": self.purchase_order, "item_code": se_item.subcontracted_item},
+ "Subcontracting Order Item",
+ {"parent": self.subcontracting_order, "item_code": se_item.subcontracted_item},
"bom",
)
@@ -845,7 +851,7 @@
required_qty = sum(
[
flt(d.required_qty)
- for d in purchase_order.supplied_items
+ for d in subcontracting_order.supplied_items
if d.rm_item_code == original_item_code
]
)
@@ -854,26 +860,43 @@
if not required_qty:
frappe.throw(
- _("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format(
- se_item.item_code, self.purchase_order
+ _("Item {0} not found in 'Raw Materials Supplied' table in Subcontracting Order {1}").format(
+ se_item.item_code, self.subcontracting_order
)
)
- total_supplied = frappe.db.sql(
- """select sum(transfer_qty)
- from `tabStock Entry Detail`, `tabStock Entry`
- where `tabStock Entry`.purchase_order = %s
- and `tabStock Entry`.docstatus = 1
- and `tabStock Entry Detail`.item_code = %s
- and `tabStock Entry Detail`.parent = `tabStock Entry`.name""",
- (self.purchase_order, se_item.item_code),
- )[0][0]
+
+ parent = frappe.qb.DocType("Stock Entry")
+ child = frappe.qb.DocType("Stock Entry Detail")
+
+ total_supplied = (
+ frappe.qb.from_(parent)
+ .inner_join(child)
+ .on(parent.name == child.parent)
+ .select(Sum(child.transfer_qty))
+ .where(parent.docstatus == 1)
+ .where(parent.subcontracting_order == self.subcontracting_order)
+ .where(child.item_code == se_item.item_code)
+ ).run()[0][0]
if flt(total_supplied, precision) > flt(total_allowed, precision):
frappe.throw(
- _("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format(
- se_item.idx, se_item.item_code, total_allowed, self.purchase_order
+ _(
+ "Row {0}# Item {1} cannot be transferred more than {2} against Subcontracting Order {3}"
+ ).format(
+ se_item.idx, se_item.item_code, total_allowed, self.subcontracting_order
)
)
+ elif not se_item.sco_rm_detail:
+ filters = {
+ "parent": self.subcontracting_order,
+ "docstatus": 1,
+ "rm_item_code": se_item.item_code,
+ "main_item_code": se_item.subcontracted_item,
+ }
+
+ sco_rm_detail = frappe.db.get_value("Subcontracting Order Supplied Item", filters, "name")
+ if sco_rm_detail:
+ se_item.db_set("sco_rm_detail", sco_rm_detail)
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
for row in self.items:
if not row.subcontracted_item:
@@ -882,17 +905,17 @@
row.idx, frappe.bold(row.item_code)
)
)
- elif not row.po_detail:
+ elif not row.sco_rm_detail:
filters = {
- "parent": self.purchase_order,
+ "parent": self.subcontracting_order,
"docstatus": 1,
"rm_item_code": row.item_code,
"main_item_code": row.subcontracted_item,
}
- po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name")
- if po_detail:
- row.db_set("po_detail", po_detail)
+ sco_rm_detail = frappe.db.get_value("Subcontracting Order Supplied Item", filters, "name")
+ if sco_rm_detail:
+ row.db_set("sco_rm_detail", sco_rm_detail)
def validate_bom(self):
for d in self.get("items"):
@@ -1239,11 +1262,13 @@
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
if (
- self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code")
+ self.purpose == "Send to Subcontractor"
+ and self.get("subcontracting_order")
+ and args.get("item_code")
):
subcontract_items = frappe.get_all(
- "Purchase Order Item Supplied",
- {"parent": self.purchase_order, "rm_item_code": args.get("item_code")},
+ "Subcontracting Order Supplied Item",
+ {"parent": self.subcontracting_order, "rm_item_code": args.get("item_code")},
"main_item_code",
)
@@ -1337,27 +1362,27 @@
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
- # Get PO Supplied Items Details
- if self.purchase_order and self.purpose == "Send to Subcontractor":
- # Get PO Supplied Items Details
- item_wh = frappe._dict(
- frappe.db.sql(
- """
- SELECT
- rm_item_code, reserve_warehouse
- FROM
- `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
- WHERE
- po.name = poitemsup.parent and po.name = %s """,
- self.purchase_order,
- )
- )
+ # Get SCO Supplied Items Details
+ if self.subcontracting_order and self.purpose == "Send to Subcontractor":
+ # Get SCO Supplied Items Details
+ parent = frappe.qb.DocType("Subcontracting Order")
+ child = frappe.qb.DocType("Subcontracting Order Supplied Item")
+
+ item_wh = (
+ frappe.qb.from_(parent)
+ .inner_join(child)
+ .on(parent.name == child.parent)
+ .select(child.rm_item_code, child.reserve_warehouse)
+ .where(parent.name == self.subcontracting_order)
+ ).run(as_list=True)
+
+ item_wh = frappe._dict(item_wh)
for item in item_dict.values():
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
item["from_warehouse"] = self.pro_doc.wip_warehouse
- # Get Reserve Warehouse from PO
- if self.purchase_order and self.purpose == "Send to Subcontractor":
+ # Get Reserve Warehouse from SCO
+ if self.subcontracting_order and self.purpose == "Send to Subcontractor":
item["from_warehouse"] = item_wh.get(item.item_code)
item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
@@ -1900,7 +1925,7 @@
se_child.is_process_loss = item_row.get("is_process_loss", 0)
for field in [
- "po_detail",
+ "sco_rm_detail",
"original_item",
"expense_account",
"description",
@@ -1974,26 +1999,27 @@
else:
frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code))
- def update_purchase_order_supplied_items(self):
- if self.purchase_order and (
- self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
+ def update_subcontracting_order_supplied_items(self):
+ if self.subcontracting_order and (
+ self.purpose in ["Send to Subcontractor", "Material Transfer"]
):
- # Get PO Supplied Items Details
- item_wh = frappe._dict(
- frappe.db.sql(
- """
- select rm_item_code, reserve_warehouse
- from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
- where po.name = poitemsup.parent
- and po.name = %s""",
- self.purchase_order,
- )
- )
+ # Get SCO Supplied Items Details
+ parent = frappe.qb.DocType("Subcontracting Order")
+ child = frappe.qb.DocType("Subcontracting Order Supplied Item")
+ item_wh = (
+ frappe.qb.from_(parent)
+ .inner_join(child)
+ .on(parent.name == child.parent)
+ .select(child.rm_item_code, child.reserve_warehouse)
+ .where(parent.name == self.subcontracting_order)
+ ).run(as_list=True)
- supplied_items = get_supplied_items(self.purchase_order)
+ item_wh = frappe._dict(item_wh)
+
+ supplied_items = get_supplied_items(self.subcontracting_order)
for name, item in supplied_items.items():
- frappe.db.set_value("Purchase Order Item Supplied", name, item)
+ frappe.db.set_value("Subcontracting Order Supplied Item", name, item)
# Update reserved sub contracted quantity in bin based on Supplied Item Details and
for d in self.get("items"):
@@ -2194,6 +2220,14 @@
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+ def update_subcontracting_order_status(self):
+ if self.subcontracting_order and self.purpose == "Send to Subcontractor":
+ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ update_subcontracting_order_status,
+ )
+
+ update_subcontracting_order_status(self.subcontracting_order)
+
def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty()
@@ -2342,12 +2376,12 @@
return operating_cost_per_unit
-def get_used_alternative_items(purchase_order=None, work_order=None):
+def get_used_alternative_items(subcontracting_order=None, work_order=None):
cond = ""
- if purchase_order:
- cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(
- purchase_order
+ if subcontracting_order:
+ cond = "and ste.purpose = 'Send to Subcontractor' and ste.subcontracting_order = '{0}'".format(
+ subcontracting_order
)
elif work_order:
cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
@@ -2401,7 +2435,6 @@
@frappe.whitelist()
def get_uom_details(item_code, uom, qty):
"""Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}`
-
:param args: dict with `item_code`, `uom` and `qty`"""
conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor")
@@ -2485,25 +2518,25 @@
return sample_quantity
-def get_supplied_items(purchase_order):
+def get_supplied_items(subcontracting_order):
fields = [
"`tabStock Entry Detail`.`transfer_qty`",
"`tabStock Entry`.`is_return`",
- "`tabStock Entry Detail`.`po_detail`",
+ "`tabStock Entry Detail`.`sco_rm_detail`",
"`tabStock Entry Detail`.`item_code`",
]
filters = [
["Stock Entry", "docstatus", "=", 1],
- ["Stock Entry", "purchase_order", "=", purchase_order],
+ ["Stock Entry", "subcontracting_order", "=", subcontracting_order],
]
supplied_item_details = {}
for row in frappe.get_all("Stock Entry", fields=fields, filters=filters):
- if not row.po_detail:
+ if not row.sco_rm_detail:
continue
- key = row.po_detail
+ key = row.sco_rm_detail
if key not in supplied_item_details:
supplied_item_details.setdefault(
key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0})
@@ -2521,3 +2554,36 @@
)
return supplied_item_details
+
+
+@frappe.whitelist()
+def get_items_from_subcontracting_order(source_name, target_doc=None):
+ sco = frappe.get_doc("Subcontracting Order", source_name)
+
+ if sco.docstatus == 1:
+ if target_doc and isinstance(target_doc, str):
+ target_doc = frappe.get_doc(json.loads(target_doc))
+
+ if target_doc.items:
+ target_doc.items = []
+
+ warehouses = {}
+ for item in sco.items:
+ warehouses[item.name] = item.warehouse
+
+ for item in sco.supplied_items:
+ target_doc.append(
+ "items",
+ {
+ "s_warehouse": warehouses.get(item.reference_name),
+ "t_warehouse": sco.supplier_warehouse,
+ "item_code": item.rm_item_code,
+ "qty": item.required_qty,
+ "transfer_qty": item.required_qty,
+ "uom": item.stock_uom,
+ "stock_uom": item.stock_uom,
+ "conversion_factor": 1,
+ },
+ )
+
+ return target_doc
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 83aed90..efde46d 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -68,6 +68,7 @@
"against_stock_entry",
"ste_detail",
"po_detail",
+ "sco_rm_detail",
"putaway_rule",
"column_break_51",
"reference_purchase_receipt",
@@ -494,6 +495,15 @@
"read_only": 1
},
{
+ "fieldname": "sco_rm_detail",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "SCO Supplied Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
"default": "0",
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",
"fieldname": "set_basic_rate_manually",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 55a213c..83d3669 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -406,61 +406,6 @@
lcv.cancel()
pr.cancel()
- def test_sub_contracted_item_costing(self):
- from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
-
- company = "_Test Company"
- rm_item_code = "_Test Item for Reposting"
- subcontracted_item = "_Test Subcontracted Item for Reposting"
-
- frappe.db.set_value(
- "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
- )
- make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR")
-
- # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
- pr = make_purchase_receipt(
- company=company,
- posting_date="2020-04-10",
- warehouse="Stores - _TC",
- item_code=rm_item_code,
- qty=10,
- rate=100,
- )
-
- # Purchase Receipt for subcontracted item
- pr1 = make_purchase_receipt(
- company=company,
- posting_date="2020-04-20",
- warehouse="Finished Goods - _TC",
- supplier_warehouse="Stores - _TC",
- item_code=subcontracted_item,
- qty=10,
- rate=20,
- is_subcontracted=1,
- )
-
- self.assertEqual(pr1.items[0].valuation_rate, 120)
-
- # Update raw material's valuation via LCV, Additional cost = 50
- lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
-
- pr1.reload()
- self.assertEqual(pr1.items[0].valuation_rate, 125)
-
- # check outgoing_rate for DN after reposting
- incoming_rate = frappe.db.get_value(
- "Stock Ledger Entry",
- {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item},
- "incoming_rate",
- )
- self.assertEqual(incoming_rate, 125)
-
- # cleanup data
- pr1.cancel()
- lcv.cancel()
- pr.cancel()
-
def test_back_dated_entry_not_allowed(self):
# Back dated stock transactions are only allowed to stock managers
frappe.db.set_value(
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index c8d9f54..e701c14 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -240,8 +240,8 @@
throw(_("Item {0} is a template, please select one of its variants").format(item.name))
elif args.transaction_type == "buying" and args.doctype != "Material Request":
- if args.get("is_subcontracted") and item.is_sub_contracted_item != 1:
- throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
+ if args.get("is_subcontracted") and item.is_stock_item:
+ throw(_("Item {0} must be a Non-Stock Item").format(item.name))
def get_basic_details(args, item, overwrite_warehouse=True):
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 4789b52..9293dde 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -636,6 +636,7 @@
"Purchase Invoice",
"Delivery Note",
"Sales Invoice",
+ "Subcontracting Receipt",
):
if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
from erpnext.controllers.sales_and_purchase_return import (
@@ -652,6 +653,8 @@
else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate"
+ elif sle.voucher_type == "Subcontracting Receipt":
+ rate_field = "rate"
else:
rate_field = "incoming_rate"
@@ -665,6 +668,8 @@
else:
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
ref_doctype = "Packed Item"
+ elif sle == "Subcontracting Receipt":
+ ref_doctype = "Subcontracting Receipt Supplied Item"
else:
ref_doctype = "Purchase Receipt Item Supplied"
@@ -690,6 +695,8 @@
self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
self.update_rate_on_purchase_receipt(sle, outgoing_rate)
+ elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
+ self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
def update_rate_on_stock_entry(self, sle, outgoing_rate):
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
@@ -731,12 +738,13 @@
"Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
)
- # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
- if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
- doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
- doc.update_valuation_rate(reset_outgoing_rate=False)
- for d in doc.items + doc.supplied_items:
- d.db_update()
+ def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
+ if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
+ else:
+ frappe.db.set_value(
+ "Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
+ )
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
diff --git a/erpnext/subcontracting/__init__.py b/erpnext/subcontracting/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/__init__.py
diff --git a/erpnext/subcontracting/doctype/__init__.py b/erpnext/subcontracting/doctype/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
new file mode 100644
index 0000000..c9e4577
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -0,0 +1,322 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.provide('erpnext.buying');
+
+frappe.ui.form.on('Subcontracting Order', {
+ setup: (frm) => {
+ frm.get_field("items").grid.cannot_add_rows = true;
+ frm.get_field("items").grid.only_sortable();
+
+ frm.set_indicator_formatter('item_code',
+ (doc) => (doc.qty <= doc.received_qty) ? 'green' : 'orange');
+
+ frm.set_query('supplier_warehouse', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query('purchase_order', () => {
+ return {
+ filters: {
+ docstatus: 1,
+ is_subcontracted: 1
+ }
+ };
+ });
+
+ frm.set_query('set_warehouse', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query('warehouse', 'items', () => ({
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ }));
+
+ frm.set_query('expense_account', 'items', () => ({
+ query: 'erpnext.controllers.queries.get_expense_account',
+ filters: {
+ company: frm.doc.company
+ }
+ }));
+
+ frm.set_query('bom', 'items', (doc, cdt, cdn) => {
+ let d = locals[cdt][cdn];
+ return {
+ filters: {
+ item: d.item_code,
+ is_active: 1,
+ docstatus: 1,
+ company: frm.doc.company
+ }
+ };
+ });
+
+ frm.set_query('set_reserve_warehouse', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ name: ['!=', frm.doc.supplier_warehouse],
+ is_group: 0
+ }
+ };
+ });
+ },
+
+ onload: (frm) => {
+ if (!frm.doc.transaction_date) {
+ frm.set_value('transaction_date', frappe.datetime.get_today());
+ }
+ },
+
+ purchase_order: (frm) => {
+ frm.set_value('service_items', null);
+ frm.set_value('items', null);
+ frm.set_value('supplied_items', null);
+
+ if (frm.doc.purchase_order) {
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order',
+ source_name: frm.doc.purchase_order,
+ target_doc: frm,
+ freeze: true,
+ freeze_message: __('Mapping Subcontracting Order ...'),
+ });
+ }
+ },
+
+ refresh: function (frm) {
+ frm.trigger('get_materials_from_supplier');
+ },
+
+ get_materials_from_supplier: function (frm) {
+ let sco_rm_details = [];
+
+ if (frm.doc.supplied_items && (frm.doc.per_received == 100)) {
+ frm.doc.supplied_items.forEach(d => {
+ if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
+ sco_rm_details.push(d.name);
+ }
+ });
+ }
+
+ if (sco_rm_details && sco_rm_details.length) {
+ frm.add_custom_button(__('Return of Components'), () => {
+ frm.call({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.get_materials_from_supplier',
+ freeze: true,
+ freeze_message: __('Creating Stock Entry'),
+ args: { subcontracting_order: frm.doc.name, sco_rm_details: sco_rm_details },
+ callback: function (r) {
+ if (r && r.message) {
+ const doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ }
+ }
+ });
+ }, __('Create'));
+ }
+ }
+});
+
+erpnext.buying.SubcontractingOrderController = class SubcontractingOrderController {
+ setup() {
+ this.frm.custom_make_buttons = {
+ 'Subcontracting Receipt': 'Subcontracting Receipt',
+ 'Stock Entry': 'Material to Supplier',
+ };
+ }
+
+ refresh(doc) {
+ var me = this;
+
+ if (doc.docstatus == 1) {
+ if (doc.status != 'Completed') {
+ if (flt(doc.per_received) < 100) {
+ cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create'));
+ if (me.has_unsupplied_items()) {
+ cur_frm.add_custom_button(__('Material to Supplier'),
+ () => {
+ me.make_stock_entry();
+ }, __('Transfer'));
+ }
+ }
+ cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+ }
+ }
+
+ items_add(doc, cdt, cdn) {
+ if (doc.set_warehouse) {
+ var row = frappe.get_doc(cdt, cdn);
+ row.warehouse = doc.set_warehouse;
+ }
+ }
+
+ set_warehouse(doc) {
+ this.set_warehouse_in_children(doc.items, "warehouse", doc.set_warehouse);
+ }
+
+ set_reserve_warehouse(doc) {
+ this.set_warehouse_in_children(doc.supplied_items, "reserve_warehouse", doc.set_reserve_warehouse);
+ }
+
+ set_warehouse_in_children(child_table, warehouse_field, warehouse) {
+ let transaction_controller = new erpnext.TransactionController();
+ transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
+ }
+
+ make_stock_entry() {
+ var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false);
+ var me = this;
+
+ if (items.length >= 1) {
+ me.raw_material_data = [];
+ me.show_dialog = 1;
+ let title = __('Transfer Material to Supplier');
+ let fields = [
+ { fieldtype: 'Section Break', label: __('Raw Materials') },
+ {
+ fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'),
+ fields: [
+ {
+ fieldtype: 'Data',
+ fieldname: 'item_code',
+ label: __('Item'),
+ read_only: 1,
+ in_list_view: 1
+ },
+ {
+ fieldtype: 'Data',
+ fieldname: 'rm_item_code',
+ label: __('Raw Material'),
+ read_only: 1,
+ in_list_view: 1
+ },
+ {
+ fieldtype: 'Float',
+ read_only: 1,
+ fieldname: 'qty',
+ label: __('Quantity'),
+ in_list_view: 1
+ },
+ {
+ fieldtype: 'Data',
+ read_only: 1,
+ fieldname: 'warehouse',
+ label: __('Reserve Warehouse'),
+ in_list_view: 1
+ },
+ {
+ fieldtype: 'Float',
+ read_only: 1,
+ fieldname: 'rate',
+ label: __('Rate'),
+ hidden: 1
+ },
+ {
+ fieldtype: 'Float',
+ read_only: 1,
+ fieldname: 'amount',
+ label: __('Amount'),
+ hidden: 1
+ },
+ {
+ fieldtype: 'Link',
+ read_only: 1,
+ fieldname: 'uom',
+ label: __('UOM'),
+ hidden: 1
+ }
+ ],
+ data: me.raw_material_data,
+ get_data: () => me.raw_material_data
+ }
+ ];
+
+ me.dialog = new frappe.ui.Dialog({
+ title: title, fields: fields
+ });
+
+ if (me.frm.doc['supplied_items']) {
+ me.frm.doc['supplied_items'].forEach((item) => {
+ if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) {
+ me.raw_material_data.push({
+ 'name': item.name,
+ 'item_code': item.main_item_code,
+ 'rm_item_code': item.rm_item_code,
+ 'item_name': item.rm_item_code,
+ 'qty': item.required_qty - item.supplied_qty,
+ 'warehouse': item.reserve_warehouse,
+ 'rate': item.rate,
+ 'amount': item.amount,
+ 'stock_uom': item.stock_uom
+ });
+ me.dialog.fields_dict.sub_con_rm_items.grid.refresh();
+ }
+ });
+ }
+
+ me.dialog.get_field('sub_con_rm_items').check_all_rows();
+
+ me.dialog.show();
+ this.dialog.set_primary_action(__('Transfer'), () => {
+ me.values = me.dialog.get_values();
+ if (me.values) {
+ me.values.sub_con_rm_items.map((row, i) => {
+ if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
+ let row_id = i + 1;
+ frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id]));
+ }
+ });
+ me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children());
+ me.dialog.hide();
+ }
+ });
+ }
+
+ me.dialog.get_close_btn().on('click', () => {
+ me.dialog.hide();
+ });
+ }
+
+ has_unsupplied_items() {
+ return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
+ }
+
+ make_subcontracting_receipt() {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
+ frm: cur_frm,
+ freeze_message: __('Creating Subcontracting Receipt ...')
+ });
+ }
+
+ make_rm_stock_entry(rm_items) {
+ frappe.call({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_rm_stock_entry',
+ args: {
+ subcontracting_order: cur_frm.doc.name,
+ rm_items: rm_items
+ },
+ callback: (r) => {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route('Form', doclist[0].doctype, doclist[0].name);
+ }
+ });
+ }
+};
+
+extend_cscript(cur_frm.cscript, new erpnext.buying.SubcontractingOrderController({ frm: cur_frm }));
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
new file mode 100644
index 0000000..c6e76c7
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json
@@ -0,0 +1,485 @@
+{
+ "actions": [],
+ "allow_auto_repeat": 1,
+ "allow_import": 1,
+ "autoname": "naming_series:",
+ "creation": "2022-04-01 22:39:17.662819",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "naming_series",
+ "purchase_order",
+ "supplier",
+ "supplier_name",
+ "supplier_warehouse",
+ "column_break_7",
+ "company",
+ "transaction_date",
+ "schedule_date",
+ "amended_from",
+ "address_and_contact_section",
+ "supplier_address",
+ "address_display",
+ "contact_person",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "column_break_19",
+ "shipping_address",
+ "shipping_address_display",
+ "billing_address",
+ "billing_address_display",
+ "section_break_24",
+ "column_break_25",
+ "set_warehouse",
+ "items",
+ "section_break_32",
+ "total_qty",
+ "column_break_29",
+ "total",
+ "service_items_section",
+ "service_items",
+ "raw_materials_supplied_section",
+ "set_reserve_warehouse",
+ "supplied_items",
+ "additional_costs_section",
+ "distribute_additional_costs_based_on",
+ "additional_costs",
+ "total_additional_costs",
+ "order_status_section",
+ "status",
+ "column_break_39",
+ "per_received",
+ "printing_settings_section",
+ "select_print_heading",
+ "column_break_43",
+ "letter_head"
+ ],
+ "fields": [
+ {
+ "allow_on_submit": 1,
+ "default": "{supplier_name}",
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "options": "SC-ORD-.YYYY.-",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "label": "Subcontracting Purchase Order",
+ "options": "Purchase Order",
+ "reqd": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_standard_filter": 1,
+ "label": "Supplier",
+ "options": "Supplier",
+ "print_hide": 1,
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "bold": 1,
+ "fetch_from": "supplier.supplier_name",
+ "fieldname": "supplier_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "label": "Supplier Name",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "depends_on": "supplier",
+ "fieldname": "supplier_warehouse",
+ "fieldtype": "Link",
+ "label": "Supplier Warehouse",
+ "options": "Warehouse",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "print_hide": 1,
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "default": "Today",
+ "fetch_from": "purchase_order.transaction_date",
+ "fetch_if_empty": 1,
+ "fieldname": "transaction_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fetch_from": "purchase_order.schedule_date",
+ "fetch_if_empty": 1,
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "label": "Required By",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Subcontracting Order",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "address_and_contact_section",
+ "fieldtype": "Section Break",
+ "label": "Address and Contact"
+ },
+ {
+ "fetch_from": "supplier.supplier_primary_address",
+ "fetch_if_empty": 1,
+ "fieldname": "supplier_address",
+ "fieldtype": "Link",
+ "label": "Supplier Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "address_display",
+ "fieldtype": "Small Text",
+ "label": "Supplier Address Details",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "supplier.supplier_primary_contact",
+ "fetch_if_empty": 1,
+ "fieldname": "contact_person",
+ "fieldtype": "Link",
+ "label": "Supplier Contact",
+ "options": "Contact",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "contact_display",
+ "fieldtype": "Small Text",
+ "in_global_search": 1,
+ "label": "Contact Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_mobile",
+ "fieldtype": "Small Text",
+ "label": "Contact Mobile No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_email",
+ "fieldtype": "Small Text",
+ "label": "Contact Email",
+ "options": "Email",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "shipping_address",
+ "fieldtype": "Link",
+ "label": "Company Shipping Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "shipping_address_display",
+ "fieldtype": "Small Text",
+ "label": "Shipping Address Details",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "billing_address",
+ "fieldtype": "Link",
+ "label": "Company Billing Address",
+ "options": "Address"
+ },
+ {
+ "fieldname": "billing_address_display",
+ "fieldtype": "Small Text",
+ "label": "Billing Address Details",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_24",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_25",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "purchase_order",
+ "description": "Sets 'Warehouse' in each row of the Items table.",
+ "fieldname": "set_warehouse",
+ "fieldtype": "Link",
+ "label": "Set Target Warehouse",
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "allow_bulk_edit": 1,
+ "depends_on": "purchase_order",
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "options": "Subcontracting Order Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_32",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "purchase_order",
+ "fieldname": "total_qty",
+ "fieldtype": "Float",
+ "label": "Total Quantity",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_29",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "purchase_order",
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "label": "Total",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "depends_on": "purchase_order",
+ "fieldname": "service_items_section",
+ "fieldtype": "Section Break",
+ "label": "Service Items"
+ },
+ {
+ "fieldname": "service_items",
+ "fieldtype": "Table",
+ "label": "Service Items",
+ "options": "Subcontracting Order Service Item",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "supplied_items",
+ "depends_on": "supplied_items",
+ "fieldname": "raw_materials_supplied_section",
+ "fieldtype": "Section Break",
+ "label": "Raw Materials Supplied"
+ },
+ {
+ "depends_on": "supplied_items",
+ "description": "Sets 'Reserve Warehouse' in each row of the Supplied Items table.",
+ "fieldname": "set_reserve_warehouse",
+ "fieldtype": "Link",
+ "label": "Set Reserve Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "supplied_items",
+ "fieldtype": "Table",
+ "label": "Supplied Items",
+ "no_copy": 1,
+ "options": "Subcontracting Order Supplied Item",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "total_additional_costs",
+ "depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)",
+ "fieldname": "additional_costs_section",
+ "fieldtype": "Section Break",
+ "label": "Additional Costs"
+ },
+ {
+ "fieldname": "additional_costs",
+ "fieldtype": "Table",
+ "label": "Additional Costs",
+ "options": "Landed Cost Taxes and Charges"
+ },
+ {
+ "fieldname": "total_additional_costs",
+ "fieldtype": "Currency",
+ "label": "Total Additional Costs",
+ "print_hide_if_no_value": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "order_status_section",
+ "fieldtype": "Section Break",
+ "label": "Order Status"
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fieldname": "column_break_39",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "per_received",
+ "fieldtype": "Percent",
+ "in_list_view": 1,
+ "label": "% Received",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "printing_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Printing Settings",
+ "print_hide": 1,
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "select_print_heading",
+ "fieldtype": "Link",
+ "label": "Print Heading",
+ "no_copy": 1,
+ "options": "Print Heading",
+ "print_hide": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "column_break_43",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "label": "Letter Head",
+ "options": "Letter Head",
+ "print_hide": 1
+ },
+ {
+ "default": "Qty",
+ "fieldname": "distribute_additional_costs_based_on",
+ "fieldtype": "Select",
+ "label": "Distribute Additional Costs Based On ",
+ "options": "Qty\nAmount"
+ }
+ ],
+ "icon": "fa fa-file-text",
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-04-11 21:02:44.097841",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Order",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "read": 1,
+ "report": 1,
+ "role": "Stock User"
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "Purchase Manager",
+ "write": 1
+ }
+ ],
+ "search_fields": "status, transaction_date, supplier",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "timeline_field": "supplier",
+ "title_field": "supplier_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
new file mode 100644
index 0000000..d12c9e8
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -0,0 +1,372 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe import _
+from frappe.model.mapper import get_mapped_doc
+from frappe.utils import flt
+
+from erpnext.controllers.subcontracting_controller import SubcontractingController
+from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
+from erpnext.stock.utils import get_bin
+
+
+class SubcontractingOrder(SubcontractingController):
+ def before_validate(self):
+ super(SubcontractingOrder, self).before_validate()
+
+ def validate(self):
+ super(SubcontractingOrder, self).validate()
+ self.validate_purchase_order_for_subcontracting()
+ self.validate_items()
+ self.validate_service_items()
+ self.validate_supplied_items()
+ self.set_missing_values()
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+
+ def on_submit(self):
+ self.update_ordered_qty_for_subcontracting()
+ self.update_reserved_qty_for_subcontracting()
+ self.update_status()
+
+ def on_cancel(self):
+ self.update_ordered_qty_for_subcontracting()
+ self.update_reserved_qty_for_subcontracting()
+ self.update_status()
+
+ def validate_purchase_order_for_subcontracting(self):
+ if self.purchase_order:
+ po = frappe.get_doc("Purchase Order", self.purchase_order)
+ if not po.is_subcontracted:
+ frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting."))
+
+ if po.docstatus != 1:
+ msg = f"Please submit Purchase Order {po.name} before proceeding."
+ frappe.throw(_(msg))
+
+ if po.per_received == 100:
+ msg = f"Cannot create more Subcontracting Orders against the Purchase Order {po.name}."
+ frappe.throw(_(msg))
+ else:
+ self.service_items = self.items = self.supplied_items = None
+ frappe.throw(_("Please select a Subcontracting Purchase Order."))
+
+ def validate_service_items(self):
+ for item in self.service_items:
+ if frappe.get_value("Item", item.item_code, "is_stock_item"):
+ msg = f"Service Item {item.item_name} must be a non-stock item."
+ frappe.throw(_(msg))
+
+ def validate_supplied_items(self):
+ if self.supplier_warehouse:
+ for item in self.supplied_items:
+ if self.supplier_warehouse == item.reserve_warehouse:
+ msg = f"Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {item.main_item_code}."
+ frappe.throw(_(msg))
+
+ def set_missing_values(self):
+ self.set_missing_values_in_additional_costs()
+ self.set_missing_values_in_service_items()
+ self.set_missing_values_in_supplied_items()
+ self.set_missing_values_in_items()
+
+ def set_missing_values_in_additional_costs(self):
+ if self.get("additional_costs"):
+ self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
+
+ if self.total_additional_costs:
+ if self.distribute_additional_costs_based_on == "Amount":
+ total_amt = sum(flt(item.amount) for item in self.get("items"))
+ for item in self.items:
+ item.additional_cost_per_qty = (
+ (item.amount * self.total_additional_costs) / total_amt
+ ) / item.qty
+ else:
+ total_qty = sum(flt(item.qty) for item in self.get("items"))
+ additional_cost_per_qty = self.total_additional_costs / total_qty
+ for item in self.items:
+ item.additional_cost_per_qty = additional_cost_per_qty
+ else:
+ self.total_additional_costs = 0
+
+ def set_missing_values_in_service_items(self):
+ for idx, item in enumerate(self.get("service_items")):
+ self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty
+
+ def set_missing_values_in_supplied_items(self):
+ for item in self.get("items"):
+ bom = frappe.get_doc("BOM", item.bom)
+ rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items)
+ item.rm_cost_per_qty = rm_cost / flt(bom.quantity)
+
+ def set_missing_values_in_items(self):
+ total_qty = total = 0
+ for item in self.items:
+ item.rate = (
+ item.rm_cost_per_qty + item.service_cost_per_qty + (item.additional_cost_per_qty or 0)
+ )
+ item.amount = item.qty * item.rate
+ total_qty += flt(item.qty)
+ total += flt(item.amount)
+ else:
+ self.total_qty = total_qty
+ self.total = total
+
+ def update_ordered_qty_for_subcontracting(self, sco_item_rows=None):
+ item_wh_list = []
+ for item in self.get("items"):
+ if (
+ (not sco_item_rows or item.name in sco_item_rows)
+ and [item.item_code, item.warehouse] not in item_wh_list
+ and frappe.get_cached_value("Item", item.item_code, "is_stock_item")
+ and item.warehouse
+ ):
+ item_wh_list.append([item.item_code, item.warehouse])
+ for item_code, warehouse in item_wh_list:
+ update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
+
+ def update_reserved_qty_for_subcontracting(self):
+ for item in self.supplied_items:
+ if item.rm_item_code:
+ stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse)
+ stock_bin.update_reserved_qty_for_sub_contracting()
+
+ def populate_items_table(self):
+ items = []
+
+ for si in self.service_items:
+ if si.fg_item:
+ item = frappe.get_doc("Item", si.fg_item)
+ bom = frappe.db.get_value("BOM", {"item": item.item_code, "is_active": 1, "is_default": 1})
+
+ items.append(
+ {
+ "item_code": item.item_code,
+ "item_name": item.item_name,
+ "schedule_date": self.schedule_date,
+ "description": item.description,
+ "qty": si.fg_item_qty,
+ "stock_uom": item.stock_uom,
+ "bom": bom,
+ },
+ )
+ else:
+ frappe.throw(
+ _("Please select Finished Good Item for Service Item {0}").format(
+ si.item_name or si.item_code
+ )
+ )
+ else:
+ for item in items:
+ self.append("items", item)
+ else:
+ self.set_missing_values()
+
+ def update_status(self, status=None, update_modified=False):
+ if self.docstatus >= 1 and not status:
+ if self.docstatus == 1:
+ if self.status == "Draft":
+ status = "Open"
+ elif self.per_received >= 100:
+ status = "Completed"
+ elif self.per_received > 0 and self.per_received < 100:
+ status = "Partially Received"
+ else:
+ total_required_qty = total_supplied_qty = 0
+ for item in self.supplied_items:
+ total_required_qty += item.required_qty
+ total_supplied_qty += item.supplied_qty or 0
+ if total_supplied_qty:
+ status = "Partial Material Transferred"
+ if total_supplied_qty >= total_required_qty:
+ status = "Material Transferred"
+ elif self.docstatus == 2:
+ status = "Cancelled"
+
+ if status:
+ frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified)
+
+
+@frappe.whitelist()
+def make_subcontracting_receipt(source_name, target_doc=None):
+ return get_mapped_subcontracting_receipt(source_name, target_doc)
+
+
+def get_mapped_subcontracting_receipt(source_name, target_doc=None):
+ def update_item(obj, target, source_parent):
+ target.qty = flt(obj.qty) - flt(obj.received_qty)
+ target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
+
+ target_doc = get_mapped_doc(
+ "Subcontracting Order",
+ source_name,
+ {
+ "Subcontracting Order": {
+ "doctype": "Subcontracting Receipt",
+ "field_map": {"supplier_warehouse": "supplier_warehouse"},
+ "validation": {
+ "docstatus": ["=", 1],
+ },
+ },
+ "Subcontracting Order Item": {
+ "doctype": "Subcontracting Receipt Item",
+ "field_map": {
+ "name": "subcontracting_order_item",
+ "parent": "subcontracting_order",
+ "bom": "bom",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
+ },
+ },
+ target_doc,
+ )
+
+ return target_doc
+
+
+def get_item_details(items):
+ item = frappe.qb.DocType("Item")
+ item_list = (
+ frappe.qb.from_(item)
+ .select(item.item_code, item.description, item.allow_alternative_item)
+ .where(item.name.isin(items))
+ .run(as_dict=True)
+ )
+
+ item_details = {}
+ for item in item_list:
+ item_details[item.item_code] = item
+
+ return item_details
+
+
+@frappe.whitelist()
+def make_rm_stock_entry(subcontracting_order, rm_items):
+ rm_items_list = rm_items
+
+ if isinstance(rm_items, str):
+ rm_items_list = json.loads(rm_items)
+ elif not rm_items:
+ frappe.throw(_("No Items available for transfer"))
+
+ if rm_items_list:
+ fg_items = list(set(item["item_code"] for item in rm_items_list))
+ else:
+ frappe.throw(_("No Items selected for transfer"))
+
+ if subcontracting_order:
+ subcontracting_order = frappe.get_doc("Subcontracting Order", subcontracting_order)
+
+ if fg_items:
+ items = tuple(set(item["rm_item_code"] for item in rm_items_list))
+ item_wh = get_item_details(items)
+
+ stock_entry = frappe.new_doc("Stock Entry")
+ stock_entry.purpose = "Send to Subcontractor"
+ stock_entry.subcontracting_order = subcontracting_order.name
+ stock_entry.supplier = subcontracting_order.supplier
+ stock_entry.supplier_name = subcontracting_order.supplier_name
+ stock_entry.supplier_address = subcontracting_order.supplier_address
+ stock_entry.address_display = subcontracting_order.address_display
+ stock_entry.company = subcontracting_order.company
+ stock_entry.to_warehouse = subcontracting_order.supplier_warehouse
+ stock_entry.set_stock_entry_type()
+
+ for item_code in fg_items:
+ for rm_item_data in rm_items_list:
+ if rm_item_data["item_code"] == item_code:
+ rm_item_code = rm_item_data["rm_item_code"]
+ items_dict = {
+ rm_item_code: {
+ "sco_rm_detail": rm_item_data.get("name"),
+ "item_name": rm_item_data["item_name"],
+ "description": item_wh.get(rm_item_code, {}).get("description", ""),
+ "qty": rm_item_data["qty"],
+ "from_warehouse": rm_item_data["warehouse"],
+ "stock_uom": rm_item_data["stock_uom"],
+ "serial_no": rm_item_data.get("serial_no"),
+ "batch_no": rm_item_data.get("batch_no"),
+ "main_item_code": rm_item_data["item_code"],
+ "allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
+ }
+ }
+ stock_entry.add_to_stock_entry_detail(items_dict)
+ return stock_entry.as_dict()
+ else:
+ frappe.throw(_("No Items selected for transfer"))
+ return subcontracting_order.name
+
+
+def add_items_in_ste(ste_doc, row, qty, sco_rm_details, batch_no=None):
+ item = ste_doc.append("items", row.item_details)
+
+ sco_rm_detail = list(set(row.sco_rm_details).intersection(sco_rm_details))
+ item.update(
+ {
+ "qty": qty,
+ "batch_no": batch_no,
+ "basic_rate": row.item_details["rate"],
+ "sco_rm_detail": sco_rm_detail[0] if sco_rm_detail else "",
+ "s_warehouse": row.item_details["t_warehouse"],
+ "t_warehouse": row.item_details["s_warehouse"],
+ "item_code": row.item_details["rm_item_code"],
+ "subcontracted_item": row.item_details["main_item_code"],
+ "serial_no": "\n".join(row.serial_no) if row.serial_no else "",
+ }
+ )
+
+
+def make_return_stock_entry_for_subcontract(available_materials, sco_doc, sco_rm_details):
+ ste_doc = frappe.new_doc("Stock Entry")
+ ste_doc.purpose = "Material Transfer"
+
+ ste_doc.subcontracting_order = sco_doc.name
+ ste_doc.company = sco_doc.company
+ ste_doc.is_return = 1
+
+ for key, value in available_materials.items():
+ if not value.qty:
+ continue
+
+ if value.batch_no:
+ for batch_no, qty in value.batch_no.items():
+ if qty > 0:
+ add_items_in_ste(ste_doc, value, value.qty, sco_rm_details, batch_no)
+ else:
+ add_items_in_ste(ste_doc, value, value.qty, sco_rm_details)
+
+ ste_doc.set_stock_entry_type()
+ ste_doc.calculate_rate_and_amount()
+
+ return ste_doc
+
+
+@frappe.whitelist()
+def get_materials_from_supplier(subcontracting_order, sco_rm_details):
+ if isinstance(sco_rm_details, str):
+ sco_rm_details = json.loads(sco_rm_details)
+
+ doc = frappe.get_cached_doc("Subcontracting Order", subcontracting_order)
+ doc.initialized_fields()
+ doc.subcontracting_orders = [doc.name]
+ doc.get_available_materials()
+
+ if not doc.available_materials:
+ frappe.throw(
+ _("Materials are already received against the Subcontracting Order {0}").format(
+ subcontracting_order
+ )
+ )
+
+ return make_return_stock_entry_for_subcontract(doc.available_materials, doc, sco_rm_details)
+
+
+@frappe.whitelist()
+def update_subcontracting_order_status(sco):
+ if isinstance(sco, str):
+ sco = frappe.get_doc("Subcontracting Order", sco)
+
+ sco.update_status()
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py
new file mode 100644
index 0000000..f17d8cd
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_dashboard.py
@@ -0,0 +1,8 @@
+from frappe import _
+
+
+def get_data():
+ return {
+ "fieldname": "subcontracting_order",
+ "transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
+ }
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
new file mode 100644
index 0000000..650419c
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.listview_settings['Subcontracting Order'] = {
+ get_indicator: function (doc) {
+ const status_colors = {
+ "Draft": "grey",
+ "Open": "orange",
+ "Partially Received": "yellow",
+ "Completed": "green",
+ "Partial Material Transferred": "purple",
+ "Material Transferred": "blue",
+ };
+ return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
+ },
+};
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
new file mode 100644
index 0000000..1454f1a
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -0,0 +1,530 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import copy
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
+from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_rm_items,
+ get_subcontracting_order,
+ make_bom_for_subcontracted_items,
+ make_raw_materials,
+ make_service_items,
+ make_stock_in_entry,
+ make_stock_transfer_entry,
+ make_subcontracted_item,
+ make_subcontracted_items,
+ set_backflush_based_on,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_rm_stock_entry,
+ make_subcontracting_receipt,
+)
+
+
+class TestSubcontractingOrder(FrappeTestCase):
+ def setUp(self):
+ make_subcontracted_items()
+ make_raw_materials()
+ make_service_items()
+ make_bom_for_subcontracted_items()
+
+ def test_populate_items_table(self):
+ sco = get_subcontracting_order()
+ sco.items = None
+ sco.populate_items_table()
+ self.assertEqual(len(sco.service_items), len(sco.items))
+
+ def test_set_missing_values(self):
+ sco = get_subcontracting_order()
+ before = {sco.total_qty, sco.total, sco.total_additional_costs}
+ sco.total_qty = sco.total = sco.total_additional_costs = 0
+ sco.set_missing_values()
+ after = {sco.total_qty, sco.total, sco.total_additional_costs}
+ self.assertSetEqual(before, after)
+
+ def test_update_status(self):
+ # Draft
+ sco = get_subcontracting_order(do_not_submit=1)
+ self.assertEqual(sco.status, "Draft")
+
+ # Open
+ sco.submit()
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Open")
+
+ # Partial Material Transferred
+ rm_items = get_rm_items(sco.supplied_items)
+ rm_items[0]["qty"] -= 1
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Partial Material Transferred")
+
+ # Material Transferred
+ rm_items[0]["qty"] = 1
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Material Transferred")
+
+ # Partially Received
+ scr = make_subcontracting_receipt(sco.name)
+ scr.items[0].qty -= 1
+ scr.save()
+ scr.submit()
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Partially Received")
+
+ # Completed
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ scr.submit()
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Completed")
+
+ def test_make_rm_stock_entry(self):
+ sco = get_subcontracting_order()
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ ste = make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ self.assertEqual(len(ste.items), len(rm_items))
+
+ def test_make_rm_stock_entry_for_serial_items(self):
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 2",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA2",
+ "fg_item_qty": 5,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 5",
+ "qty": 6,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA5",
+ "fg_item_qty": 6,
+ },
+ ]
+
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ ste = make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ self.assertEqual(len(ste.items), len(rm_items))
+
+ def test_make_rm_stock_entry_for_batch_items(self):
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 4",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA4",
+ "fg_item_qty": 5,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 6",
+ "qty": 6,
+ "rate": 100,
+ "fg_item": "Subcontracted Item SA6",
+ "fg_item_qty": 6,
+ },
+ ]
+
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ ste = make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ self.assertEqual(len(ste.items), len(rm_items))
+
+ def test_update_reserved_qty_for_subcontracting(self):
+ # Make stock available for raw materials
+ make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse 1 - _TC",
+ item_code="_Test Item Home Desktop 100",
+ qty=30,
+ basic_rate=100,
+ )
+
+ bin1 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
+ as_dict=1,
+ )
+
+ # Create SCO
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "_Test FG Item",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+
+ bin2 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
+ as_dict=1,
+ )
+
+ self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
+ self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
+ self.assertNotEqual(bin1.modified, bin2.modified)
+
+ # Create stock transfer
+ rm_items = [
+ {
+ "item_code": "_Test FG Item",
+ "rm_item_code": "_Test Item",
+ "item_name": "_Test Item",
+ "qty": 6,
+ "warehouse": "_Test Warehouse - _TC",
+ "rate": 100,
+ "amount": 600,
+ "stock_uom": "Nos",
+ }
+ ]
+ ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
+ ste.to_warehouse = "_Test Warehouse 1 - _TC"
+ ste.save()
+ ste.submit()
+
+ bin3 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname="reserved_qty_for_sub_contract",
+ as_dict=1,
+ )
+
+ self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
+
+ make_stock_entry(
+ target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse 1 - _TC",
+ item_code="_Test Item Home Desktop 100",
+ qty=40,
+ basic_rate=100,
+ )
+
+ # Make SCR against the SCO
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ scr.submit()
+
+ bin4 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname="reserved_qty_for_sub_contract",
+ as_dict=1,
+ )
+
+ self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
+
+ # Cancel SCR
+ scr.reload()
+ scr.cancel()
+ bin5 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname="reserved_qty_for_sub_contract",
+ as_dict=1,
+ )
+
+ self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
+
+ # Cancel Stock Entry
+ ste.cancel()
+ bin6 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname="reserved_qty_for_sub_contract",
+ as_dict=1,
+ )
+
+ self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
+
+ # Cancel PO
+ sco.reload()
+ sco.cancel()
+ bin7 = frappe.db.get_value(
+ "Bin",
+ filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
+ fieldname="reserved_qty_for_sub_contract",
+ as_dict=1,
+ )
+
+ self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
+
+ def test_exploded_items(self):
+ item_code = "_Test Subcontracted FG Item 11"
+ make_subcontracted_item(item_code=item_code)
+
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 100,
+ "fg_item": item_code,
+ "fg_item_qty": 1,
+ },
+ ]
+
+ sco1 = get_subcontracting_order(service_items=service_items, include_exploded_items=1)
+ item_name = frappe.db.get_value("BOM", {"item": item_code}, "name")
+ bom = frappe.get_doc("BOM", item_name)
+ exploded_items = sorted([item.item_code for item in bom.exploded_items])
+ supplied_items = sorted([item.rm_item_code for item in sco1.supplied_items])
+ self.assertEqual(exploded_items, supplied_items)
+
+ sco2 = get_subcontracting_order(service_items=service_items, include_exploded_items=0)
+ supplied_items1 = sorted([item.rm_item_code for item in sco2.supplied_items])
+ bom_items = sorted([item.item_code for item in bom.items])
+ self.assertEqual(supplied_items1, bom_items)
+
+ def test_backflush_based_on_stock_entry(self):
+ item_code = "_Test Subcontracted FG Item 1"
+ make_subcontracted_item(item_code=item_code)
+ make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1})
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+
+ order_qty = 5
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": order_qty,
+ "rate": 100,
+ "fg_item": item_code,
+ "fg_item_qty": order_qty,
+ },
+ ]
+
+ sco = get_subcontracting_order(service_items=service_items)
+
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="Sub Contracted Raw Material 1",
+ qty=10,
+ basic_rate=100,
+ )
+
+ rm_items = [
+ {
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 1",
+ "item_name": "_Test Item",
+ "qty": 10,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ },
+ {
+ "item_code": item_code,
+ "rm_item_code": "_Test Item Home Desktop 100",
+ "item_name": "_Test Item Home Desktop 100",
+ "qty": 20,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ },
+ {
+ "item_code": item_code,
+ "rm_item_code": "Test Extra Item 1",
+ "item_name": "Test Extra Item 1",
+ "qty": 10,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ },
+ {
+ "item_code": item_code,
+ "rm_item_code": "Test Extra Item 2",
+ "stock_uom": "Nos",
+ "qty": 10,
+ "warehouse": "_Test Warehouse - _TC",
+ "item_name": "Test Extra Item 2",
+ },
+ ]
+
+ ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
+ ste.submit()
+
+ scr = make_subcontracting_receipt(sco.name)
+ received_qty = 2
+
+ # partial receipt
+ scr.get("items")[0].qty = received_qty
+ scr.save()
+ scr.submit()
+
+ transferred_items = sorted(
+ [item.item_code for item in ste.get("items") if ste.subcontracting_order == sco.name]
+ )
+ issued_items = sorted([item.rm_item_code for item in scr.get("supplied_items")])
+
+ self.assertEqual(transferred_items, issued_items)
+ self.assertEqual(scr.get_supplied_items_cost(scr.get("items")[0].name), 2000)
+
+ transferred_rm_map = frappe._dict()
+ for item in rm_items:
+ transferred_rm_map[item.get("rm_item_code")] = item
+
+ set_backflush_based_on("BOM")
+
+ def test_supplied_qty(self):
+ item_code = "_Test Subcontracted FG Item 5"
+ make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1})
+
+ make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+
+ order_qty = 250
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": order_qty,
+ "rate": 100,
+ "fg_item": item_code,
+ "fg_item_qty": order_qty,
+ },
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": order_qty,
+ "rate": 100,
+ "fg_item": item_code,
+ "fg_item_qty": order_qty,
+ },
+ ]
+
+ sco = get_subcontracting_order(service_items=service_items)
+
+ # Material receipt entry for the raw materials which will be send to supplier
+ make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="Sub Contracted Raw Material 4",
+ qty=500,
+ basic_rate=100,
+ )
+
+ rm_items = [
+ {
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 4",
+ "item_name": "_Test Item",
+ "qty": 250,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ "name": sco.supplied_items[0].name,
+ },
+ {
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 4",
+ "item_name": "_Test Item",
+ "qty": 250,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ },
+ ]
+
+ # Raw Materials transfer entry from stores to supplier's warehouse
+ ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
+ ste.submit()
+
+ # Test sco_rm_detail field has value or not
+ for item_row in ste.items:
+ self.assertEqual(item_row.sco_rm_detail, sco.supplied_items[item_row.idx - 1].name)
+
+ sco.load_from_db()
+ for row in sco.supplied_items:
+ # Valid that whether transferred quantity is matching with supplied qty or not in the subcontracting order
+ self.assertEqual(row.supplied_qty, 250.0)
+
+ set_backflush_based_on("BOM")
+
+
+def create_subcontracting_order(**args):
+ args = frappe._dict(args)
+ sco = get_mapped_subcontracting_order(source_name=args.po_name)
+
+ for item in sco.items:
+ item.include_exploded_items = args.get("include_exploded_items", 1)
+
+ if args.get("warehouse"):
+ for item in sco.items:
+ item.warehouse = args.warehouse
+ else:
+ warehouse = frappe.get_value("Purchase Order", args.po_name, "set_warehouse")
+ if warehouse:
+ for item in sco.items:
+ item.warehouse = warehouse
+ else:
+ po = frappe.get_doc("Purchase Order", args.po_name)
+ warehouses = []
+ for item in po.items:
+ warehouses.append(item.warehouse)
+ else:
+ for idx, val in enumerate(sco.items):
+ val.warehouse = warehouses[idx]
+
+ if not args.do_not_save:
+ sco.insert()
+ if not args.do_not_submit:
+ sco.submit()
+
+ return sco
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
new file mode 100644
index 0000000..291f47a
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.json
@@ -0,0 +1,326 @@
+{
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2022-04-01 19:26:31.475015",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "item_name",
+ "bom",
+ "include_exploded_items",
+ "column_break_3",
+ "schedule_date",
+ "expected_delivery_date",
+ "description_section",
+ "description",
+ "column_break_8",
+ "image",
+ "image_view",
+ "quantity_and_rate_section",
+ "qty",
+ "received_qty",
+ "returned_qty",
+ "column_break_13",
+ "stock_uom",
+ "conversion_factor",
+ "section_break_16",
+ "rate",
+ "amount",
+ "column_break_19",
+ "rm_cost_per_qty",
+ "service_cost_per_qty",
+ "additional_cost_per_qty",
+ "warehouse_section",
+ "warehouse",
+ "accounting_details_section",
+ "expense_account",
+ "manufacture_section",
+ "manufacturer",
+ "manufacturer_part_no",
+ "section_break_34",
+ "page_break"
+ ],
+ "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": 2,
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "label": "Required By",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "bold": 1,
+ "fieldname": "expected_delivery_date",
+ "fieldtype": "Date",
+ "label": "Expected Delivery Date",
+ "search_index": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "description_section",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description",
+ "print_width": "300px",
+ "reqd": 1,
+ "width": "300px"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach",
+ "hidden": 1,
+ "label": "Image"
+ },
+ {
+ "fieldname": "image_view",
+ "fieldtype": "Image",
+ "label": "Image View",
+ "options": "image",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "quantity_and_rate_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity and Rate"
+ },
+ {
+ "bold": 1,
+ "columns": 1,
+ "default": "1",
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Quantity",
+ "print_width": "60px",
+ "read_only": 1,
+ "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
+ },
+ {
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break"
+ },
+ {
+ "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",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "currency",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "warehouse_section",
+ "fieldtype": "Section Break",
+ "label": "Warehouse Details"
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "options": "Warehouse",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "expense_account",
+ "fieldtype": "Link",
+ "label": "Expense Account",
+ "options": "Account",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "manufacture_section",
+ "fieldtype": "Section Break",
+ "label": "Manufacture"
+ },
+ {
+ "fieldname": "manufacturer",
+ "fieldtype": "Link",
+ "label": "Manufacturer",
+ "options": "Manufacturer"
+ },
+ {
+ "fieldname": "manufacturer_part_no",
+ "fieldtype": "Data",
+ "label": "Manufacturer Part Number"
+ },
+ {
+ "depends_on": "item_code",
+ "fetch_from": "item_code.default_bom",
+ "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
+ },
+ {
+ "fieldname": "service_cost_per_qty",
+ "fieldtype": "Currency",
+ "label": "Service Cost Per Qty",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "additional_cost_per_qty",
+ "fieldtype": "Currency",
+ "label": "Additional Cost Per Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "rm_cost_per_qty",
+ "fieldtype": "Currency",
+ "label": "Raw Material Cost Per Qty",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "page_break",
+ "fieldtype": "Check",
+ "label": "Page Break",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_34",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "received_qty",
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "label": "Received Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "returned_qty",
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "idx": 1,
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-11 21:28:06.585338",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Order Item",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "search_fields": "item_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
new file mode 100644
index 0000000..174f5b2
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_item/subcontracting_order_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SubcontractingOrderItem(Document):
+ pass
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json
new file mode 100644
index 0000000..f213313
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.json
@@ -0,0 +1,131 @@
+{
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2022-04-01 19:23:05.728354",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "column_break_2",
+ "item_name",
+ "section_break_4",
+ "qty",
+ "column_break_6",
+ "rate",
+ "column_break_8",
+ "amount",
+ "section_break_10",
+ "fg_item",
+ "column_break_12",
+ "fg_item_qty"
+ ],
+ "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": "column_break_8",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-07 11:43:43.094867",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Order Service Item",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "search_fields": "item_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py
new file mode 100644
index 0000000..ad6289d
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_service_item/subcontracting_order_service_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SubcontractingOrderServiceItem(Document):
+ pass
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json
new file mode 100644
index 0000000..a206a21
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.json
@@ -0,0 +1,178 @@
+{
+ "actions": [],
+ "creation": "2022-04-01 19:29:30.923800",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "main_item_code",
+ "rm_item_code",
+ "column_break_3",
+ "stock_uom",
+ "conversion_factor",
+ "reserve_warehouse",
+ "column_break_6",
+ "bom_detail_no",
+ "reference_name",
+ "section_break_9",
+ "rate",
+ "column_break_11",
+ "amount",
+ "section_break_13",
+ "required_qty",
+ "supplied_qty",
+ "column_break_16",
+ "consumed_qty",
+ "returned_qty",
+ "total_supplied_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
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock Uom",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Conversion Factor",
+ "read_only": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "reserve_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Reserve Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "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_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate",
+ "options": "Company:company:default_currency"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Required Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "supplied_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "label": "Consumed Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1,
+ "hidden": 1
+ },
+ {
+ "fieldname": "total_supplied_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Total Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "hide_toolbar": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-07 12:58:28.208847",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Order Supplied Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py
new file mode 100644
index 0000000..5619e3b
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order_supplied_item/subcontracting_order_supplied_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SubcontractingOrderSuppliedItem(Document):
+ pass
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
new file mode 100644
index 0000000..b2506cd
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -0,0 +1,157 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.provide('erpnext.buying');
+
+frappe.ui.form.on('Subcontracting Receipt', {
+ setup: (frm) => {
+ frm.get_field('supplied_items').grid.cannot_add_rows = true;
+ frm.get_field('supplied_items').grid.only_sortable();
+
+ frm.set_query('set_warehouse', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query('rejected_warehouse', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query('supplier_warehouse', () => {
+ return {
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ };
+ });
+
+ frm.set_query('warehouse', 'items', () => ({
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ }));
+
+ frm.set_query('rejected_warehouse', 'items', () => ({
+ filters: {
+ company: frm.doc.company,
+ is_group: 0
+ }
+ }));
+ },
+
+ refresh: (frm) => {
+ if (frm.doc.docstatus > 0) {
+ frm.add_custom_button(__("Stock Ledger"), function () {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route("query-report", "Stock Ledger");
+ }, __("View"));
+
+ frm.add_custom_button(__('Accounting Ledger'), function () {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ group_by: "Group by Voucher (Consolidated)",
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route("query-report", "General Ledger");
+ }, __("View"));
+ }
+
+ if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) {
+ frm.add_custom_button(__('Subcontract Return'), function () {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
+ frm: frm
+ });
+ }, __('Create'));
+ frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+
+ if (frm.doc.docstatus == 0) {
+ frm.add_custom_button(__('Subcontracting Order'), function () {
+ if (!frm.doc.supplier) {
+ frappe.throw({
+ title: __("Mandatory"),
+ message: __("Please Select a Supplier")
+ });
+ }
+
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
+ source_doctype: "Subcontracting Order",
+ target: frm,
+ setters: {
+ supplier: frm.doc.supplier,
+ },
+ get_query_filters: {
+ docstatus: 1,
+ per_received: ["<", 100],
+ company: frm.doc.company
+ }
+ });
+ }, __("Get Items From"));
+ }
+ },
+
+ set_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
+ },
+
+ rejected_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
+ },
+});
+
+frappe.ui.form.on('Subcontracting Receipt Item', {
+ item_code(frm) {
+ set_missing_values(frm);
+ },
+
+ qty(frm) {
+ set_missing_values(frm);
+ },
+
+ rate(frm) {
+ set_missing_values(frm);
+ },
+});
+
+frappe.ui.form.on('Subcontracting Receipt Supplied Item', {
+ consumed_qty(frm) {
+ set_missing_values(frm);
+ },
+});
+
+let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => {
+ let transaction_controller = new erpnext.TransactionController();
+ transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
+};
+
+let set_missing_values = (frm) => {
+ frappe.call({
+ doc: frm.doc,
+ method: 'set_missing_values',
+ callback: (r) => {
+ if (!r.exc) frm.refresh();
+ },
+ });
+};
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
new file mode 100644
index 0000000..e963814
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
@@ -0,0 +1,645 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2022-04-18 11:20:44.226738",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "naming_series",
+ "supplier",
+ "supplier_name",
+ "column_break1",
+ "company",
+ "posting_date",
+ "posting_time",
+ "is_return",
+ "return_against",
+ "section_addresses",
+ "supplier_address",
+ "contact_person",
+ "address_display",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "col_break_address",
+ "shipping_address",
+ "shipping_address_display",
+ "billing_address",
+ "billing_address_display",
+ "sec_warehouse",
+ "set_warehouse",
+ "rejected_warehouse",
+ "col_break_warehouse",
+ "supplier_warehouse",
+ "items_section",
+ "items",
+ "section_break0",
+ "total_qty",
+ "column_break_27",
+ "total",
+ "raw_material_details",
+ "get_current_stock",
+ "supplied_items",
+ "section_break_46",
+ "in_words",
+ "bill_no",
+ "bill_date",
+ "accounting_details_section",
+ "provisional_expense_account",
+ "more_info",
+ "status",
+ "column_break_39",
+ "per_returned",
+ "section_break_47",
+ "amended_from",
+ "range",
+ "column_break4",
+ "represents_company",
+ "subscription_detail",
+ "auto_repeat",
+ "printing_settings",
+ "letter_head",
+ "language",
+ "instructions",
+ "column_break_97",
+ "select_print_heading",
+ "other_details",
+ "remarks",
+ "transporter_info",
+ "transporter_name",
+ "column_break5",
+ "lr_no",
+ "lr_date"
+ ],
+ "fields": [
+ {
+ "allow_on_submit": 1,
+ "default": "{supplier_name}",
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "options": "MAT-SCR-.YYYY.-\nMAT-SCR-RET-.YYYY.-",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "label": "Supplier",
+ "options": "Supplier",
+ "print_hide": 1,
+ "print_width": "150px",
+ "reqd": 1,
+ "search_index": 1,
+ "width": "150px"
+ },
+ {
+ "bold": 1,
+ "depends_on": "supplier",
+ "fetch_from": "supplier.supplier_name",
+ "fieldname": "supplier_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "label": "Supplier Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break1",
+ "fieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "no_copy": 1,
+ "print_width": "100px",
+ "reqd": 1,
+ "search_index": 1,
+ "width": "100px"
+ },
+ {
+ "description": "Time at which materials were received",
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time",
+ "no_copy": 1,
+ "print_hide": 1,
+ "print_width": "100px",
+ "reqd": 1,
+ "width": "100px"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "print_hide": 1,
+ "print_width": "150px",
+ "remember_last_selected_value": 1,
+ "reqd": 1,
+ "width": "150px"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_addresses",
+ "fieldtype": "Section Break",
+ "label": "Address and Contact"
+ },
+ {
+ "fieldname": "supplier_address",
+ "fieldtype": "Link",
+ "label": "Select Supplier Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "contact_person",
+ "fieldtype": "Link",
+ "label": "Contact Person",
+ "options": "Contact",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "address_display",
+ "fieldtype": "Small Text",
+ "label": "Address",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_display",
+ "fieldtype": "Small Text",
+ "in_global_search": 1,
+ "label": "Contact",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_mobile",
+ "fieldtype": "Small Text",
+ "label": "Mobile No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "contact_email",
+ "fieldtype": "Small Text",
+ "label": "Contact Email",
+ "options": "Email",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "col_break_address",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "shipping_address",
+ "fieldtype": "Link",
+ "label": "Select Shipping Address",
+ "options": "Address",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "shipping_address_display",
+ "fieldtype": "Small Text",
+ "label": "Shipping Address",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "sec_warehouse",
+ "fieldtype": "Section Break"
+ },
+ {
+ "description": "Sets 'Accepted Warehouse' in each row of the Items table.",
+ "fieldname": "set_warehouse",
+ "fieldtype": "Link",
+ "label": "Accepted Warehouse",
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "description": "Sets 'Rejected Warehouse' in each row of the Items table.",
+ "fieldname": "rejected_warehouse",
+ "fieldtype": "Link",
+ "label": "Rejected Warehouse",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "col_break_warehouse",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "supplier_warehouse",
+ "fieldtype": "Link",
+ "label": "Supplier Warehouse",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1,
+ "print_width": "50px",
+ "width": "50px"
+ },
+ {
+ "fieldname": "items_section",
+ "fieldtype": "Section Break",
+ "options": "fa fa-shopping-cart"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "options": "Subcontracting Receipt Item",
+ "reqd": 1
+ },
+ {
+ "depends_on": "supplied_items",
+ "fieldname": "get_current_stock",
+ "fieldtype": "Button",
+ "label": "Get Current Stock",
+ "options": "get_current_stock",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "supplied_items",
+ "depends_on": "supplied_items",
+ "fieldname": "raw_material_details",
+ "fieldtype": "Section Break",
+ "label": "Raw Materials Consumed",
+ "options": "fa fa-table",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "supplied_items",
+ "fieldtype": "Table",
+ "label": "Consumed Items",
+ "no_copy": 1,
+ "options": "Subcontracting Receipt Supplied Item",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break0",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "total_qty",
+ "fieldtype": "Float",
+ "label": "Total Quantity",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "label": "Total",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_46",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "in_words",
+ "fieldtype": "Data",
+ "label": "In Words",
+ "length": 240,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "bill_no",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Bill No",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "bill_date",
+ "fieldtype": "Date",
+ "hidden": 1,
+ "label": "Bill Date",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "more_info",
+ "fieldtype": "Section Break",
+ "label": "More Information",
+ "options": "fa fa-file-text"
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled",
+ "print_hide": 1,
+ "print_width": "150px",
+ "read_only": 1,
+ "reqd": 1,
+ "search_index": 1,
+ "width": "150px"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "ignore_user_permissions": 1,
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Subcontracting Receipt",
+ "print_hide": 1,
+ "print_width": "150px",
+ "read_only": 1,
+ "width": "150px"
+ },
+ {
+ "fieldname": "range",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Range",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break4",
+ "fieldtype": "Column Break",
+ "print_hide": 1,
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "fieldname": "subscription_detail",
+ "fieldtype": "Section Break",
+ "label": "Auto Repeat Detail"
+ },
+ {
+ "fieldname": "auto_repeat",
+ "fieldtype": "Link",
+ "label": "Auto Repeat",
+ "no_copy": 1,
+ "options": "Auto Repeat",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "printing_settings",
+ "fieldtype": "Section Break",
+ "label": "Printing Settings"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "label": "Letter Head",
+ "options": "Letter Head",
+ "print_hide": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "select_print_heading",
+ "fieldtype": "Link",
+ "label": "Print Heading",
+ "no_copy": 1,
+ "options": "Print Heading",
+ "print_hide": 1,
+ "report_hide": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Data",
+ "label": "Print Language",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_97",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "other_details",
+ "fieldtype": "HTML",
+ "hidden": 1,
+ "label": "Other Details",
+ "options": "<div class=\"columnHeading\">Other Details</div>",
+ "print_hide": 1,
+ "print_width": "30%",
+ "width": "30%"
+ },
+ {
+ "fieldname": "instructions",
+ "fieldtype": "Small Text",
+ "label": "Instructions"
+ },
+ {
+ "fieldname": "remarks",
+ "fieldtype": "Small Text",
+ "label": "Remarks",
+ "print_hide": 1
+ },
+ {
+ "collapsible": 1,
+ "collapsible_depends_on": "transporter_name",
+ "fieldname": "transporter_info",
+ "fieldtype": "Section Break",
+ "label": "Transporter Details",
+ "options": "fa fa-truck"
+ },
+ {
+ "fieldname": "transporter_name",
+ "fieldtype": "Data",
+ "label": "Transporter Name"
+ },
+ {
+ "fieldname": "column_break5",
+ "fieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
+ },
+ {
+ "fieldname": "lr_no",
+ "fieldtype": "Data",
+ "label": "Vehicle Number",
+ "no_copy": 1,
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "fieldname": "lr_date",
+ "fieldtype": "Date",
+ "label": "Vehicle Date",
+ "no_copy": 1,
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "fieldname": "billing_address",
+ "fieldtype": "Link",
+ "label": "Select Billing Address",
+ "options": "Address"
+ },
+ {
+ "fieldname": "billing_address_display",
+ "fieldtype": "Small Text",
+ "label": "Billing Address",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "supplier.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "provisional_expense_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Provisional Expense Account",
+ "options": "Account"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_return",
+ "fieldtype": "Check",
+ "label": "Is Return",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "is_return",
+ "fieldname": "return_against",
+ "fieldtype": "Link",
+ "label": "Return Against Subcontracting Receipt",
+ "no_copy": 1,
+ "options": "Subcontracting Receipt",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_39",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:(!doc.__islocal && doc.is_return==0)",
+ "fieldname": "per_returned",
+ "fieldtype": "Percent",
+ "in_list_view": 1,
+ "label": "% Returned",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_47",
+ "fieldtype": "Section Break"
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-04-18 13:15:12.011682",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Receipt",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User"
+ },
+ {
+ "permlevel": 1,
+ "read": 1,
+ "role": "Stock Manager",
+ "write": 1
+ }
+ ],
+ "search_fields": "status, posting_date, supplier",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "timeline_field": "supplier",
+ "title_field": "title",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
new file mode 100644
index 0000000..5ee49d8
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -0,0 +1,199 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.utils import cint, flt, getdate, nowdate
+
+from erpnext.controllers.subcontracting_controller import SubcontractingController
+
+
+class SubcontractingReceipt(SubcontractingController):
+ def __init__(self, *args, **kwargs):
+ super(SubcontractingReceipt, self).__init__(*args, **kwargs)
+ self.status_updater = [
+ {
+ "target_dt": "Subcontracting Order Item",
+ "join_field": "subcontracting_order_item",
+ "target_field": "received_qty",
+ "target_parent_dt": "Subcontracting Order",
+ "target_parent_field": "per_received",
+ "target_ref_field": "qty",
+ "source_dt": "Subcontracting Receipt Item",
+ "source_field": "received_qty",
+ "percent_join_field": "subcontracting_order",
+ "overflow_type": "receipt",
+ },
+ ]
+
+ def update_status_updater_args(self):
+ if cint(self.is_return):
+ self.status_updater.extend(
+ [
+ {
+ "source_dt": "Subcontracting Receipt Item",
+ "target_dt": "Subcontracting Order Item",
+ "join_field": "subcontracting_order_item",
+ "target_field": "returned_qty",
+ "source_field": "-1 * qty",
+ "extra_cond": """ and exists (select name from `tabSubcontracting Receipt`
+ where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""",
+ },
+ {
+ "source_dt": "Subcontracting Receipt Item",
+ "target_dt": "Subcontracting Receipt Item",
+ "join_field": "subcontracting_receipt_item",
+ "target_field": "returned_qty",
+ "target_parent_dt": "Subcontracting Receipt",
+ "target_parent_field": "per_returned",
+ "target_ref_field": "received_qty",
+ "source_field": "-1 * received_qty",
+ "percent_join_field_parent": "return_against",
+ },
+ ]
+ )
+
+ def before_validate(self):
+ super(SubcontractingReceipt, self).before_validate()
+ self.set_items_cost_center()
+ self.set_items_expense_account()
+
+ def validate(self):
+ super(SubcontractingReceipt, self).validate()
+ self.set_missing_values()
+ self.validate_posting_time()
+ self.validate_rejected_warehouse()
+
+ if self._action == "submit":
+ self.make_batches("warehouse")
+
+ if getdate(self.posting_date) > getdate(nowdate()):
+ frappe.throw(_("Posting Date cannot be future date"))
+
+ self.reset_default_field_value("set_warehouse", "items", "warehouse")
+ self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
+ self.get_current_stock()
+
+ def on_submit(self):
+ self.update_status_updater_args()
+ self.update_prevdoc_status()
+ self.set_subcontracting_order_status()
+ self.set_consumed_qty_in_sco()
+ self.update_stock_ledger()
+
+ from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
+
+ update_serial_nos_after_submit(self, "items")
+
+ self.make_gl_entries()
+ self.repost_future_sle_and_gle()
+ self.update_status()
+
+ def on_cancel(self):
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
+ self.update_status_updater_args()
+ self.update_prevdoc_status()
+ self.update_stock_ledger()
+ self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
+ self.delete_auto_created_batches()
+ self.set_consumed_qty_in_sco()
+ self.set_subcontracting_order_status()
+ self.update_status()
+
+ @frappe.whitelist()
+ def set_missing_values(self):
+ self.set_missing_values_in_supplied_items()
+ self.set_missing_values_in_items()
+
+ def set_missing_values_in_supplied_items(self):
+ for item in self.get("supplied_items") or []:
+ item.amount = item.rate * item.consumed_qty
+
+ def set_missing_values_in_items(self):
+ rm_supp_cost = {}
+ for item in self.get("supplied_items") or []:
+ if item.reference_name in rm_supp_cost:
+ rm_supp_cost[item.reference_name] += item.amount
+ else:
+ rm_supp_cost[item.reference_name] = item.amount
+
+ total_qty = total_amount = 0
+ for item in self.items:
+ if item.name in rm_supp_cost:
+ item.rm_supp_cost = rm_supp_cost[item.name]
+ item.rm_cost_per_qty = item.rm_supp_cost / item.qty
+ rm_supp_cost.pop(item.name)
+
+ if self.is_new() and item.rm_supp_cost > 0:
+ item.rate = (
+ item.rm_cost_per_qty + (item.service_cost_per_qty or 0) + item.additional_cost_per_qty
+ )
+
+ item.received_qty = item.qty + (item.rejected_qty or 0)
+ item.amount = item.qty * item.rate
+ total_qty += item.qty
+ total_amount += item.amount
+ else:
+ self.total_qty = total_qty
+ self.total = total_amount
+
+ def validate_rejected_warehouse(self):
+ if not self.rejected_warehouse:
+ for item in self.items:
+ if item.rejected_qty:
+ frappe.throw(
+ _("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code)
+ )
+
+ def set_items_cost_center(self):
+ if self.company:
+ cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
+
+ for item in self.items:
+ if not item.cost_center:
+ item.cost_center = cost_center
+
+ def set_items_expense_account(self):
+ if self.company:
+ expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
+
+ for item in self.items:
+ if not item.expense_account:
+ item.expense_account = expense_account
+
+ @frappe.whitelist()
+ def get_current_stock(self):
+ for item in self.get("supplied_items"):
+ if self.supplier_warehouse:
+ actual_qty = frappe.db.get_value(
+ "Bin",
+ {"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse},
+ "actual_qty",
+ )
+ item.current_stock = flt(actual_qty) or 0
+
+ def update_status(self, status=None, update_modified=False):
+ if self.docstatus >= 1 and not status:
+ if self.docstatus == 1:
+ if self.is_return:
+ status = "Return"
+ return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
+ return_against.run_method("update_status")
+ else:
+ if self.per_returned == 100:
+ status = "Return Issued"
+ elif self.status == "Draft":
+ status = "Completed"
+ elif self.docstatus == 2:
+ status = "Cancelled"
+
+ if status:
+ frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
+
+
+@frappe.whitelist()
+def make_subcontract_return(source_name, target_doc=None):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ return make_return_doc("Subcontracting Receipt", source_name, target_doc)
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py
new file mode 100644
index 0000000..a9e5193
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_dashboard.py
@@ -0,0 +1,15 @@
+from frappe import _
+
+
+def get_data():
+ return {
+ "fieldname": "subcontracting_receipt_no",
+ "internal_links": {
+ "Subcontracting Order": ["items", "subcontracting_order"],
+ "Project": ["items", "project"],
+ "Quality Inspection": ["items", "quality_inspection"],
+ },
+ "transactions": [
+ {"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]},
+ ],
+ }
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js
new file mode 100644
index 0000000..14a4e4a
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt_list.js
@@ -0,0 +1,14 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.listview_settings['Subcontracting Receipt'] = {
+ get_indicator: function (doc) {
+ const status_colors = {
+ "Draft": "grey",
+ "Return": "gray",
+ "Return Issued": "grey",
+ "Completed": "green",
+ };
+ return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
+ },
+};
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
new file mode 100644
index 0000000..156a271
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -0,0 +1,374 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+
+import copy
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import flt
+
+from erpnext.controllers.sales_and_purchase_return import make_return_doc
+from erpnext.controllers.tests.test_subcontracting_controller import (
+ get_rm_items,
+ get_subcontracting_order,
+ make_bom_for_subcontracted_items,
+ make_raw_materials,
+ make_service_items,
+ make_stock_in_entry,
+ make_stock_transfer_entry,
+ make_subcontracted_item,
+ make_subcontracted_items,
+ set_backflush_based_on,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_subcontracting_receipt,
+)
+
+
+class TestSubcontractingReceipt(FrappeTestCase):
+ def setUp(self):
+ make_subcontracted_items()
+ make_raw_materials()
+ make_service_items()
+ make_bom_for_subcontracted_items()
+
+ def test_subcontracting(self):
+ set_backflush_based_on("BOM")
+ make_stock_entry(
+ item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
+ )
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100",
+ qty=100,
+ target="_Test Warehouse 1 - _TC",
+ basic_rate=100,
+ )
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": "_Test FG Item",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ scr.submit()
+ rm_supp_cost = sum(item.amount for item in scr.get("supplied_items"))
+ self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
+
+ def test_subcontracting_gle_fg_item_rate_zero(self):
+ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
+
+ set_backflush_based_on("BOM")
+ make_stock_entry(
+ item_code="_Test Item",
+ target="Work In Progress - TCP1",
+ qty=100,
+ basic_rate=100,
+ company="_Test Company with perpetual inventory",
+ )
+ make_stock_entry(
+ item_code="_Test Item Home Desktop 100",
+ target="Work In Progress - TCP1",
+ qty=100,
+ basic_rate=100,
+ company="_Test Company with perpetual inventory",
+ )
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 0,
+ "fg_item": "_Test FG Item",
+ "fg_item_qty": 10,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ scr.submit()
+
+ gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
+ self.assertFalse(gl_entries)
+
+ def test_subcontracting_over_receipt(self):
+ """
+ Behaviour: Raise multiple SCRs against one SCO that in total
+ receive more than the required qty in the SCO.
+ Expected Result: Error Raised for Over Receipt against SCO.
+ """
+ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_rm_stock_entry as make_subcontract_transfer_entry,
+ )
+ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_subcontracting_receipt,
+ )
+ from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import (
+ make_subcontracted_item,
+ )
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ item_code = "_Test Subcontracted FG Item 1"
+ make_subcontracted_item(item_code=item_code)
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 1,
+ "rate": 100,
+ "fg_item": "_Test Subcontracted FG Item 1",
+ "fg_item_qty": 1,
+ },
+ ]
+ sco = get_subcontracting_order(
+ service_items=service_items,
+ include_exploded_items=0,
+ )
+
+ # stock raw materials in a warehouse before transfer
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100
+ )
+ make_stock_entry(
+ target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100
+ )
+
+ rm_items = [
+ {
+ "item_code": item_code,
+ "rm_item_code": sco.supplied_items[0].rm_item_code,
+ "item_name": "_Test FG Item",
+ "qty": sco.supplied_items[0].required_qty,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ },
+ {
+ "item_code": item_code,
+ "rm_item_code": sco.supplied_items[1].rm_item_code,
+ "item_name": "Test Extra Item 1",
+ "qty": sco.supplied_items[1].required_qty,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ },
+ ]
+ ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items))
+ ste.to_warehouse = "_Test Warehouse 1 - _TC"
+ ste.save()
+ ste.submit()
+
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr2 = make_subcontracting_receipt(sco.name)
+
+ scr1.submit()
+ self.assertRaises(frappe.ValidationError, scr2.submit)
+
+ def test_subcontracted_scr_for_multi_transfer_batches(self):
+ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ make_rm_stock_entry,
+ make_subcontracting_receipt,
+ )
+
+ set_backflush_based_on("Material Transferred for Subcontract")
+ item_code = "_Test Subcontracted FG Item 3"
+
+ make_item(
+ "Sub Contracted Raw Material 3",
+ {"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1},
+ )
+
+ make_subcontracted_item(
+ item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"]
+ )
+
+ order_qty = 500
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 3",
+ "qty": order_qty,
+ "rate": 100,
+ "fg_item": "_Test Subcontracted FG Item 3",
+ "fg_item_qty": order_qty,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+
+ ste1 = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="Sub Contracted Raw Material 3",
+ qty=300,
+ basic_rate=100,
+ )
+ ste2 = make_stock_entry(
+ target="_Test Warehouse - _TC",
+ item_code="Sub Contracted Raw Material 3",
+ qty=200,
+ basic_rate=100,
+ )
+
+ transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200}
+
+ rm_items = [
+ {
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 3",
+ "item_name": "_Test Item",
+ "qty": 300,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ "name": sco.supplied_items[0].name,
+ },
+ {
+ "item_code": item_code,
+ "rm_item_code": "Sub Contracted Raw Material 3",
+ "item_name": "_Test Item",
+ "qty": 200,
+ "warehouse": "_Test Warehouse - _TC",
+ "stock_uom": "Nos",
+ "name": sco.supplied_items[0].name,
+ },
+ ]
+
+ se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
+ self.assertEqual(len(se.items), 2)
+ se.items[0].batch_no = ste1.items[0].batch_no
+ se.items[1].batch_no = ste2.items[0].batch_no
+ se.submit()
+
+ supplied_qty = frappe.db.get_value(
+ "Subcontracting Order Supplied Item",
+ {"parent": sco.name, "rm_item_code": "Sub Contracted Raw Material 3"},
+ "supplied_qty",
+ )
+
+ self.assertEqual(supplied_qty, 500.00)
+
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ self.assertEqual(len(scr.supplied_items), 2)
+
+ for row in scr.supplied_items:
+ self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty)
+
+ def test_subcontracting_order_partial_return(self):
+ sco = get_subcontracting_order()
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+ scr1.submit()
+
+ scr1_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-3)
+ scr1.load_from_db()
+ self.assertEqual(scr1_return.status, "Return")
+ self.assertEqual(scr1.items[0].returned_qty, 3)
+
+ scr2_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-7)
+ scr1.load_from_db()
+ self.assertEqual(scr2_return.status, "Return")
+ self.assertEqual(scr1.status, "Return Issued")
+ self.assertEqual(scr1.items[0].returned_qty, 10)
+
+ def test_subcontracting_order_over_return(self):
+ sco = get_subcontracting_order()
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+ scr1.submit()
+
+ from erpnext.controllers.status_updater import OverAllowanceError
+
+ args = frappe._dict(scr_name=scr1.name, qty=-15)
+ self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args)
+
+
+def make_return_subcontracting_receipt(**args):
+ args = frappe._dict(args)
+ return_doc = make_return_doc("Subcontracting Receipt", args.scr_name)
+ return_doc.supplier_warehouse = (
+ args.supplier_warehouse or args.warehouse or "_Test Warehouse 1 - _TC"
+ )
+
+ if args.qty:
+ for item in return_doc.items:
+ item.qty = args.qty
+
+ if not args.do_not_save:
+ return_doc.save()
+ if not args.do_not_submit:
+ return_doc.submit()
+
+ return_doc.load_from_db()
+ return return_doc
+
+
+def get_items(**args):
+ args = frappe._dict(args)
+ return [
+ {
+ "conversion_factor": 1.0,
+ "description": "_Test Item",
+ "doctype": "Subcontracting Receipt Item",
+ "item_code": "_Test Item",
+ "item_name": "_Test Item",
+ "parentfield": "items",
+ "qty": 5.0,
+ "rate": 50.0,
+ "received_qty": 5.0,
+ "rejected_qty": 0.0,
+ "stock_uom": "_Test UOM",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "cost_center": args.cost_center or "Main - _TC",
+ },
+ {
+ "conversion_factor": 1.0,
+ "description": "_Test Item Home Desktop 100",
+ "doctype": "Subcontracting Receipt Item",
+ "item_code": "_Test Item Home Desktop 100",
+ "item_name": "_Test Item Home Desktop 100",
+ "parentfield": "items",
+ "qty": 5.0,
+ "rate": 50.0,
+ "received_qty": 5.0,
+ "rejected_qty": 0.0,
+ "stock_uom": "_Test UOM",
+ "warehouse": args.warehouse or "_Test Warehouse 1 - _TC",
+ "cost_center": args.cost_center or "Main - _TC",
+ },
+ ]
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
new file mode 100644
index 0000000..e2785ce
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
@@ -0,0 +1,475 @@
+{
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2022-04-13 16:05:55.395695",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "column_break_2",
+ "item_name",
+ "section_break_4",
+ "description",
+ "brand",
+ "image_column",
+ "image",
+ "image_view",
+ "received_and_accepted",
+ "received_qty",
+ "qty",
+ "rejected_qty",
+ "returned_qty",
+ "col_break2",
+ "stock_uom",
+ "conversion_factor",
+ "tracking_section",
+ "col_break_tracking_section",
+ "rate_and_amount",
+ "rate",
+ "amount",
+ "column_break_19",
+ "rm_cost_per_qty",
+ "service_cost_per_qty",
+ "additional_cost_per_qty",
+ "rm_supp_cost",
+ "warehouse_and_reference",
+ "warehouse",
+ "rejected_warehouse",
+ "subcontracting_order",
+ "column_break_40",
+ "schedule_date",
+ "quality_inspection",
+ "subcontracting_order_item",
+ "subcontracting_receipt_item",
+ "section_break_45",
+ "bom",
+ "serial_no",
+ "col_break5",
+ "batch_no",
+ "rejected_serial_no",
+ "expense_account",
+ "manufacture_details",
+ "manufacturer",
+ "column_break_16",
+ "manufacturer_part_no",
+ "accounting_dimensions_section",
+ "project",
+ "dimension_col_break",
+ "cost_center",
+ "section_break_80",
+ "page_break"
+ ],
+ "fields": [
+ {
+ "bold": 1,
+ "columns": 3,
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "print_width": "100px",
+ "reqd": 1,
+ "search_index": 1,
+ "width": "100px"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "label": "Item Name",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description",
+ "print_width": "300px",
+ "reqd": 1,
+ "width": "300px"
+ },
+ {
+ "fieldname": "image",
+ "fieldtype": "Attach",
+ "hidden": 1,
+ "label": "Image"
+ },
+ {
+ "fieldname": "image_view",
+ "fieldtype": "Image",
+ "label": "Image View",
+ "options": "image",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "received_and_accepted",
+ "fieldtype": "Section Break",
+ "label": "Received and Accepted"
+ },
+ {
+ "bold": 1,
+ "default": "0",
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "label": "Received Quantity",
+ "no_copy": 1,
+ "print_hide": 1,
+ "print_width": "100px",
+ "read_only": 1,
+ "reqd": 1,
+ "width": "100px"
+ },
+ {
+ "columns": 2,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Accepted Quantity",
+ "no_copy": 1,
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "columns": 1,
+ "fieldname": "rejected_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Rejected Quantity",
+ "no_copy": 1,
+ "print_hide": 1,
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "fieldname": "col_break2",
+ "fieldtype": "Column Break",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "print_hide": 1,
+ "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
+ },
+ {
+ "fieldname": "rate_and_amount",
+ "fieldtype": "Section Break",
+ "label": "Rate and Amount"
+ },
+ {
+ "bold": 1,
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate",
+ "options": "currency",
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_19",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "rm_cost_per_qty",
+ "fieldtype": "Currency",
+ "label": "Raw Material Cost Per Qty",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "service_cost_per_qty",
+ "fieldtype": "Currency",
+ "label": "Service Cost Per Qty",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "additional_cost_per_qty",
+ "fieldtype": "Currency",
+ "label": "Additional Cost Per Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "warehouse_and_reference",
+ "fieldtype": "Section Break",
+ "label": "Warehouse and Reference"
+ },
+ {
+ "bold": 1,
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Accepted Warehouse",
+ "options": "Warehouse",
+ "print_hide": 1,
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "fieldname": "rejected_warehouse",
+ "fieldtype": "Link",
+ "label": "Rejected Warehouse",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1,
+ "print_width": "100px",
+ "width": "100px"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "quality_inspection",
+ "fieldtype": "Link",
+ "label": "Quality Inspection",
+ "no_copy": 1,
+ "options": "Quality Inspection",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_40",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "subcontracting_order",
+ "fieldtype": "Link",
+ "label": "Subcontracting Order",
+ "no_copy": 1,
+ "options": "Subcontracting Order",
+ "print_width": "150px",
+ "read_only": 1,
+ "search_index": 1,
+ "width": "150px"
+ },
+ {
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "label": "Required By",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_45",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "eval:!doc.is_fixed_asset",
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Serial No",
+ "no_copy": 1
+ },
+ {
+ "depends_on": "eval:!doc.is_fixed_asset",
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Batch No",
+ "no_copy": 1,
+ "options": "Batch",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:!doc.is_fixed_asset",
+ "fieldname": "rejected_serial_no",
+ "fieldtype": "Small Text",
+ "label": "Rejected Serial No",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "fieldname": "subcontracting_order_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Subcontracting Order Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "print_width": "150px",
+ "read_only": 1,
+ "search_index": 1,
+ "width": "150px"
+ },
+ {
+ "fieldname": "col_break5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "bom",
+ "fieldtype": "Link",
+ "label": "BOM",
+ "no_copy": 1,
+ "options": "BOM",
+ "print_hide": 1
+ },
+ {
+ "fetch_from": "item_code.brand",
+ "fieldname": "brand",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Brand",
+ "options": "Brand",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "rm_supp_cost",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Raw Materials Supplied Cost",
+ "no_copy": 1,
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "print_width": "150px",
+ "read_only": 1,
+ "width": "150px"
+ },
+ {
+ "fieldname": "expense_account",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Expense Account",
+ "options": "Account",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "manufacture_details",
+ "fieldtype": "Section Break",
+ "label": "Manufacture"
+ },
+ {
+ "fieldname": "manufacturer",
+ "fieldtype": "Link",
+ "label": "Manufacturer",
+ "options": "Manufacturer"
+ },
+ {
+ "fieldname": "column_break_16",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "manufacturer_part_no",
+ "fieldtype": "Data",
+ "label": "Manufacturer Part Number"
+ },
+ {
+ "fieldname": "subcontracting_receipt_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Subcontracting Receipt Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "image_column",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "tracking_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "col_break_tracking_section",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "dimension_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": ":Company",
+ "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_80",
+ "fieldtype": "Section Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "page_break",
+ "fieldtype": "Check",
+ "label": "Page Break",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "returned_qty",
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-21 12:07:55.899701",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Receipt Item",
+ "naming_rule": "Random",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
new file mode 100644
index 0000000..374f95b
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SubcontractingReceiptItem(Document):
+ pass
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/__init__.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/__init__.py
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
new file mode 100644
index 0000000..100a806
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.json
@@ -0,0 +1,198 @@
+{
+ "actions": [],
+ "creation": "2022-04-18 10:45:16.538479",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "main_item_code",
+ "rm_item_code",
+ "item_name",
+ "bom_detail_no",
+ "col_break1",
+ "description",
+ "stock_uom",
+ "conversion_factor",
+ "reference_name",
+ "secbreak_1",
+ "rate",
+ "col_break2",
+ "amount",
+ "secbreak_2",
+ "required_qty",
+ "col_break3",
+ "consumed_qty",
+ "current_stock",
+ "secbreak_3",
+ "batch_no",
+ "col_break4",
+ "serial_no",
+ "subcontracting_order"
+ ],
+ "fields": [
+ {
+ "fieldname": "main_item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "rm_item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Raw Material Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "in_global_search": 1,
+ "label": "Description",
+ "print_width": "300px",
+ "read_only": 1,
+ "width": "300px"
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "no_copy": 1,
+ "options": "Batch"
+ },
+ {
+ "fieldname": "serial_no",
+ "fieldtype": "Text",
+ "label": "Serial No",
+ "no_copy": 1
+ },
+ {
+ "fieldname": "col_break1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Available Qty For Consumption",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "columns": 2,
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Qty to be Consumed",
+ "reqd": 1
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock Uom",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "label": "Rate",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Conversion Factor",
+ "read_only": 1
+ },
+ {
+ "fieldname": "current_stock",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Current Stock",
+ "read_only": 1
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "bom_detail_no",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "BOM Detail No",
+ "read_only": 1
+ },
+ {
+ "fieldname": "secbreak_1",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "col_break2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "secbreak_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "col_break3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "secbreak_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "col_break4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "subcontracting_order",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Subcontracting Order",
+ "no_copy": 1,
+ "options": "Subcontracting Order",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-04-18 10:45:16.538479",
+ "modified_by": "Administrator",
+ "module": "Subcontracting",
+ "name": "Subcontracting Receipt Supplied Item",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
new file mode 100644
index 0000000..f4d2805
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_supplied_item/subcontracting_receipt_supplied_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class SubcontractingReceiptSuppliedItem(Document):
+ pass
diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py
deleted file mode 100644
index bf12181..0000000
--- a/erpnext/tests/test_subcontracting.py
+++ /dev/null
@@ -1,1065 +0,0 @@
-import copy
-import unittest
-from collections import defaultdict
-
-import frappe
-from frappe.utils import cint
-
-from erpnext.buying.doctype.purchase_order.purchase_order import (
- get_materials_from_supplier,
- make_purchase_invoice,
- make_purchase_receipt,
- make_rm_stock_entry,
-)
-from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
-from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
-from erpnext.stock.doctype.item.test_item import make_item
-from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
-from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
-
-
-class TestSubcontracting(unittest.TestCase):
- def setUp(self):
- make_subcontract_items()
- make_raw_materials()
- make_bom_for_subcontracted_items()
-
- def test_po_with_bom(self):
- """
- - Set backflush based on BOM
- - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times.
- - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- - Create purchase receipt against the PO and check serial nos and batch no.
- """
-
- set_backflush_based_on("BOM")
- item_code = "Subcontracted Item SA1"
- items = [
- {"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 5, "rate": 100},
- {"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 6, "rate": 100},
- ]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 1", "qty": 5},
- {"item_code": "Subcontracted SRM Item 2", "qty": 5},
- {"item_code": "Subcontracted SRM Item 3", "qty": 5},
- {"item_code": "Subcontracted SRM Item 1", "qty": 6},
- {"item_code": "Subcontracted SRM Item 2", "qty": 6},
- {"item_code": "Subcontracted SRM Item 3", "qty": 6},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- transferred_detais = itemwise_details.get(key)
-
- for field in ["qty", "serial_no", "batch_no"]:
- if value.get(field):
- transfer, consumed = (transferred_detais.get(field), value.get(field))
- if field == "serial_no":
- transfer, consumed = (sorted(transfer), sorted(consumed))
-
- self.assertEqual(transfer, consumed)
-
- def test_po_with_material_transfer(self):
- """
- - Set backflush based on Material Transfer
- - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5.
- - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5.
- - Create partial purchase receipt against the PO and check serial nos and batch no.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- items = [
- {
- "warehouse": "_Test Warehouse - _TC",
- "item_code": "Subcontracted Item SA1",
- "qty": 5,
- "rate": 100,
- },
- {
- "warehouse": "_Test Warehouse - _TC",
- "item_code": "Subcontracted Item SA5",
- "qty": 6,
- "rate": 100,
- },
- ]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 1", "qty": 5, "main_item_code": "Subcontracted Item SA1"},
- {"item_code": "Subcontracted SRM Item 2", "qty": 5, "main_item_code": "Subcontracted Item SA1"},
- {"item_code": "Subcontracted SRM Item 3", "qty": 5, "main_item_code": "Subcontracted Item SA1"},
- {"item_code": "Subcontracted SRM Item 5", "qty": 6, "main_item_code": "Subcontracted Item SA5"},
- {"item_code": "Subcontracted SRM Item 4", "qty": 6, "main_item_code": "Subcontracted Item SA5"},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name
-
- make_stock_transfer_entry(
- po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.remove(pr1.items[1])
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- transferred_detais = itemwise_details.get(key)
-
- for field in ["qty", "serial_no", "batch_no"]:
- if value.get(field):
- self.assertEqual(value.get(field), transferred_detais.get(field))
-
- pr2 = make_purchase_receipt(po.name)
- pr2.submit()
-
- for key, value in get_supplied_items(pr2).items():
- transferred_detais = itemwise_details.get(key)
-
- for field in ["qty", "serial_no", "batch_no"]:
- if value.get(field):
- self.assertEqual(value.get(field), transferred_detais.get(field))
-
- def test_subcontract_with_same_components_different_fg(self):
- """
- - Set backflush based on Material Transfer
- - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3.
- - Transfer the components from Stores to Supplier warehouse with serial nos.
- - Transfer extra qty of components for the item Subcontracted Item SA2.
- - Create partial purchase receipt against the PO and check serial nos and batch no.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- items = [
- {
- "warehouse": "_Test Warehouse - _TC",
- "item_code": "Subcontracted Item SA2",
- "qty": 5,
- "rate": 100,
- },
- {
- "warehouse": "_Test Warehouse - _TC",
- "item_code": "Subcontracted Item SA3",
- "qty": 6,
- "rate": 100,
- },
- ]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2"},
- {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA3"},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name if d.get("qty") == 5 else po.items[1].name
-
- make_stock_transfer_entry(
- po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 3
- pr1.remove(pr1.items[1])
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- transferred_detais = itemwise_details.get(key)
- self.assertEqual(value.qty, 4)
- self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4]))
-
- pr2 = make_purchase_receipt(po.name)
- pr2.items[0].qty = 2
- pr2.remove(pr2.items[1])
- pr2.submit()
-
- for key, value in get_supplied_items(pr2).items():
- transferred_detais = itemwise_details.get(key)
-
- self.assertEqual(value.qty, 2)
- self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6]))
-
- pr3 = make_purchase_receipt(po.name)
- pr3.submit()
- for key, value in get_supplied_items(pr3).items():
- transferred_detais = itemwise_details.get(key)
-
- self.assertEqual(value.qty, 6)
- self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12]))
-
- def test_return_non_consumed_materials(self):
- """
- - Set backflush based on Material Transfer
- - Create subcontracted PO for the item Subcontracted Item SA2.
- - Transfer the components from Stores to Supplier warehouse with serial nos.
- - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
- - Create purchase receipt for full qty against the PO and change the qty of raw material.
- - After that return the non consumed material back to the store from supplier's warehouse.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- items = [
- {
- "warehouse": "_Test Warehouse - _TC",
- "item_code": "Subcontracted Item SA2",
- "qty": 5,
- "rate": 100,
- }
- ]
- rm_items = [
- {"item_code": "Subcontracted SRM Item 2", "qty": 6, "main_item_code": "Subcontracted Item SA2"}
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.save()
- pr1.supplied_items[0].consumed_qty = 5
- pr1.supplied_items[0].serial_no = "\n".join(
- sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5])
- )
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- transferred_detais = itemwise_details.get(key)
- self.assertEqual(value.qty, 5)
- self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5]))
-
- po.load_from_db()
- self.assertEqual(po.supplied_items[0].consumed_qty, 5)
- doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items])
- self.assertEqual(doc.items[0].qty, 1)
- self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
- self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
- self.assertEqual(
- get_serial_nos(doc.items[0].serial_no),
- itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6],
- )
-
- def test_item_with_batch_based_on_bom(self):
- """
- - Set backflush based on BOM
- - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- - Transfer the components in multiple batches.
- - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- """
-
- set_backflush_based_on("BOM")
- item_code = "Subcontracted Item SA4"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 1", "qty": 10},
- {"item_code": "Subcontracted SRM Item 2", "qty": 10},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 1},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 2
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 4)
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 2
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 4)
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 2
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 2)
-
- def test_item_with_batch_based_on_material_transfer(self):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- - Transfer the components in multiple batches with extra 2 qty for the batched item.
- - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- - In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA4"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 1", "qty": 10},
- {"item_code": "Subcontracted SRM Item 2", "qty": 10},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 2
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- qty = 4 if key != "Subcontracted SRM Item 3" else 6
- self.assertEqual(value.qty, qty)
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 2
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 4)
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 2
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 2)
-
- def test_partial_transfer_serial_no_components_based_on_material_transfer(self):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA2.
- - Transfer the partial components from Stores to Supplier warehouse with serial nos.
- - Create partial purchase receipt against the PO and change the qty manually.
- - Transfer the remaining components from Stores to Supplier warehouse with serial nos.
- - Create purchase receipt for remaining qty against the PO and change the qty manually.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA2"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 5}]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 5
- pr1.save()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, 3)
- self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
-
- pr1.load_from_db()
- pr1.supplied_items[0].consumed_qty = 5
- pr1.supplied_items[0].serial_no = "\n".join(
- itemwise_details[pr1.supplied_items[0].rm_item_code]["serial_no"]
- )
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
-
- def test_incorrect_serial_no_components_based_on_material_transfer(self):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA2.
- - Transfer the serialized componenets to the supplier.
- - Create purchase receipt and change the serial no which is not transferred.
- - System should throw the error and not allowed to save the purchase receipt.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA2"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 10}]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.save()
- pr1.supplied_items[0].serial_no = "ABCD"
- self.assertRaises(frappe.ValidationError, pr1.save)
- pr1.delete()
-
- def test_partial_transfer_batch_based_on_material_transfer(self):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA6.
- - Transfer the partial components from Stores to Supplier warehouse with batch.
- - Create partial purchase receipt against the PO and change the qty manually.
- - Transfer the remaining components from Stores to Supplier warehouse with batch.
- - Create purchase receipt for remaining qty against the PO and change the qty manually.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA6"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [{"item_code": "Subcontracted SRM Item 3", "qty": 5}]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.items[0].qty = 5
- pr1.save()
-
- transferred_batch_no = ""
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, 3)
- transferred_batch_no = details.batch_no
- self.assertEqual(value.batch_no, details.batch_no)
-
- pr1.load_from_db()
- pr1.supplied_items[0].consumed_qty = 5
- pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(value.batch_no, details.batch_no)
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_receipt(po.name)
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(value.batch_no, details.batch_no)
-
- def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- - Transfer the components in multiple batches with extra 2 qty for the batched item.
- - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- - In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA4"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 1", "qty": 10},
- {"item_code": "Subcontracted SRM Item 2", "qty": 10},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 2
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- qty = 4 if key != "Subcontracted SRM Item 3" else 6
- self.assertEqual(value.qty, qty)
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.items[0].qty = 2
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 4)
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 2
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 2)
-
- def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(
- self,
- ):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA2.
- - Transfer the partial components from Stores to Supplier warehouse with serial nos.
- - Create partial purchase receipt against the PO and change the qty manually.
- - Transfer the remaining components from Stores to Supplier warehouse with serial nos.
- - Create purchase receipt for remaining qty against the PO and change the qty manually.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA2"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [{"item_code": "Subcontracted SRM Item 2", "qty": 5}]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 5
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.save()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, 3)
- self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
-
- pr1.load_from_db()
- pr1.supplied_items[0].consumed_qty = 5
- pr1.supplied_items[0].serial_no = "\n".join(
- itemwise_details[pr1.supplied_items[0].rm_item_code]["serial_no"]
- )
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
-
- def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self):
- """
- - Set backflush based on Material Transferred for Subcontract
- - Create subcontracted PO for the item Subcontracted Item SA6.
- - Transfer the partial components from Stores to Supplier warehouse with batch.
- - Create partial purchase receipt against the PO and change the qty manually.
- - Transfer the remaining components from Stores to Supplier warehouse with batch.
- - Create purchase receipt for remaining qty against the PO and change the qty manually.
- """
-
- set_backflush_based_on("Material Transferred for Subcontract")
- item_code = "Subcontracted Item SA6"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [{"item_code": "Subcontracted SRM Item 3", "qty": 5}]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 5
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.save()
-
- transferred_batch_no = ""
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, 3)
- transferred_batch_no = details.batch_no
- self.assertEqual(value.batch_no, details.batch_no)
-
- pr1.load_from_db()
- pr1.supplied_items[0].consumed_qty = 5
- pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(value.batch_no, details.batch_no)
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- details = itemwise_details.get(key)
- self.assertEqual(value.qty, details.qty)
- self.assertEqual(value.batch_no, details.batch_no)
-
- def test_item_with_batch_based_on_bom_for_purchase_invoice(self):
- """
- - Set backflush based on BOM
- - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- - Transfer the components in multiple batches.
- - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- """
-
- set_backflush_based_on("BOM")
- item_code = "Subcontracted Item SA4"
- items = [{"warehouse": "_Test Warehouse - _TC", "item_code": item_code, "qty": 10, "rate": 100}]
-
- rm_items = [
- {"item_code": "Subcontracted SRM Item 1", "qty": 10},
- {"item_code": "Subcontracted SRM Item 2", "qty": 10},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 3},
- {"item_code": "Subcontracted SRM Item 3", "qty": 1},
- ]
-
- itemwise_details = make_stock_in_entry(rm_items=rm_items)
- po = create_purchase_order(
- rm_items=items, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
- )
-
- for d in rm_items:
- d["po_detail"] = po.items[0].name
-
- make_stock_transfer_entry(
- po_no=po.name,
- main_item_code=item_code,
- rm_items=rm_items,
- itemwise_details=copy.deepcopy(itemwise_details),
- )
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 2
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 4)
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 2
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- add_second_row_in_pr(pr1)
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 4)
-
- pr1 = make_purchase_invoice(po.name)
- pr1.update_stock = 1
- pr1.items[0].qty = 2
- pr1.items[0].expense_account = "Stock Adjustment - _TC"
- pr1.save()
- pr1.submit()
-
- for key, value in get_supplied_items(pr1).items():
- self.assertEqual(value.qty, 2)
-
-
-def add_second_row_in_pr(pr):
- item_dict = {}
- for column in [
- "item_code",
- "item_name",
- "qty",
- "uom",
- "warehouse",
- "stock_uom",
- "purchase_order",
- "purchase_order_item",
- "conversion_factor",
- "rate",
- "expense_account",
- "po_detail",
- ]:
- item_dict[column] = pr.items[0].get(column)
-
- pr.append("items", item_dict)
- pr.set_missing_values()
-
-
-def get_supplied_items(pr_doc):
- supplied_items = {}
- for row in pr_doc.get("supplied_items"):
- if row.rm_item_code not in supplied_items:
- supplied_items.setdefault(
- row.rm_item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)})
- )
-
- details = supplied_items[row.rm_item_code]
- update_item_details(row, details)
-
- return supplied_items
-
-
-def make_stock_in_entry(**args):
- args = frappe._dict(args)
-
- items = {}
- for row in args.rm_items:
- row = frappe._dict(row)
-
- doc = make_stock_entry(
- target=row.warehouse or "_Test Warehouse - _TC",
- item_code=row.item_code,
- qty=row.qty or 1,
- basic_rate=row.rate or 100,
- )
-
- if row.item_code not in items:
- items.setdefault(
- row.item_code, frappe._dict({"qty": 0, "serial_no": [], "batch_no": defaultdict(float)})
- )
-
- child_row = doc.items[0]
- details = items[child_row.item_code]
- update_item_details(child_row, details)
-
- return items
-
-
-def update_item_details(child_row, details):
- details.qty += (
- child_row.get("qty")
- if child_row.doctype == "Stock Entry Detail"
- else child_row.get("consumed_qty")
- )
-
- if child_row.serial_no:
- details.serial_no.extend(get_serial_nos(child_row.serial_no))
-
- if child_row.batch_no:
- details.batch_no[child_row.batch_no] += child_row.get("qty") or child_row.get("consumed_qty")
-
-
-def make_stock_transfer_entry(**args):
- args = frappe._dict(args)
-
- items = []
- for row in args.rm_items:
- row = frappe._dict(row)
-
- item = {
- "item_code": row.main_item_code or args.main_item_code,
- "rm_item_code": row.item_code,
- "qty": row.qty or 1,
- "item_name": row.item_code,
- "rate": row.rate or 100,
- "stock_uom": row.stock_uom or "Nos",
- "warehouse": row.warehuose or "_Test Warehouse - _TC",
- }
-
- item_details = args.itemwise_details.get(row.item_code)
-
- if item_details and item_details.serial_no:
- serial_nos = item_details.serial_no[0 : cint(row.qty)]
- item["serial_no"] = "\n".join(serial_nos)
- item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
-
- if item_details and item_details.batch_no:
- for batch_no, batch_qty in item_details.batch_no.items():
- if batch_qty >= row.qty:
- item["batch_no"] = batch_no
- item_details.batch_no[batch_no] -= row.qty
- break
-
- items.append(item)
-
- ste_dict = make_rm_stock_entry(args.po_no, items)
- doc = frappe.get_doc(ste_dict)
- doc.insert()
- doc.submit()
-
- return doc
-
-
-def make_subcontract_items():
- sub_contracted_items = {
- "Subcontracted Item SA1": {},
- "Subcontracted Item SA2": {},
- "Subcontracted Item SA3": {},
- "Subcontracted Item SA4": {
- "has_batch_no": 1,
- "create_new_batch": 1,
- "batch_number_series": "SBAT.####",
- },
- "Subcontracted Item SA5": {},
- "Subcontracted Item SA6": {},
- }
-
- 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():
- raw_materials = {
- "Subcontracted SRM Item 1": {},
- "Subcontracted SRM Item 2": {"has_serial_no": 1, "serial_no_series": "SRI.####"},
- "Subcontracted SRM Item 3": {
- "has_batch_no": 1,
- "create_new_batch": 1,
- "batch_number_series": "BAT.####",
- },
- "Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
- "Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
- }
-
- for item, properties in raw_materials.items():
- if not frappe.db.exists("Item", item):
- properties.update({"is_stock_item": 1})
- make_item(item, properties)
-
-
-def make_bom_for_subcontracted_items():
- boms = {
- "Subcontracted Item SA1": [
- "Subcontracted SRM Item 1",
- "Subcontracted SRM Item 2",
- "Subcontracted SRM Item 3",
- ],
- "Subcontracted Item SA2": ["Subcontracted SRM Item 2"],
- "Subcontracted Item SA3": ["Subcontracted SRM Item 2"],
- "Subcontracted Item SA4": [
- "Subcontracted SRM Item 1",
- "Subcontracted SRM Item 2",
- "Subcontracted SRM Item 3",
- ],
- "Subcontracted Item SA5": ["Subcontracted SRM Item 5"],
- "Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
- }
-
- 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)
-
-
-def set_backflush_based_on(based_on):
- frappe.db.set_value(
- "Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", based_on
- )