feat: subcontract code refactor and enhancement
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 503dda7..ff433b9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -621,8 +621,10 @@
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)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 0f6d927..440cde6 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -53,6 +53,38 @@
} else {
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_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'));
+ }
}
});
@@ -217,7 +249,7 @@
},
has_unsupplied_items: function() {
- return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
+ return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty)
},
make_stock_entry: function() {
@@ -513,12 +545,14 @@
],
primary_action: function() {
var data = d.get_values();
+ var content_msg = 'Reason for hold: ' + data.reason_for_hold;
+
frappe.call({
method: "frappe.desk.form.utils.add_comment",
args: {
reference_doctype: me.frm.doctype,
reference_name: me.frm.docname,
- content: __('Reason for hold:') + " " +data.reason_for_hold,
+ content: __(content_msg),
comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname
},
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 41668c6..bb0ad60 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -609,6 +609,7 @@
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
+ "no_copy": 1,
"oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item Supplied",
@@ -1377,7 +1378,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 00:55:30.781375",
+ "modified": "2021-05-30 15:17:53.663648",
"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 2629ba7..724f863 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -503,9 +503,10 @@
@frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items):
+ rm_items_list = rm_items
if isinstance(rm_items, string_types):
rm_items_list = json.loads(rm_items)
- else:
+ elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
@@ -543,6 +544,8 @@
'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')
}
@@ -582,3 +585,57 @@
def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
+
+@frappe.whitelist()
+def get_materials_from_supplier(purchase_order, po_details):
+ if isinstance(po_details, string_types):
+ 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_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
+
+ 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():
+ 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.calculate_rate_and_amount()
+
+ 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({
+ '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 ''
+ })
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 3b9f8e9..33d1971 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -20,7 +20,6 @@
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch
-from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@@ -771,7 +770,7 @@
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 1"
+ item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
@@ -853,76 +852,6 @@
update_backflush_based_on("BOM")
- def test_backflushed_based_on_for_multiple_batches(self):
- item_code = "_Test Subcontracted FG Item 2"
- make_item('Sub Contracted Raw Material 2', {
- 'is_stock_item': 1,
- 'is_sub_contracted_item': 1
- })
-
- make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
- raw_materials=["Sub Contracted Raw Material 2"])
-
- update_backflush_based_on("Material Transferred for Subcontract")
-
- order_qty = 500
- po = create_purchase_order(item_code=item_code, qty=order_qty,
- is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
-
- make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
-
- rm_items = [
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
- "qty":552,"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.submit()
-
- for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
- make_new_batch(batch_id=batch, item_code=item_code)
-
- pr = make_purchase_receipt(po.name)
-
- # partial receipt
- pr.get('items')[0].qty = 30
- pr.get('items')[0].batch_no = "ABCD1"
-
- purchase_order = po.name
- purchase_order_item = po.items[0].name
-
- for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
- pr.append("items", {
- "item_code": pr.get('items')[0].item_code,
- "item_name": pr.get('items')[0].item_name,
- "uom": pr.get('items')[0].uom,
- "stock_uom": pr.get('items')[0].stock_uom,
- "warehouse": pr.get('items')[0].warehouse,
- "conversion_factor": pr.get('items')[0].conversion_factor,
- "cost_center": pr.get('items')[0].cost_center,
- "rate": pr.get('items')[0].rate,
- "qty": qty,
- "batch_no": batch_no,
- "purchase_order": purchase_order,
- "purchase_order_item": purchase_order_item
- })
-
- pr.submit()
-
- pr1 = make_purchase_receipt(po.name)
- pr1.get('items')[0].qty = 300
- pr1.get('items')[0].batch_no = "ABCD1"
- pr1.save()
-
- pr_key = ("Sub Contracted Raw Material 2", po.name)
- consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
-
- self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
- self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
-
- 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', {
@@ -1117,22 +1046,29 @@
po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None
- po.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 10,
- "rate": args.rate or 500,
- "schedule_date": add_days(nowdate(), 1),
- "include_exploded_items": args.get('include_exploded_items', 1),
- "against_blanket_order": args.against_blanket_order
- })
+ if args.rm_items:
+ for row in args.rm_items:
+ po.append("items", row)
+ else:
+ po.append("items", {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 10,
+ "rate": args.rate or 500,
+ "schedule_date": add_days(nowdate(), 1),
+ "include_exploded_items": args.get('include_exploded_items', 1),
+ "against_blanket_order": args.against_blanket_order
+ })
+
+ po.set_missing_values()
if not args.do_not_save:
po.insert()
if not args.do_not_submit:
if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items")
for d in supp_items:
- d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
+ if not d.reserve_warehouse:
+ d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit()
return po
diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
index d7ea9c1..505ecd8 100644
--- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
@@ -6,21 +6,25 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
- "bom_detail_no",
+ "rm_item_code",
+ "column_break_3",
"stock_uom",
+ "reserve_warehouse",
"conversion_factor",
"column_break_6",
- "rm_item_code",
+ "bom_detail_no",
"reference_name",
- "reserve_warehouse",
"section_break2",
"rate",
"col_break2",
"amount",
"section_break1",
"required_qty",
+ "supplied_qty",
"col_break1",
- "supplied_qty"
+ "returned_qty",
+ "total_supplied_qty",
+ "consumed_qty"
],
"fields": [
{
@@ -125,6 +129,8 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
"read_only": 1
},
{
@@ -142,13 +148,42 @@
{
"fieldname": "col_break2",
"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
+ },
+ {
+ "fieldname": "total_supplied_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Total Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-01 00:41:54.123436",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item Supplied",
diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
index dc00bca..d8c37f5 100644
--- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
@@ -6,10 +6,11 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
- "description",
+ "rm_item_code",
+ "item_name",
"bom_detail_no",
"col_break1",
- "rm_item_code",
+ "description",
"stock_uom",
"conversion_factor",
"reference_name",
@@ -52,7 +53,6 @@
"fieldname": "description",
"fieldtype": "Text Editor",
"in_global_search": 1,
- "in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Data",
@@ -87,12 +87,13 @@
"read_only": 1
},
{
+ "columns": 2,
"fieldname": "consumed_qty",
"fieldtype": "Float",
+ "in_list_view": 1,
"label": "Consumed Qty",
"oldfieldname": "consumed_qty",
"oldfieldtype": "Currency",
- "read_only": 1,
"reqd": 1
},
{
@@ -183,12 +184,18 @@
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-05-29 17:22:14.977117",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Receipt Item Supplied",
diff --git a/erpnext/buying/report/subcontract_order_summary/__init__.py b/erpnext/buying/report/subcontract_order_summary/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/__init__.py
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
new file mode 100644
index 0000000..5ba52f1
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Subcontract Order Summary"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("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",
+ fieldtype: "Date",
+ default: frappe.datetime.get_today(),
+ reqd: 1
+ },
+ {
+ label: __("Purchase Order"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Purchase Order",
+ get_query: function() {
+ return {
+ filters: {
+ docstatus: 1,
+ is_subcontracted: 'Yes',
+ company: frappe.query_report.get_filter_value('company')
+ }
+ }
+ }
+ }
+ ]
+};
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
new file mode 100644
index 0000000..526a8d8
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-31 14:43:32.417694",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-05-31 14:43:32.417694",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Subcontract Order Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Purchase Order",
+ "report_name": "Subcontract Order Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Purchase Manager"
+ },
+ {
+ "role": "Purchase User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
new file mode 100644
index 0000000..8b08d2a
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -0,0 +1,158 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ data = []
+ orders = get_subcontracted_orders(report_filters)
+
+ if orders:
+ supplied_items = get_supplied_items(orders, report_filters)
+ po_details = prepare_subcontracted_data(orders, supplied_items)
+ get_subcontracted_data(po_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`']
+
+ filters = get_filters(report_filters)
+
+ return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
+
+def get_filters(report_filters):
+ filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
+ ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
+
+ for field in ['name', 'company']:
+ if report_filters.get(field):
+ filters.append(['Purchase Order', field, '=', report_filters.get(field)])
+
+ return filters
+
+def get_supplied_items(orders, report_filters):
+ if not orders:
+ return []
+
+ fields = ['parent', 'main_item_code', '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}
+
+ supplied_items = {}
+ for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
+ new_key = (row.parent, row.reference_name, row.main_item_code)
+
+ supplied_items.setdefault(new_key, []).append(row)
+
+ return supplied_items
+
+def prepare_subcontracted_data(orders, supplied_items):
+ po_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': []}))
+
+ details = po_details[key]
+
+ if supplied_items.get(key):
+ for supplied_item in supplied_items[key]:
+ details['supplied_items'].append(supplied_item)
+
+ return po_details
+
+def get_subcontracted_data(po_details, data):
+ for key, details in po_details.items():
+ res = details.po_item
+ for index, row in enumerate(details.supplied_items):
+ if index != 0:
+ res = {}
+
+ res.update(row)
+ data.append(res)
+
+def get_columns():
+ return [
+ {
+ "label": _("Id"),
+ "fieldname": "po_id",
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "width": 100
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "width": 80
+ },
+ {
+ "label": _("Subcontracted Item"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 140
+ },
+ {
+ "label": _("Qty"),
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "width": 70
+ },
+ {
+ "label": _("Received"),
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "width": 80
+ },
+ {
+ "label": _("Supplied Item"),
+ "fieldname": "rm_item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 140
+ },
+ {
+ "label": _("Required Qty"),
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Supplied Qty"),
+ "fieldname": "supplied_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Returned Qty"),
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Total Supplied"),
+ "fieldname": "total_supplied_qty",
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "label": _("Consumed Qty"),
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ ]
\ No newline at end of file
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 20f5445..1907885 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -11,16 +11,17 @@
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate
-from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
-from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate
-class BuyingController(StockController):
+from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.subcontracting import Subcontracting
+
+class BuyingController(StockController, Subcontracting):
def get_feed(self):
if self.get("supplier_name"):
@@ -256,7 +257,7 @@
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.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+ 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,
@@ -298,23 +299,7 @@
def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes":
- parent_items = []
- backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
- "backflush_raw_materials_of_subcontract_based_on")
- if (self.doctype == 'Purchase Receipt' and
- backflush_raw_materials_based_on != 'BOM'):
- self.update_raw_materials_supplied_based_on_stock_entries()
- else:
- for item in self.get("items"):
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- item.rm_supp_cost = 0.0
- if item.bom and item.item_code in self.sub_contracted_items:
- self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
-
- if [item.item_code, item.name] not in parent_items:
- parent_items.append([item.item_code, item.name])
-
- self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
+ self.set_materials_for_subcontracted_items(raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
@@ -323,176 +308,6 @@
if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', [])
- def update_raw_materials_supplied_based_on_stock_entries(self):
- self.set('supplied_items', [])
-
- purchase_orders = set(d.purchase_order for d in self.items)
-
- # qty of raw materials backflushed (for each item per purchase order)
- backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
-
- # qty of "finished good" item yet to be received
- qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
-
- for item in self.get('items'):
- if not item.purchase_order:
- continue
-
- # reset raw_material cost
- item.rm_supp_cost = 0
-
- # qty of raw materials transferred to the supplier
- transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
-
- non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
-
- item_key = '{}{}'.format(item.item_code, item.purchase_order)
-
- fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
-
- if not fg_yet_to_be_received:
- frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
- .format(item.idx, frappe.bold(item.item_code),
- frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
- title=_("Limit Crossed"))
-
- transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
- # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
-
- for raw_material in transferred_raw_materials + non_stock_items:
- rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
- raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
-
- consumed_qty = raw_material_data.get('qty', 0)
- consumed_serial_nos = raw_material_data.get('serial_no', '')
- consumed_batch_nos = raw_material_data.get('batch_nos', '')
-
- transferred_qty = raw_material.qty
-
- rm_qty_to_be_consumed = transferred_qty - consumed_qty
-
- # backflush all remaining transferred qty in the last Purchase Receipt
- if fg_yet_to_be_received == item.qty:
- qty = rm_qty_to_be_consumed
- else:
- qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
-
- if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
- qty = frappe.utils.ceil(qty)
-
- if qty > rm_qty_to_be_consumed:
- qty = rm_qty_to_be_consumed
-
- if not qty: continue
-
- if raw_material.serial_nos:
- set_serial_nos(raw_material, consumed_serial_nos, qty)
-
- if raw_material.batch_nos:
- backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
-
- batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
- qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
-
- for batch_data in batches_qty:
- qty = batch_data['qty']
- raw_material.batch_no = batch_data['batch']
- if qty > 0:
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- else:
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
-
- def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
- rm = self.append('supplied_items', {})
- rm.update(raw_material_data)
-
- if not rm.main_item_code:
- rm.main_item_code = fg_item_row.item_code
-
- rm.reference_name = fg_item_row.name
- rm.required_qty = qty
- rm.consumed_qty = qty
-
- def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
- exploded_item = 1
- if hasattr(item, 'include_exploded_items'):
- exploded_item = item.get('include_exploded_items')
-
- bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
-
- used_alternative_items = []
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
- used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
-
- raw_materials_cost = 0
- items = list(set([d.item_code for d in bom_items]))
- item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
- from `tabItem` i, `tabItem Default` id
- where id.parent=i.name and id.company=%s and i.name in ({0})"""
- .format(", ".join(["%s"] * len(items))), [self.company] + items))
-
- for bom_item in bom_items:
- if self.doctype == "Purchase Order":
- reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
- if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
- reserve_warehouse = None
-
- conversion_factor = item.conversion_factor
- if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
- bom_item.item_code in used_alternative_items):
- alternative_item_data = used_alternative_items.get(bom_item.item_code)
- bom_item.item_code = alternative_item_data.item_code
- bom_item.item_name = alternative_item_data.item_name
- bom_item.stock_uom = alternative_item_data.stock_uom
- conversion_factor = alternative_item_data.conversion_factor
- bom_item.description = alternative_item_data.description
-
- # check if exists
- exists = 0
- for d in self.get(raw_material_table):
- if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
- and d.reference_name == item.name:
- rm, exists = d, 1
- break
-
- if not exists:
- rm = self.append(raw_material_table, {})
-
- required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
- flt(conversion_factor), rm.precision("required_qty"))
- rm.reference_name = item.name
- rm.bom_detail_no = bom_item.name
- rm.main_item_code = item.item_code
- rm.rm_item_code = bom_item.item_code
- rm.stock_uom = bom_item.stock_uom
- rm.required_qty = required_qty
- rm.rate = bom_item.rate
- rm.conversion_factor = conversion_factor
-
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- rm.consumed_qty = required_qty
- rm.description = bom_item.description
- if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
- rm.batch_no = item.batch_no
- elif not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
- def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
- """Remove all those child items which are no longer present in main item table"""
- delete_list = []
- for d in self.get(raw_material_table):
- if [d.main_item_code, d.reference_name] not in parent_items:
- # mark for deletion from doclist
- delete_list.append(d)
-
- # delete from doclist
- if delete_list:
- rm_supplied_details = self.get(raw_material_table)
- self.set(raw_material_table, [])
- for d in rm_supplied_details:
- if d not in delete_list:
- self.append(raw_material_table, d)
-
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
@@ -867,104 +682,6 @@
else:
validate_item_type(self, "is_purchase_item", "purchase")
-
-def get_items_from_bom(item_code, bom, exploded_item=1):
- doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
-
- bom_items = frappe.db.sql("""select t2.item_code, t2.name,
- t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
- t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
- from
- `tabBOM` t1, `tab{0}` t2, tabItem t3
- where
- t2.parent = t1.name and t1.item = %s
- and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
- and t2.sourced_by_supplier = 0
- and t2.item_code = t3.name""".format(doctype),
- (item_code, bom), as_dict=1)
-
- if not bom_items:
- msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
-
- return bom_items
-
-def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
- common_query = """
- SELECT
- sed.item_code AS rm_item_code,
- SUM(sed.qty) AS qty,
- sed.description,
- sed.stock_uom,
- sed.subcontracted_item AS main_item_code,
- {serial_no_concat_syntax} AS serial_nos,
- {batch_no_concat_syntax} AS batch_nos
- FROM `tabStock Entry` se,`tabStock Entry Detail` sed
- WHERE
- se.name = sed.parent
- AND se.docstatus=1
- AND se.purpose='Send to Subcontractor'
- AND se.purchase_order = %s
- AND IFNULL(sed.t_warehouse, '') != ''
- AND IFNULL(sed.subcontracted_item, '') in ('', %s)
- GROUP BY sed.item_code, sed.subcontracted_item
- """
- raw_materials = frappe.db.multisql({
- 'mariadb': common_query.format(
- serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
- batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
- ),
- 'postgres': common_query.format(
- serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
- batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
- )
- }, (purchase_order, fg_item), as_dict=1)
-
- return raw_materials
-
-def get_backflushed_subcontracted_raw_materials(purchase_orders):
- purchase_receipts = frappe.get_all("Purchase Receipt Item",
- fields = ["purchase_order", "item_code", "name", "parent"],
- filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
-
- distinct_purchase_receipts = {}
- for pr in purchase_receipts:
- key = (pr.purchase_order, pr.item_code, pr.parent)
- distinct_purchase_receipts.setdefault(key, []).append(pr.name)
-
- backflushed_raw_materials_map = frappe._dict()
- for args, references in iteritems(distinct_purchase_receipts):
- purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
-
- for data in purchase_receipt_supplied_items:
- pr_key = (data.rm_item_code, data.main_item_code, args[0])
- if pr_key not in backflushed_raw_materials_map:
- backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
- "qty": 0.0,
- "serial_no": [],
- "batch_no": [],
- "consumed_batch": {}
- }))
-
- row = backflushed_raw_materials_map.get(pr_key)
- row.qty += data.consumed_qty
-
- for field in ["serial_no", "batch_no"]:
- if data.get(field):
- row[field].append(data.get(field))
-
- if data.get("batch_no"):
- if data.get("batch_no") in row.consumed_batch:
- row.consumed_batch[data.get("batch_no")] += data.consumed_qty
- else:
- row.consumed_batch[data.get("batch_no")] = data.consumed_qty
-
- return backflushed_raw_materials_map
-
-def get_supplied_items(item_code, purchase_receipt, references):
- return frappe.get_all("Purchase Receipt Item Supplied",
- fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
- filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
-
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@@ -996,135 +713,3 @@
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message)
-
-def get_qty_to_be_received(purchase_orders):
- return frappe._dict(frappe.db.sql("""
- SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
- SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
- FROM `tabPurchase Order Item` poi
- WHERE
- poi.`parent` in %s
- GROUP BY poi.`item_code`, poi.`parent`
- HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
- """, (purchase_orders)))
-
-def get_non_stock_items(purchase_order, fg_item_code):
- return frappe.db.sql("""
- SELECT
- pois.main_item_code,
- pois.rm_item_code,
- item.description,
- pois.required_qty AS qty,
- pois.rate,
- 1 as non_stock_item,
- pois.stock_uom
- FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
- WHERE
- pois.`rm_item_code` = item.`name`
- AND item.is_stock_item = 0
- AND pois.`parent` = %s
- AND pois.`main_item_code` = %s
- """, (purchase_order, fg_item_code), as_dict=1)
-
-
-def set_serial_nos(raw_material, consumed_serial_nos, qty):
- serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
- set(get_serial_nos(consumed_serial_nos))
- if serial_nos and qty <= len(serial_nos):
- raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
-
-def get_transferred_batch_qty_map(purchase_order, fg_item):
- # returns
- # {
- # (item_code, fg_code): {
- # batch1: 10, # qty
- # batch2: 16
- # },
- # }
- transferred_batch_qty_map = {}
- transferred_batches = frappe.db.sql("""
- SELECT
- sed.batch_no,
- SUM(sed.qty) AS qty,
- sed.item_code,
- sed.subcontracted_item
- FROM `tabStock Entry` se,`tabStock Entry Detail` sed
- WHERE
- se.name = sed.parent
- AND se.docstatus=1
- AND se.purpose='Send to Subcontractor'
- AND se.purchase_order = %s
- AND ifnull(sed.subcontracted_item, '') in ('', %s)
- AND sed.batch_no IS NOT NULL
- GROUP BY
- sed.batch_no,
- sed.item_code
- """, (purchase_order, fg_item), as_dict=1)
-
- for batch_data in transferred_batches:
- key = ((batch_data.item_code, fg_item)
- if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
- transferred_batch_qty_map.setdefault(key, OrderedDict())
- transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
-
- return transferred_batch_qty_map
-
-def get_backflushed_batch_qty_map(purchase_order, fg_item):
- # returns
- # {
- # (item_code, fg_code): {
- # batch1: 10, # qty
- # batch2: 16
- # },
- # }
- backflushed_batch_qty_map = {}
- backflushed_batches = frappe.db.sql("""
- SELECT
- pris.batch_no,
- SUM(pris.consumed_qty) AS qty,
- pris.rm_item_code AS item_code
- FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
- WHERE
- pr.name = pri.parent
- AND pri.parent = pris.parent
- AND pri.purchase_order = %s
- AND pri.item_code = pris.main_item_code
- AND pr.docstatus = 1
- AND pris.main_item_code = %s
- AND pris.batch_no IS NOT NULL
- GROUP BY
- pris.rm_item_code, pris.batch_no
- """, (purchase_order, fg_item), as_dict=1)
-
- for batch_data in backflushed_batches:
- backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
- backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
-
- return backflushed_batch_qty_map
-
-def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
- # Returns available batches to be backflushed based on requirements
- transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
- if not transferred_batches:
- transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
-
- available_batches = []
-
- for (batch, transferred_qty) in transferred_batches.items():
- backflushed_qty = backflushed_batches.get(batch, 0)
- available_qty = transferred_qty - backflushed_qty
-
- if available_qty >= required_qty:
- available_batches.append({'batch': batch, 'qty': required_qty})
- break
- elif available_qty != 0:
- available_batches.append({'batch': batch, 'qty': available_qty})
- required_qty -= available_qty
-
- for row in available_batches:
- if backflushed_batches.get(row.get('batch'), 0) > 0:
- backflushed_batches[row.get('batch')] += row.get('qty')
- else:
- backflushed_batches[row.get('batch')] = row.get('qty')
-
- return available_batches
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
new file mode 100644
index 0000000..fe77576
--- /dev/null
+++ b/erpnext/controllers/subcontracting.py
@@ -0,0 +1,342 @@
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from frappe.utils import flt, cint
+from collections import defaultdict
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+class Subcontracting(object):
+ 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_consumed_qty()
+
+ 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.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 = []
+
+ if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table):
+ 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:
+ 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 == 'Purchase 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 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)
+
+ for doctype in ['Purchase Receipt', 'Purchase Invoice']:
+ self.remove_consumed_materials(doctype)
+
+ def remove_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' if doctype == 'Purchase Receipt' else 'po_detail'
+
+ 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(f'{doctype} 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))})
+
+ 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
+
+ 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))
+ 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:
+ 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']):
+ 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
+ else:
+ rm_obj.required_qty = qty
+ rm_obj.consumed_qty = qty
+ self.set_serial_nos(item_row, rm_obj)
+
+ 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})
+ 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):
+ if self.is_subcontracted != 'Yes':
+ return
+
+ self.get_purchase_orders()
+ consumed_items, pr_items = self.remove_consumed_materials(self.doctype, return_consumed_items=True)
+
+ itemwise_consumed_qty = defaultdict(float)
+ 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_consumed_qty(self):
+ for row in self.get(self.raw_material_table):
+ if flt(row.consumed_qty) == 0.0 and row.get('serial_no'):
+ msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
+
+ frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index e1cca9e..42b23f2 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -12,6 +12,7 @@
from six import string_types
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records('BOM')
@@ -160,6 +161,7 @@
def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1"
+ set_backflush_based_on('Material Transferred for Subcontract')
if not frappe.db.exists('Item', item_code):
make_item(item_code, {
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 0514bd2..4364201 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -54,7 +54,7 @@
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
+
self.set_projected_qty()
self.db_update()
@@ -115,7 +115,7 @@
#Get Transferred Entries
materials_transferred = frappe.db.sql("""
select
- ifnull(sum(transfer_qty),0)
+ ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
from
`tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
where
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index d5700fe..8f76844 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -18,6 +18,9 @@
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')
+
create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC',
qty=5, rate=2000)
create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
@@ -65,6 +68,8 @@
status = True
self.assertEqual(status, True)
+ frappe.db.set_value('Buying Settings', None,
+ 'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract')
def test_alternative_item_for_production_rm(self):
create_stock_reconciliation(item_code='Alternate Item For A RW 1',
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index befdad9..887b15a 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -41,6 +41,8 @@
}
});
+ frm.set_df_property('supplied_items', 'cannot_add_rows', 1);
+
},
onload: function(frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index ad350d3..44fb736 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -514,8 +514,7 @@
"oldfieldname": "pr_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Receipt Item Supplied",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "section_break0",
@@ -1149,7 +1148,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 01:01:00.754119",
+ "modified": "2021-05-25 00:15:12.239017",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 83ba324..b8580f9 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -202,6 +202,7 @@
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("""select t1.name
@@ -233,6 +234,7 @@
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):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 8d9b675..95096d7 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -335,6 +335,10 @@
se2.cancel()
se3.cancel()
po.reload()
+ pr2.load_from_db()
+ pr2.cancel()
+
+ po.load_from_db()
po.cancel()
def test_serial_no_supplier(self):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 1a25994..6708393 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1079,6 +1079,10 @@
}
function attach_bom_items(bom_no) {
+ if (!bom_no) {
+ return
+ }
+
if (check_should_not_attach_bom_items(bom_no)) return
frappe.db.get_doc("BOM",bom_no).then(bom => {
const {name, items} = bom
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index a0b5457..523d332 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -74,7 +74,8 @@
"total_amount",
"job_card",
"amended_from",
- "credit_note"
+ "credit_note",
+ "is_return"
],
"fields": [
{
@@ -611,6 +612,16 @@
"fieldname": "apply_putaway_rule",
"fieldtype": "Check",
"label": "Apply Putaway Rule"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_return",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Return",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -618,7 +629,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-05-24 11:32:23.904307",
+ "modified": "2021-05-26 17:07:58.015737",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 560ceaa..2132808 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -97,8 +97,7 @@
update_serial_nos_after_submit(self, "items")
self.update_work_order()
self.validate_purchase_order()
- if self.purchase_order and self.purpose == "Send to Subcontractor":
- self.update_purchase_order_supplied_items()
+ self.update_purchase_order_supplied_items()
self.make_gl_entries()
@@ -117,9 +116,7 @@
self.set_material_request_transfer_status('Completed')
def on_cancel(self):
-
- if self.purchase_order and self.purpose == "Send to Subcontractor":
- self.update_purchase_order_supplied_items()
+ self.update_purchase_order_supplied_items()
if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status()
@@ -1347,7 +1344,7 @@
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item",
- "expense_account", "description", "item_name"]:
+ "expense_account", "description", "item_name", "serial_no", "batch_no"]:
if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field))
@@ -1400,33 +1397,26 @@
.format(item.batch_no, item.item_code))
def update_purchase_order_supplied_items(self):
- #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))
+ if (self.purchase_order and
+ (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)):
- #Update Supplied Qty in PO Supplied Items
+ #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))
- frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos
- SET
- pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
- FROM
- `tabStock Entry Detail` sed, `tabStock Entry` se
- WHERE
- pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code
- AND pos.parent = se.purchase_order AND sed.docstatus = 1
- AND se.name = sed.parent and se.purchase_order = %(po)s
- ), 0)
- WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
+ supplied_items = get_supplied_items(self.purchase_order)
+ for name, item in supplied_items.items():
+ frappe.db.set_value('Purchase Order Item Supplied', name, item)
- #Update reserved sub contracted quantity in bin based on Supplied Item Details and
- for d in self.get("items"):
- item_code = d.get('original_item') or d.get('item_code')
- reserve_warehouse = item_wh.get(item_code)
- stock_bin = get_bin(item_code, reserve_warehouse)
- stock_bin.update_reserved_qty_for_sub_contracting()
+ #Update reserved sub contracted quantity in bin based on Supplied Item Details and
+ for d in self.get("items"):
+ item_code = d.get('original_item') or d.get('item_code')
+ reserve_warehouse = item_wh.get(item_code)
+ stock_bin = get_bin(item_code, reserve_warehouse)
+ stock_bin.update_reserved_qty_for_sub_contracting()
def update_so_in_serial_number(self):
so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"])
@@ -1480,7 +1470,7 @@
cond += """ WHEN (parent = %s and name = %s) THEN %s
""" %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty)
- if cond and stock_entries_child_list:
+ if stock_entries_child_list:
frappe.db.sql(""" UPDATE `tabStock Entry Detail`
SET
transferred_qty = CASE {cond} END
@@ -1751,3 +1741,30 @@
format(max_retain_qty, batch_no, item_code), alert=True)
sample_quantity = qty_diff
return sample_quantity
+
+def get_supplied_items(purchase_order):
+ fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`',
+ '`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`']
+
+ filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]]
+
+ supplied_item_details = {}
+ for row in frappe.get_all('Stock Entry', fields = fields, filters = filters):
+ if not row.po_detail:
+ continue
+
+ key = row.po_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}))
+
+ supplied_item = supplied_item_details[key]
+
+ if row.is_return:
+ supplied_item.returned_qty += row.transfer_qty
+ else:
+ supplied_item.supplied_qty += row.transfer_qty
+
+ supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
+
+ return supplied_item_details
\ No newline at end of file
diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py
new file mode 100644
index 0000000..c1a458a
--- /dev/null
+++ b/erpnext/tests/test_subcontracting.py
@@ -0,0 +1,583 @@
+from __future__ import unicode_literals
+import frappe
+import unittest
+import copy
+from frappe.utils import cint
+from collections import defaultdict
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry,
+ make_purchase_receipt, get_materials_from_supplier)
+
+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="Yes",
+ 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="Yes",
+ 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="Yes",
+ 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="Yes",
+ 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="Yes",
+ 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="Yes",
+ 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="Yes",
+ 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_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="Yes",
+ 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 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']:
+ 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)
\ No newline at end of file