Merge branch 'develop' into make-image-field-obsolete-in-web-item
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 686db9d..ec861a2 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -572,9 +572,10 @@
},
is_subcontracted: function(frm) {
- if (frm.doc.is_subcontracted) {
+ if (frm.doc.is_old_subcontracting_flow) {
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..534b879 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -169,7 +169,8 @@
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference",
- "per_received"
+ "per_received",
+ "is_old_subcontracting_flow"
],
"fields": [
{
@@ -547,7 +548,8 @@
"fieldname": "is_subcontracted",
"fieldtype": "Check",
"label": "Is Subcontracted",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"fieldname": "items_section",
@@ -1365,7 +1367,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",
@@ -1416,13 +1418,21 @@
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
- }
+ },
+ {
+ "default": "0",
+ "fieldname": "is_old_subcontracting_flow",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Old Subcontracting Flow",
+ "read_only": 1
+ }
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-11-25 13:31:02.716727",
+ "modified": "2022-06-15 15:40:58.527065",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 4e0d1c9..775d255 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -502,7 +502,10 @@
# 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()
+
+ if self.is_old_subcontracting_flow:
+ self.set_consumed_qty_in_subcontract_order()
+
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@@ -1405,7 +1408,9 @@
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
- self.set_consumed_qty_in_po()
+
+ if self.is_old_subcontracting_flow:
+ self.set_consumed_qty_in_subcontract_order()
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 427e481..e55d3a7 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 1f79d47..7fa2fe2 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -619,10 +619,13 @@
"search_index": 1
},
{
+ "depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
- "options": "BOM"
+ "options": "BOM",
+ "read_only": 1,
+ "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
},
{
"default": "0",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 33dbe3f..fbb42fe 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -8,15 +8,17 @@
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
+ if (frm.doc.is_old_subcontracting_flow) {
+ 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,12 +30,67 @@
}
});
+ frm.set_query("fg_item", "items", function() {
+ return {
+ filters: {
+ 'is_sub_contracted_item': 1,
+ 'default_bom': ['!=', '']
+ }
+ }
+ });
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
+ refresh: function(frm) {
+ if(frm.doc.is_old_subcontracting_flow) {
+ frm.trigger('get_materials_from_supplier');
+
+ $('a.grey-link').each(function () {
+ var id = $(this).children(':first-child').attr('data-label');
+ if (id == 'Duplicate') {
+ $(this).remove();
+ return false;
+ }
+ });
+ }
+ },
+
+ 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.controllers.subcontracting_controller.get_materials_from_supplier',
+ freeze: true,
+ freeze_message: __('Creating Stock Entry'),
+ args: {
+ subcontract_order: frm.doc.name,
+ rm_details: po_details,
+ order_doctype: cur_frm.doc.doctype
+ },
+ 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'));
+ }
+ },
+
onload: function(frm) {
set_schedule_date(frm);
if (!frm.doc.transaction_date){
@@ -52,39 +109,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", {
@@ -97,6 +121,16 @@
set_schedule_date(frm);
}
}
+ },
+
+ qty: function(frm, cdt, cdn) {
+ if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
+ var row = locals[cdt][cdn];
+
+ if (row.qty) {
+ row.fg_item_qty = row.qty;
+ }
+ }
}
});
@@ -105,12 +139,12 @@
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',
+ 'Stock Entry': 'Material to Supplier'
}
super.setup();
-
}
refresh(doc, cdt, cdn) {
@@ -142,14 +176,17 @@
if(!in_list(["Closed", "Delivered"], doc.status)) {
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
- this.frm.add_custom_button(__('Update Items'), () => {
- erpnext.utils.update_child_items({
- frm: this.frm,
- child_docname: "items",
- child_doctype: "Purchase Order Detail",
- cannot_add_row: false,
- })
- });
+ // Don't add Update Items button if the PO is following the new subcontracting flow.
+ if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
+ this.frm.add_custom_button(__('Update Items'), () => {
+ erpnext.utils.update_child_items({
+ frm: this.frm,
+ child_docname: "items",
+ child_doctype: "Purchase Order Detail",
+ cannot_add_row: false,
+ })
+ });
+ }
}
if (this.frm.has_perm("submit")) {
if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) {
@@ -177,9 +214,15 @@
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) {
+ if (doc.is_old_subcontracting_flow) {
+ if (me.has_unsupplied_items()) {
+ cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer"));
+ }
+ }
+ else {
+ cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create'));
+ }
}
}
if(flt(doc.per_billed) < 100)
@@ -370,10 +413,11 @@
_make_rm_stock_entry(rm_items) {
frappe.call({
- method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry",
+ method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry",
args: {
- purchase_order: cur_frm.doc.name,
- rm_items: rm_items
+ subcontract_order: cur_frm.doc.name,
+ rm_items: rm_items,
+ order_doctype: cur_frm.doc.doctype
}
,
callback: function(r) {
@@ -405,6 +449,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,15 +665,17 @@
}
}
-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]
- ]
+if (cur_frm.doc.is_old_subcontracting_flow) {
+ 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]
+ ]
+ }
}
}
@@ -634,7 +688,7 @@
frappe.provide("erpnext.buying");
frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) {
- if (frm.doc.is_subcontracted) {
+ if (frm.doc.is_old_subcontracting_flow) {
erpnext.buying.get_default_bom(frm);
}
-});
+});
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index b365a83..aa50487 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",
@@ -142,7 +141,8 @@
"party_account_currency",
"is_internal_supplier",
"represents_company",
- "inter_company_order_reference"
+ "inter_company_order_reference",
+ "is_old_subcontracting_flow"
],
"fields": [
{
@@ -158,7 +158,8 @@
"hidden": 1,
"label": "Title",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "reqd": 1
},
{
"fieldname": "naming_series",
@@ -444,11 +445,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",
@@ -457,14 +453,9 @@
"print_hide": 1
},
{
- "fieldname": "col_break_warehouse",
- "fieldtype": "Column Break"
- },
- {
"default": "0",
"fieldname": "is_subcontracted",
"fieldtype": "Check",
- "in_standard_filter": 1,
"label": "Is Subcontracted",
"print_hide": 1
},
@@ -1143,6 +1134,10 @@
"options": "Tax Withholding Category"
},
{
+ "fieldname": "section_break_45",
+ "fieldtype": "Section Break"
+ },
+ {
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -1163,13 +1158,21 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_old_subcontracting_flow",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Old Subcontracting Flow",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-04-26 12:16:38.694276",
+ "modified": "2022-06-15 15:40:58.527065",
"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..cd58d25 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -69,8 +69,12 @@
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")
+
+ if self.is_old_subcontracting_flow:
+ self.validate_bom_for_subcontracting_items()
+ self.create_raw_materials_supplied()
+
+ 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
@@ -194,12 +198,38 @@
)
def validate_bom_for_subcontracting_items(self):
- if self.is_subcontracted:
+ for item in self.items:
+ if not item.bom:
+ frappe.throw(
+ _("Row #{0}: BOM is not specified for subcontracting item {0}").format(
+ item.idx, item.item_code
+ )
+ )
+
+ def validate_fg_item_for_subcontracting(self):
+ if self.is_subcontracted and not self.is_old_subcontracting_flow:
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(
- item.item_code, item.idx
+ _("Row #{0}: Finished Good Item is not specified for service item {1}").format(
+ item.idx, item.item_code
+ )
+ )
+ else:
+ if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
+ frappe.throw(
+ _(
+ "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
+ ).format(item.idx, item.fg_item, item.item_code)
+ )
+ elif not frappe.get_value("Item", item.fg_item, "default_bom"):
+ frappe.throw(
+ _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
+ )
+ if not item.fg_item_qty:
+ frappe.throw(
+ _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
+ item.idx, item.item_code
)
)
@@ -294,9 +324,7 @@
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.update_reserved_qty_for_subcontract()
self.notify_update()
clear_doctype_notifications(self)
@@ -310,9 +338,7 @@
self.update_requested_qty()
self.update_ordered_qty()
self.validate_budget()
-
- if self.is_subcontracted:
- self.update_reserved_qty_for_subcontract()
+ self.update_reserved_qty_for_subcontract()
frappe.get_doc("Authorization Control").validate_approving_authority(
self.doctype, self.company, self.base_grand_total
@@ -332,9 +358,7 @@
if self.has_drop_ship_item():
self.update_delivered_qty_in_sales_order()
- if self.is_subcontracted:
- self.update_reserved_qty_for_subcontract()
-
+ self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled")
@@ -405,10 +429,11 @@
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()
+ if self.is_old_subcontracting_flow:
+ 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(subcontract_doctype="Purchase Order")
def update_receiving_percentage(self):
total_qty, received_qty = 0.0, 0.0
@@ -587,80 +612,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 +642,61 @@
@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()
+
+ if target_doc.set_warehouse:
+ for item in target_doc.items:
+ item.warehouse = target_doc.set_warehouse
+ else:
+ source_doc = frappe.get_doc("Purchase Order", source_name)
+ if source_doc.set_warehouse:
+ for item in target_doc.items:
+ item.warehouse = source_doc.set_warehouse
+ else:
+ for idx, item in enumerate(target_doc.items):
+ item.warehouse = source_doc.items[idx].warehouse
+
+ return target_doc
+
+
+@frappe.whitelist()
+def is_subcontracting_order_created(po_name) -> bool:
+ count = frappe.db.count(
+ "Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]}
+ )
+
+ return True if count else False
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
index 81f2010..01b55c0 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", "Stock Entry"]},
],
}
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 5f84de6..bd7e4e8 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):
@@ -140,43 +136,6 @@
# ordered qty decreases as ordered qty is 0 (deleted row)
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
- def test_supplied_items_validations_on_po_update_after_submit(self):
- po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100)
- item = po.items[0]
-
- original_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
-
- # Just update rate
- trans_item = [
- {
- "item_code": "_Test FG Item",
- "rate": 20,
- "qty": 5,
- "conversion_factor": 1.0,
- "docname": item.name,
- }
- ]
- update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
- po.reload()
-
- new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
- self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys()))
-
- # Update qty to 2x
- trans_item[0]["qty"] *= 2
- update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
- po.reload()
-
- new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
- self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values()))
-
- # Set transfer qty and attempt to update qty, shouldn't be allowed
- po.supplied_items[0].supplied_qty = 2
- po.supplied_items[0].db_update()
- trans_item[0]["qty"] *= 2
- with self.assertRaises(frappe.ValidationError):
- update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
-
def test_update_child(self):
mr = make_material_request(qty=10)
po = make_purchase_order(mr.name)
@@ -426,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()
@@ -609,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
@@ -777,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
@@ -1248,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 7994b08..1a98453 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",
@@ -574,16 +576,18 @@
"read_only": 1
},
{
- "depends_on": "eval:parent.is_subcontracted",
+ "depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
"options": "BOM",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1,
+ "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
},
{
"default": "0",
- "depends_on": "eval:parent.is_subcontracted",
+ "depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
"label": "Include Exploded Items",
@@ -848,6 +852,22 @@
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1
+ },
+ {
+ "depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
+ "fieldname": "fg_item",
+ "fieldtype": "Link",
+ "label": "Finished Good Item",
+ "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
+ "options": "Item"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
+ "fieldname": "fg_item_qty",
+ "fieldtype": "Float",
+ "label": "Finished Good Item Qty",
+ "mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow"
}
],
"idx": 1,
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 67affe7..f319506 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -180,12 +180,20 @@
doc_args = self.as_dict()
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
+ # Get Contact Full Name
+ supplier_name = None
+ if data.get("contact"):
+ contact_name = frappe.db.get_value(
+ "Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
+ )
+ supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
+
args = {
"update_password_link": update_password_link,
"message": frappe.render_template(self.message_for_supplier, doc_args),
"rfq_link": rfq_link,
"user_fullname": full_name,
- "supplier_name": data.get("supplier_name"),
+ "supplier_name": supplier_name or data.get("supplier_name"),
"supplier_salutation": self.salutation or "Dear Mx.",
}
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..075671f 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,29 @@
},
{
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: __("Order Type"),
+ fieldname: "order_type",
+ fieldtype: "Select",
+ options: ["Purchase Order", "Subcontracting Order"],
+ default: "Subcontracting Order"
+ },
+ {
+ label: __("Subcontract Order"),
fieldname: "name",
- fieldtype: "Link",
- options: "Purchase Order",
- get_query: function() {
- return {
- filters: {
- docstatus: 1,
- is_subcontracted: 1,
- company: frappe.query_report.get_filter_value('company')
- }
- }
- }
+ fieldtype: "Data"
}
]
-};
+};
\ 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..0213051 100644
--- a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -8,7 +8,7 @@
def execute(filters=None):
columns, data = [], []
- columns = get_columns()
+ columns = get_columns(filters)
data = get_data(filters)
return columns, data
@@ -20,43 +20,45 @@
if orders:
supplied_items = get_supplied_items(orders, report_filters)
- po_details = prepare_subcontracted_data(orders, supplied_items)
- get_subcontracted_data(po_details, data)
+ order_details = prepare_subcontracted_data(orders, supplied_items)
+ get_subcontracted_data(order_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`",
+ f"`tab{report_filters.order_type} Item`.`parent` as order_id",
+ f"`tab{report_filters.order_type} Item`.`item_code`",
+ f"`tab{report_filters.order_type} Item`.`item_name`",
+ f"`tab{report_filters.order_type} Item`.`qty`",
+ f"`tab{report_filters.order_type} Item`.`name`",
+ f"`tab{report_filters.order_type} Item`.`received_qty`",
+ f"`tab{report_filters.order_type}`.`status`",
]
filters = get_filters(report_filters)
- return frappe.get_all("Purchase Order", fields=fields, filters=filters) or []
+ return frappe.get_all(report_filters.order_type, fields=fields, filters=filters) or []
def get_filters(report_filters):
filters = [
- ["Purchase Order", "docstatus", "=", 1],
- ["Purchase Order", "is_subcontracted", "=", 1],
+ [report_filters.order_type, "docstatus", "=", 1],
[
- "Purchase Order",
+ report_filters.order_type,
"transaction_date",
"between",
(report_filters.from_date, report_filters.to_date),
],
]
+ if report_filters.order_type == "Purchase Order":
+ filters.append(["Purchase Order", "is_old_subcontracting_flow", "=", 1])
+
for field in ["name", "company"]:
if report_filters.get(field):
- filters.append(["Purchase Order", field, "=", report_filters.get(field)])
+ filters.append([report_filters.order_type, field, "=", report_filters.get(field)])
return filters
@@ -77,10 +79,15 @@
"reference_name",
]
- filters = {"parent": ("in", [d.po_id for d in orders]), "docstatus": 1}
+ filters = {"parent": ("in", [d.order_id for d in orders]), "docstatus": 1}
supplied_items = {}
- for row in frappe.get_all("Purchase Order Item Supplied", fields=fields, filters=filters):
+ supplied_items_table = (
+ "Purchase Order Item Supplied"
+ if report_filters.order_type == "Purchase Order"
+ else "Subcontracting Order Supplied Item"
+ )
+ for row in frappe.get_all(supplied_items_table, fields=fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
@@ -89,24 +96,24 @@
def prepare_subcontracted_data(orders, supplied_items):
- po_details = {}
+ order_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.order_id, row.name, row.item_code)
+ if key not in order_details:
+ order_details.setdefault(key, frappe._dict({"order_item": row, "supplied_items": []}))
- details = po_details[key]
+ details = order_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details["supplied_items"].append(supplied_item)
- return po_details
+ return order_details
-def get_subcontracted_data(po_details, data):
- for key, details in po_details.items():
- res = details.po_item
+def get_subcontracted_data(order_details, data):
+ for key, details in order_details.items():
+ res = details.order_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
@@ -115,13 +122,13 @@
data.append(res)
-def get_columns():
+def get_columns(filters):
return [
{
- "label": _("Purchase Order"),
- "fieldname": "po_id",
+ "label": _("Subcontract Order"),
+ "fieldname": "order_id",
"fieldtype": "Link",
- "options": "Purchase Order",
+ "options": filters.order_type,
"width": 100,
},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80},
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
index fc58b6a..6304a09 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js
@@ -5,6 +5,13 @@
frappe.query_reports["Subcontracted Item To Be Received"] = {
"filters": [
{
+ label: __("Order Type"),
+ fieldname: "order_type",
+ fieldtype: "Select",
+ options: ["Purchase Order", "Subcontracting Order"],
+ default: "Subcontracting Order"
+ },
+ {
fieldname: "supplier",
label: __("Supplier"),
fieldtype: "Link",
diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json
index fdf6cf7..f40b788 100644
--- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json
+++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.json
@@ -13,7 +13,7 @@
"name": "Subcontracted Item To Be Received",
"owner": "Administrator",
"prepared_report": 0,
- "ref_doctype": "Purchase Order",
+ "ref_doctype": "Subcontracting Order",
"report_name": "Subcontracted Item To Be Received",
"report_type": "Script Report",
"roles": [
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..135449b 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
@@ -11,18 +11,18 @@
frappe.msgprint(_("To Date must be greater than From Date"))
data = []
- columns = get_columns()
+ columns = get_columns(filters)
get_data(data, filters)
return columns, data
-def get_columns():
+def get_columns(filters):
return [
{
- "label": _("Purchase Order"),
+ "label": _("Subcontract Order"),
"fieldtype": "Link",
- "fieldname": "purchase_order",
- "options": "Purchase Order",
+ "fieldname": "subcontract_order",
+ "options": filters.order_type,
"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)
- for item in sub_items:
- for order in po:
+ orders = get_subcontract_orders(filters)
+ orders_name = [order.name for order in orders]
+ subcontracted_items = get_subcontract_order_supplied_item(filters.order_type, orders_name)
+ for item in subcontracted_items:
+ for order in orders:
if order.name == item.parent and item.received_qty < item.qty:
row = {
- "purchase_order": item.parent,
+ "subcontract_order": item.parent,
"date": order.transaction_date,
"supplier": order.supplier,
"fg_item_code": item.item_code,
@@ -76,22 +76,25 @@
data.append(row)
-def get_po(filters):
+def get_subcontract_orders(filters):
record_filters = [
- ["is_subcontracted", "=", 1],
["supplier", "=", filters.supplier],
["transaction_date", "<=", filters.to_date],
["transaction_date", ">=", filters.from_date],
["docstatus", "=", 1],
]
+
+ if filters.order_type == "Purchase Order":
+ record_filters.append(["is_old_subcontracting_flow", "=", 1])
+
return frappe.get_all(
- "Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]
+ filters.order_type, filters=record_filters, fields=["name", "transaction_date", "supplier"]
)
-def get_purchase_order_item_supplied(po):
+def get_subcontract_order_supplied_item(order_type, orders):
return frappe.get_all(
- "Purchase Order Item",
- filters=[("parent", "IN", po)],
+ f"{order_type} Item",
+ filters=[("parent", "IN", orders)],
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..c772c1a 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,28 @@
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,
+ "order_type": "Subcontracting Order",
+ "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]["subcontract_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.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
index 0853afd..b6739fe 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js
@@ -5,6 +5,13 @@
frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
"filters": [
{
+ label: __("Order Type"),
+ fieldname: "order_type",
+ fieldtype: "Select",
+ options: ["Purchase Order", "Subcontracting Order"],
+ default: "Subcontracting Order"
+ },
+ {
fieldname: "supplier",
label: __("Supplier"),
fieldtype: "Link",
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json
index c7cee5e..f689fbc 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.json
@@ -13,7 +13,7 @@
"name": "Subcontracted Raw Materials To Be Transferred",
"owner": "Administrator",
"prepared_report": 0,
- "ref_doctype": "Purchase Order",
+ "ref_doctype": "Subcontracting Order",
"report_name": "Subcontracted Raw Materials To Be Transferred",
"report_type": "Script Report",
"roles": [
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..ef28eda 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
@@ -10,19 +10,19 @@
if filters.from_date >= filters.to_date:
frappe.msgprint(_("To Date must be greater than From Date"))
- columns = get_columns()
+ columns = get_columns(filters)
data = get_data(filters)
return columns, data or []
-def get_columns():
+def get_columns(filters):
return [
{
- "label": _("Purchase Order"),
+ "label": _("Subcontract Order"),
"fieldtype": "Link",
- "fieldname": "purchase_order",
- "options": "Purchase Order",
+ "fieldname": "subcontract_order",
+ "options": filters.order_type,
"width": 200,
},
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150},
@@ -46,10 +46,10 @@
def get_data(filters):
- po_rm_item_details = get_po_items_to_supply(filters)
+ order_rm_item_details = get_order_items_to_supply(filters)
data = []
- for row in po_rm_item_details:
+ for row in order_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,33 @@
return data
-def get_po_items_to_supply(filters):
+def get_order_items_to_supply(filters):
+ supplied_items_table = (
+ "Purchase Order Item Supplied"
+ if filters.order_type == "Purchase Order"
+ else "Subcontracting Order Supplied Item"
+ )
+
+ record_filters = [
+ [filters.order_type, "per_received", "<", "100"],
+ [filters.order_type, "supplier", "=", filters.supplier],
+ [filters.order_type, "transaction_date", "<=", filters.to_date],
+ [filters.order_type, "transaction_date", ">=", filters.from_date],
+ [filters.order_type, "docstatus", "=", 1],
+ ]
+
+ if filters.order_type == "Purchase Order":
+ record_filters.append([filters.order_type, "is_old_subcontracting_flow", "=", 1])
+
return frappe.db.get_all(
- "Purchase Order",
+ filters.order_type,
fields=[
- "name as purchase_order",
+ "name as subcontract_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",
+ f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
+ f"`tab{supplied_items_table}`.required_qty as reqd_qty",
+ f"`tab{supplied_items_table}`.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],
- ],
+ filters=record_filters,
)
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..1602957 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,34 @@
# 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.subcontracting_controller import make_rm_stock_entry
+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
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 +38,48 @@
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,
+ "order_type": "Subcontracting Order",
+ "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("subcontract_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]["subcontract_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 +90,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 +101,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 ceac815..5528b4d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2709,10 +2709,10 @@
parent.update_ordered_qty()
parent.update_ordered_and_reserved_qty()
parent.update_receiving_percentage()
- if parent.is_subcontracted:
+ if parent.is_old_subcontracting_flow:
if should_update_supplied_items(parent):
parent.update_reserved_qty_for_subcontract()
- parent.create_raw_materials_supplied("supplied_items")
+ parent.create_raw_materials_supplied()
parent.save()
else: # Sales Order
parent.validate_warehouse()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 8d5a042..036733c 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -11,8 +11,7 @@
from erpnext.accounts.party import get_party_details
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.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
@@ -21,7 +20,7 @@
pass
-class BuyingController(StockController, Subcontracting):
+class BuyingController(SubcontractingController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
@@ -55,7 +54,8 @@
# sub-contracting
self.validate_for_subcontracting()
- self.create_raw_materials_supplied("supplied_items")
+ if self.get("is_old_subcontracting_flow"):
+ self.create_raw_materials_supplied()
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
@@ -256,13 +256,18 @@
)
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)
- ) / qty_in_stock_uom
+ if self.get("is_old_subcontracting_flow"):
+ 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)
+ ) / qty_in_stock_uom
+ else:
+ item.valuation_rate = (
+ 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 +322,25 @@
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.is_subcontracted and self.get("is_old_subcontracting_flow"):
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 +464,9 @@
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.bom or d.fg_item)) or d.from_warehouse
+ else 0,
}
)
sl_entries.append(sle)
@@ -538,7 +494,8 @@
)
)
- self.make_sl_entries_for_supplier_warehouse(sl_entries)
+ if self.get("is_old_subcontracting_flow"):
+ self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
allow_negative_stock=allow_negative_stock,
@@ -565,26 +522,9 @@
)
po_obj.update_ordered_qty(po_item_rows)
- if self.is_subcontracted:
+ if self.get("is_old_subcontracting_flow"):
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"):
return
@@ -808,7 +748,7 @@
if self.doctype == "Material Request":
return
- if hasattr(self, "is_subcontracted") and self.is_subcontracted:
+ if self.get("is_old_subcontracting_flow"):
validate_item_type(self, "is_sub_contracted_item", "subcontracted")
else:
validate_item_type(self, "is_purchase_item", "purchase")
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..2a2f8f5
--- /dev/null
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -0,0 +1,902 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import copy
+import json
+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 __init__(self, *args, **kwargs):
+ super(SubcontractingController, self).__init__(*args, **kwargs)
+ if self.get("is_old_subcontracting_flow"):
+ self.subcontract_data = frappe._dict(
+ {
+ "order_doctype": "Purchase Order",
+ "order_field": "purchase_order",
+ "rm_detail_field": "po_detail",
+ "receipt_supplied_items_field": "Purchase Receipt Item Supplied",
+ "order_supplied_items_field": "Purchase Order Item Supplied",
+ }
+ )
+ else:
+ self.subcontract_data = frappe._dict(
+ {
+ "order_doctype": "Subcontracting Order",
+ "order_field": "subcontracting_order",
+ "rm_detail_field": "sco_rm_detail",
+ "receipt_supplied_items_field": "Subcontracting Receipt Supplied Item",
+ "order_supplied_items_field": "Subcontracting Order Supplied Item",
+ }
+ )
+
+ def before_validate(self):
+ if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
+ self.remove_empty_rows()
+ self.set_items_conversion_factor()
+
+ def validate(self):
+ if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
+ self.validate_items()
+ self.create_raw_materials_supplied()
+ else:
+ super(SubcontractingController, self).validate()
+
+ 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 in ["Purchase Receipt", "Purchase Invoice", "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 in ["Purchase Order", "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_subcontract_orders(self):
+ self.subcontract_orders = []
+
+ if self.doctype in ["Purchase Order", "Subcontracting Order"]:
+ return
+
+ self.subcontract_orders = [
+ item.get(self.subcontract_data.order_field)
+ for item in self.items
+ if item.get(self.subcontract_data.order_field)
+ ]
+
+ def __get_pending_qty_to_receive(self):
+ """Get qty to be received against the subcontract order."""
+
+ self.qty_to_be_received = defaultdict(float)
+
+ if (
+ self.doctype != self.subcontract_data.order_doctype
+ and self.backflush_based_on != "BOM"
+ and self.subcontract_orders
+ ):
+ for row in frappe.get_all(
+ f"{self.subcontract_data.order_doctype} Item",
+ fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
+ filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
+ ):
+
+ self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
+
+ def __get_transferred_items(self):
+ fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"]
+ 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",
+ self.subcontract_data.rm_detail_field,
+ ]
+
+ 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", self.subcontract_data.order_field, "in", self.subcontract_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 = []
+ for field in ["name", self.subcontract_data.order_field, "parent"]:
+ fields.append(f"`tab{doctype} Item`.`{field}`")
+
+ filters = [
+ [doctype, "docstatus", "=", 1],
+ [f"{doctype} Item", self.subcontract_data.order_field, "in", self.subcontract_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, receipt_items):
+ return frappe.get_all(
+ self.subcontract_data.receipt_supplied_items_field,
+ fields=[
+ "serial_no",
+ "rm_item_code",
+ "reference_name",
+ "batch_no",
+ "consumed_qty",
+ "main_item_code",
+ ],
+ filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
+ )
+
+ def __update_consumed_materials(self, doctype, return_consumed_items=False):
+ """Deduct the consumed materials from the available materials."""
+
+ receipt_items = self.__get_received_items(doctype)
+ if not receipt_items:
+ return ([], {}) if return_consumed_items else None
+
+ receipt_items = {
+ item.name: item.get(self.subcontract_data.order_field) for item in receipt_items
+ }
+ consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys())
+
+ if return_consumed_items:
+ return (consumed_materials, receipt_items)
+
+ for row in consumed_materials:
+ key = (row.rm_item_code, row.main_item_code, receipt_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, subcontract_order): {
+ 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
+ }
+ }
+ """
+ if not self.subcontract_orders:
+ return
+
+ for row in self.__get_transferred_items():
+ key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
+
+ 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,
+ f"{self.subcontract_data.rm_detail_field}s": [],
+ }
+ ),
+ )
+
+ details = self.available_materials[key]
+ details.qty += row.qty
+ details[f"{self.subcontract_data.rm_detail_field}s"].append(
+ row.get(self.subcontract_data.rm_detail_field)
+ )
+
+ 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)
+ if self.get("is_old_subcontracting_flow"):
+ for doctype in ["Purchase Receipt", "Purchase Invoice"]:
+ self.__update_consumed_materials(doctype)
+ else:
+ 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 == self.subcontract_data.order_doctype:
+ 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.get(self.subcontract_data.order_field))
+ 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,
+ self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field),
+ }
+ )
+
+ 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.get(self.subcontract_data.order_field))
+
+ 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 == self.subcontract_data.order_doctype:
+ rm_obj.required_qty = qty
+ rm_obj.amount = rm_obj.required_qty * rm_obj.rate
+ else:
+ rm_obj.consumed_qty = 0
+ setattr(
+ rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
+ )
+ 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.get(self.subcontract_data.order_field))
+
+ 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 != self.subcontract_data.order_doctype and (
+ (self.__changed_name and row.name not in self.__changed_name)
+ or (has_supplied_items and not self.__changed_name)
+ ):
+ continue
+
+ if self.doctype == self.subcontract_data.order_doctype 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.get(self.subcontract_data.order_field),
+ ) 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.get(self.subcontract_data.order_field))
+ ] -= row.qty
+
+ def __prepare_supplied_items(self):
+ self.initialized_fields()
+ self.__get_subcontract_orders()
+ self.__get_pending_qty_to_receive()
+ self.get_available_materials()
+ self.__remove_changed_rows()
+ self.__set_supplied_items()
+
+ 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(
+ self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
+ )
+ msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {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(
+ self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
+ )
+ msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
+ frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
+
+ def __validate_supplied_items(self):
+ if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
+ return
+
+ for row in self.get(self.raw_material_table):
+ key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
+ 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):
+ 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 create_raw_materials_supplied(self, raw_material_table="supplied_items"):
+ self.set_materials_for_subcontracted_items(raw_material_table)
+
+ if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]:
+ for item in self.get("items"):
+ item.rm_supp_cost = 0.0
+
+ def __update_consumed_qty_in_subcontract_order(self, itemwise_consumed_qty):
+ fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
+ filters = {"docstatus": 1, "parent": ("in", self.subcontract_orders)}
+
+ for row in frappe.get_all(
+ self.subcontract_data.order_supplied_items_field, 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(
+ self.subcontract_data.order_supplied_items_field, row.name, "consumed_qty", consumed_qty
+ )
+
+ def set_consumed_qty_in_subcontract_order(self):
+ # Update consumed qty back in the subcontract order
+ if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"] or self.get(
+ "is_old_subcontracting_flow"
+ ):
+ self.__get_subcontract_orders()
+ itemwise_consumed_qty = defaultdict(float)
+ if self.get("is_old_subcontracting_flow"):
+ doctypes = ["Purchase Receipt", "Purchase Invoice"]
+ else:
+ doctypes = ["Subcontracting Receipt"]
+
+ for doctype in doctypes:
+ consumed_items, receipt_items = self.__update_consumed_materials(
+ doctype, return_consumed_items=True
+ )
+
+ for row in consumed_items:
+ key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name))
+ itemwise_consumed_qty[key] += row.consumed_qty
+
+ self.__update_consumed_qty_in_subcontract_order(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, reset_outgoing_rate=True):
+ supplied_items_cost = 0.0
+ for item in self.get("supplied_items"):
+ if item.reference_name == item_row_id:
+ if (
+ self.get("is_old_subcontracting_flow")
+ and reset_outgoing_rate
+ and frappe.get_cached_value("Item", item.rm_item_code, "is_stock_item")
+ ):
+ rate = get_incoming_rate(
+ {
+ "item_code": item.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1 * item.consumed_qty,
+ "serial_no": item.serial_no,
+ "batch_no": item.batch_no,
+ }
+ )
+
+ if rate > 0:
+ item.rate = rate
+
+ 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_subcontract_orders
+
+ if self.subcontract_orders:
+ for sco in set(self.subcontract_orders):
+ sco_doc = frappe.get_doc("Subcontracting Order", sco)
+ sco_doc.update_status()
+
+ @frappe.whitelist()
+ def get_current_stock(self):
+ if self.doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
+ 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
+
+ @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 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(subcontract_order, rm_items, order_doctype="Subcontracting Order"):
+ 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 subcontract_order:
+ subcontract_order = frappe.get_doc(order_doctype, subcontract_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"
+ if order_doctype == "Purchase Order":
+ stock_entry.purchase_order = subcontract_order.name
+ else:
+ stock_entry.subcontracting_order = subcontract_order.name
+ stock_entry.supplier = subcontract_order.supplier
+ stock_entry.supplier_name = subcontract_order.supplier_name
+ stock_entry.supplier_address = subcontract_order.supplier_address
+ stock_entry.address_display = subcontract_order.address_display
+ stock_entry.company = subcontract_order.company
+ stock_entry.to_warehouse = subcontract_order.supplier_warehouse
+ stock_entry.set_stock_entry_type()
+
+ if order_doctype == "Purchase Order":
+ rm_detail_field = "po_detail"
+ else:
+ rm_detail_field = "sco_rm_detail"
+
+ 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: {
+ rm_detail_field: 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 subcontract_order.name
+
+
+def add_items_in_ste(
+ ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_detail", batch_no=None
+):
+ item = ste_doc.append("items", row.item_details)
+
+ rm_detail = list(set(row.get(f"{rm_detail_field}s")).intersection(rm_details))
+ item.update(
+ {
+ "qty": qty,
+ "batch_no": batch_no,
+ "basic_rate": row.item_details["rate"],
+ rm_detail_field: rm_detail[0] if 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, order_doc, rm_details, order_doctype="Subcontracting Order"
+):
+ ste_doc = frappe.new_doc("Stock Entry")
+ ste_doc.purpose = "Material Transfer"
+
+ if order_doctype == "Purchase Order":
+ ste_doc.purchase_order = order_doc.name
+ rm_detail_field = "po_detail"
+ else:
+ ste_doc.subcontracting_order = order_doc.name
+ rm_detail_field = "sco_rm_detail"
+ ste_doc.company = order_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, rm_details, rm_detail_field, batch_no)
+ else:
+ add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
+
+ ste_doc.set_stock_entry_type()
+ ste_doc.calculate_rate_and_amount()
+
+ return ste_doc
+
+
+@frappe.whitelist()
+def get_materials_from_supplier(
+ subcontract_order, rm_details, order_doctype="Subcontracting Order"
+):
+ if isinstance(rm_details, str):
+ rm_details = json.loads(rm_details)
+
+ doc = frappe.get_cached_doc(order_doctype, subcontract_order)
+ doc.initialized_fields()
+ doc.subcontract_orders = [doc.name]
+ doc.get_available_materials()
+
+ if not doc.available_materials:
+ frappe.throw(
+ _("Materials are already received against the {0} {1}").format(order_doctype, subcontract_order)
+ )
+
+ return make_return_stock_entry_for_subcontract(
+ doc.available_materials, doc, rm_details, order_doctype
+ )
diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py
new file mode 100644
index 0000000..4fab805
--- /dev/null
+++ b/erpnext/controllers/tests/test_subcontracting_controller.py
@@ -0,0 +1,1077 @@
+# 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.controllers.subcontracting_controller import (
+ get_materials_from_supplier,
+ make_rm_stock_entry,
+)
+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 (
+ 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 test_sco_supplied_qty(self):
+ """
+ Check if 'Supplied Qty' in SCO's Supplied Items table is reset on submit/cancel.
+ """
+ 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 = [
+ {"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)
+
+ for item in rm_items:
+ item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name
+
+ se = make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ sco.reload()
+ for item in sco.get("supplied_items"):
+ self.assertIn(item.supplied_qty, [5.0, 6.0])
+
+ se.cancel()
+ sco.reload()
+ for item in sco.get("supplied_items"):
+ self.assertEqual(item.supplied_qty, 0.0)
+
+
+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 860512c..a190cc7 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 a73b9bc..70ccb78 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -508,7 +508,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,
@@ -518,9 +518,6 @@
for field in [
"schedule_date",
"qty",
- "uom",
- "stock_uom",
- "item_name",
"description",
"production_plan_item",
]:
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
index a86c7a4..5083b73 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
@@ -36,7 +36,7 @@
"total_time_in_mins",
]
- for field in ["work_order", "workstation", "operation", "company"]:
+ for field in ["work_order", "workstation", "operation", "status", "company"]:
if filters.get(field):
query_filters[field] = ("in", filters.get(field))
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.txt b/erpnext/patches.txt
index aef80bc..9eaecbd 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -343,6 +343,7 @@
execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.set_payroll_entry_status
erpnext.patches.v13_0.job_card_status_on_hold
+erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.crm_ux_cleanup
erpnext.patches.v14_0.remove_india_localisation
diff --git a/erpnext/patches/v13_0/add_bin_unique_constraint.py b/erpnext/patches/v13_0/add_bin_unique_constraint.py
index 38a8500..7ad2bec 100644
--- a/erpnext/patches/v13_0/add_bin_unique_constraint.py
+++ b/erpnext/patches/v13_0/add_bin_unique_constraint.py
@@ -64,4 +64,8 @@
bin.update(qty_dict)
bin.update_reserved_qty_for_production()
bin.update_reserved_qty_for_sub_contracting()
+ if frappe.db.count(
+ "Purchase Order", {"status": ["!=", "Completed"], "is_old_subcontracting_flow": 1}
+ ):
+ bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order")
bin.db_update()
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/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py
new file mode 100644
index 0000000..607ef695
--- /dev/null
+++ b/erpnext/patches/v14_0/copy_is_subcontracted_value_to_is_old_subcontracting_flow.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+
+
+def execute():
+ for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
+ tab = frappe.qb.DocType(doctype).as_("tab")
+ frappe.qb.update(tab).set(tab.is_old_subcontracting_flow, 1).where(
+ tab.is_subcontracted == 1
+ ).run()
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index a5b7699..09779d8 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -83,9 +83,17 @@
this.frm.set_query("item_code", "items", function() {
if (me.frm.doc.is_subcontracted) {
+ var filters = {'supplier': me.frm.doc.supplier};
+ if (me.frm.doc.is_old_subcontracting_flow) {
+ filters["is_sub_contracted_item"] = 1;
+ }
+ else {
+ filters["is_stock_item"] = 0;
+ }
+
return{
query: "erpnext.controllers.queries.item_query",
- filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 }
+ filters: filters
}
}
else {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 62b98ec..6b618b2 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -470,7 +470,8 @@
cost_center: item.cost_center,
tax_category: me.frm.doc.tax_category,
item_tax_template: item.item_tax_template,
- child_docname: item.name
+ child_docname: item.name,
+ is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
}
},
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 096175a..62abb74 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -486,7 +486,11 @@
filters = {"is_sales_item": 1};
} else if (frm.doc.doctype == 'Purchase Order') {
if (frm.doc.is_subcontracted) {
- filters = {"is_sub_contracted_item": 1};
+ if (frm.doc.is_old_subcontracting_flow) {
+ filters = {"is_sub_contracted_item": 1};
+ } else {
+ filters = {"is_stock_item": 0};
+ }
} else {
filters = {"is_purchase_item": 1};
}
diff --git a/erpnext/selling/doctype/quotation/regional/india.js b/erpnext/selling/doctype/quotation/regional/india.js
deleted file mode 100644
index 9550835..0000000
--- a/erpnext/selling/doctype/quotation/regional/india.js
+++ /dev/null
@@ -1,3 +0,0 @@
-{% include "erpnext/regional/india/taxes.js" %}
-
-erpnext.setup_auto_gst_taxation('Quotation');
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 9ffd6df..bffa829 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -10,8 +10,9 @@
from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import cint, formatdate, get_timestamp, today
-from frappe.utils.nestedset import NestedSet
+from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
@@ -150,9 +151,7 @@
self.create_default_tax_template()
if not frappe.db.get_value("Department", {"company": self.name}):
- from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures
-
- install_post_company_fixtures(frappe._dict({"company_name": self.name}))
+ self.create_default_departments()
if not frappe.local.flags.ignore_chart_of_accounts:
self.set_default_accounts()
@@ -224,6 +223,104 @@
),
)
+ def create_default_departments(self):
+ records = [
+ # Department
+ {
+ "doctype": "Department",
+ "department_name": _("All Departments"),
+ "is_group": 1,
+ "parent_department": "",
+ "__condition": lambda: not frappe.db.exists("Department", _("All Departments")),
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Accounts"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Marketing"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Sales"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Purchase"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Operations"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Production"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Dispatch"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Customer Service"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Human Resources"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Management"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Quality Management"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Research & Development"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ {
+ "doctype": "Department",
+ "department_name": _("Legal"),
+ "parent_department": _("All Departments"),
+ "company": self.name,
+ },
+ ]
+
+ # Make root department with NSM updation
+ make_records(records[:1])
+
+ frappe.local.flags.ignore_update_nsm = True
+ make_records(records)
+ frappe.local.flags.ignore_update_nsm = False
+ rebuild_tree("Department", "parent_department")
+
def validate_coa_input(self):
if self.create_chart_of_accounts_based_on == "Existing Company":
self.chart_of_accounts = None
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 4235e1f..0d329ba 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -12,7 +12,6 @@
)
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import cstr, getdate
-from frappe.utils.nestedset import rebuild_tree
from erpnext.accounts.doctype.account.account import RootNotEditable
from erpnext.regional.address_template.setup import set_up_address_templates
@@ -656,104 +655,6 @@
make_records(records)
-def install_post_company_fixtures(args=None):
- records = [
- # Department
- {
- "doctype": "Department",
- "department_name": _("All Departments"),
- "is_group": 1,
- "parent_department": "",
- },
- {
- "doctype": "Department",
- "department_name": _("Accounts"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Marketing"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Sales"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Purchase"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Operations"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Production"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Dispatch"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Customer Service"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Human Resources"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Management"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Quality Management"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Research & Development"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- {
- "doctype": "Department",
- "department_name": _("Legal"),
- "parent_department": _("All Departments"),
- "company": args.company_name,
- },
- ]
-
- # Make root department with NSM updation
- make_records(records[:1])
-
- frappe.local.flags.ignore_update_nsm = True
- make_records(records[1:])
- frappe.local.flags.ignore_update_nsm = False
- rebuild_tree("Department", "parent_department")
-
-
def install_defaults(args=None):
records = [
# Price Lists
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index bc235d9..548df31 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -40,25 +40,37 @@
self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production))
self.db_set("projected_qty", self.projected_qty)
- def update_reserved_qty_for_sub_contracting(self):
+ def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"):
# reserved qty
- po = frappe.qb.DocType("Purchase Order")
- supplied_item = frappe.qb.DocType("Purchase Order Item Supplied")
+ subcontract_order = frappe.qb.DocType(subcontract_doctype)
+ supplied_item = frappe.qb.DocType(
+ "Purchase Order Item Supplied"
+ if subcontract_doctype == "Purchase Order"
+ else "Subcontracting Order Supplied Item"
+ )
+
+ conditions = (
+ (supplied_item.rm_item_code == self.item_code)
+ & (subcontract_order.name == supplied_item.parent)
+ & (subcontract_order.per_received < 100)
+ & (supplied_item.reserve_warehouse == self.warehouse)
+ & (
+ (
+ (subcontract_order.is_old_subcontracting_flow == 1)
+ & (subcontract_order.status != "Closed")
+ & (subcontract_order.docstatus == 1)
+ )
+ if subcontract_doctype == "Purchase Order"
+ else (subcontract_order.docstatus == 1)
+ )
+ )
reserved_qty_for_sub_contract = (
- frappe.qb.from_(po)
+ frappe.qb.from_(subcontract_order)
.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)
- & (supplied_item.reserve_warehouse == self.warehouse)
- )
+ .where(conditions)
).run()[0][0] or 0.0
se = frappe.qb.DocType("Stock Entry")
@@ -71,23 +83,34 @@
else:
qty_field = se_item.transfer_qty
+ conditions = (
+ (se.docstatus == 1)
+ & (se.purpose == "Send to Subcontractor")
+ & ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code))
+ & (se.name == se_item.parent)
+ & (subcontract_order.docstatus == 1)
+ & (subcontract_order.per_received < 100)
+ & (
+ (
+ (Coalesce(se.purchase_order, "") != "")
+ & (subcontract_order.name == se.purchase_order)
+ & (subcontract_order.is_old_subcontracting_flow == 1)
+ & (subcontract_order.status != "Closed")
+ )
+ if subcontract_doctype == "Purchase Order"
+ else (
+ (Coalesce(se.subcontracting_order, "") != "")
+ & (subcontract_order.name == se.subcontracting_order)
+ )
+ )
+ )
+
materials_transferred = (
frappe.qb.from_(se)
.from_(se_item)
- .from_(po)
+ .from_(subcontract_order)
.select(Sum(qty_field))
- .where(
- (se.docstatus == 1)
- & (se.purpose == "Send to Subcontractor")
- & (Coalesce(se.purchase_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)
- )
+ .where(conditions)
).run()[0][0] or 0.0
if reserved_qty_for_sub_contract > materials_transferred:
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index b8f4803..1996418 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -1,17 +1,16 @@
# 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.subcontracting_controller import 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 +21,9 @@
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
+from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ 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..312c166 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -198,7 +198,7 @@
cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status"))
}
- this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
+ this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_old_subcontracting_flow);
}
make_purchase_invoice() {
@@ -296,10 +296,11 @@
frappe.provide("erpnext.buying");
frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) {
- if (frm.doc.is_subcontracted) {
+ if (frm.doc.is_old_subcontracting_flow) {
erpnext.buying.get_default_bom(frm);
}
- frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
+
+ frm.toggle_reqd("supplier_warehouse", frm.doc.is_old_subcontracting_flow);
});
frappe.ui.form.on('Purchase Receipt Item', {
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 923ceb3..a70415d 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -133,7 +133,8 @@
"transporter_name",
"column_break5",
"lr_no",
- "lr_date"
+ "lr_date",
+ "is_old_subcontracting_flow"
],
"fields": [
{
@@ -442,7 +443,8 @@
"label": "Is Subcontracted",
"oldfieldname": "is_subcontracted",
"oldfieldtype": "Select",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"depends_on": "eval:doc.is_subcontracted",
@@ -1142,13 +1144,21 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_old_subcontracting_flow",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Old Subcontracting Flow",
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-05-27 15:59:18.550583",
+ "modified": "2022-06-15 15:43:40.664382",
"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 704e1fc..84da3cc 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -123,6 +123,7 @@
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
+ self.get_current_stock()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
@@ -234,7 +235,7 @@
self.make_gl_entries()
self.repost_future_sle_and_gle()
- self.set_consumed_qty_in_po()
+ self.set_consumed_qty_in_subcontract_order()
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
@@ -270,18 +271,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):
- 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
+ self.set_consumed_qty_in_subcontract_order()
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 be4f274..d0d115d 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 b45d663..c97dbee 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -645,12 +645,15 @@
"print_hide": 1
},
{
+ "depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
"no_copy": 1,
"options": "BOM",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1,
+ "read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
},
{
"default": "0",
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index b1017d2..7c57ecd 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -138,6 +138,11 @@
doc.set_status("Completed")
except Exception as e:
+ if frappe.flags.in_test:
+ # Don't silently fail in tests,
+ # there is no reason for reposts to fail in CI
+ raise
+
frappe.db.rollback()
traceback = frappe.get_traceback()
doc.log_error("Unable to repost item valuation")
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..1c514a9 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -613,7 +613,25 @@
apply_putaway_rule: function (frm) {
if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose);
- }
+ },
+
+ purchase_order: (frm) => {
+ if (frm.doc.purchase_order) {
+ frm.set_value("subcontracting_order", "");
+ }
+ },
+
+ subcontracting_order: (frm) => {
+ if (frm.doc.subcontracting_order) {
+ frm.set_value("purchase_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,
+ });
+ }
+ },
});
frappe.ui.form.on('Stock Entry Detail', {
@@ -780,7 +798,16 @@
return {
"filters": {
"docstatus": 1,
- "is_subcontracted": 1,
+ "is_old_subcontracting_flow": 1,
+ "company": me.frm.doc.company
+ }
+ };
+ });
+
+ this.frm.set_query("subcontracting_order", function() {
+ return {
+ "filters": {
+ "docstatus": 1,
"company": me.frm.doc.company
}
};
@@ -801,7 +828,12 @@
}
}
- this.frm.add_fetch("purchase_order", "supplier", "supplier");
+ if (me.frm.doc.purchase_order) {
+ this.frm.add_fetch("purchase_order", "supplier", "supplier");
+ }
+ else {
+ 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..abe98e2 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"
+ "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "label": "Purchase Order",
+ "options": "Purchase Order"
},
{
+ "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 46a1e70..9c49408 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -62,6 +62,27 @@
class StockEntry(StockController):
+ def __init__(self, *args, **kwargs):
+ super(StockEntry, self).__init__(*args, **kwargs)
+ if self.purchase_order:
+ self.subcontract_data = frappe._dict(
+ {
+ "order_doctype": "Purchase Order",
+ "order_field": "purchase_order",
+ "rm_detail_field": "po_detail",
+ "order_supplied_items_field": "Purchase Order Item Supplied",
+ }
+ )
+ else:
+ self.subcontract_data = frappe._dict(
+ {
+ "order_doctype": "Subcontracting Order",
+ "order_field": "subcontracting_order",
+ "rm_detail_field": "sco_rm_detail",
+ "order_supplied_items_field": "Subcontracting Order Supplied Item",
+ }
+ )
+
def get_feed(self):
return self.stock_entry_type
@@ -134,8 +155,9 @@
update_serial_nos_after_submit(self, "items")
self.update_work_order()
- self.validate_purchase_order()
- self.update_purchase_order_supplied_items()
+ self.validate_subcontract_order()
+ self.update_subcontract_order_supplied_items()
+ self.update_subcontracting_order_status()
self.make_gl_entries()
@@ -154,7 +176,8 @@
self.set_material_request_transfer_status("Completed")
def on_cancel(self):
- self.update_purchase_order_supplied_items()
+ self.update_subcontract_order_supplied_items()
+ self.update_subcontracting_order_status()
if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status()
@@ -792,8 +815,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_subcontract_order(self):
+ """Throw exception if more raw material is transferred against Subcontract 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"
@@ -801,24 +824,29 @@
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.get(self.subcontract_data.order_field)):
return
if backflush_raw_materials_based_on == "BOM":
- purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
+ subcontract_order = frappe.get_doc(
+ self.subcontract_data.order_doctype, self.get(self.subcontract_data.order_field)
+ )
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 subcontract_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},
+ f"{self.subcontract_data.order_doctype} Item",
+ {
+ "parent": self.get(self.subcontract_data.order_field),
+ "item_code": se_item.subcontracted_item,
+ },
"bom",
)
@@ -830,7 +858,7 @@
required_qty = sum(
[
flt(d.required_qty)
- for d in purchase_order.supplied_items
+ for d in subcontract_order.supplied_items
if d.rm_item_code == original_item_code
]
)
@@ -839,26 +867,57 @@
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 {1} {2}").format(
+ se_item.item_code,
+ self.subcontract_data.order_doctype,
+ self.get(self.subcontract_data.order_field),
)
)
- 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")
+
+ conditions = (
+ (parent.docstatus == 1)
+ & (child.item_code == se_item.item_code)
+ & (
+ (parent.purchase_order == self.purchase_order)
+ if self.subcontract_data.order_doctype == "Purchase Order"
+ else (parent.subcontracting_order == self.subcontracting_order)
+ )
+ )
+
+ total_supplied = (
+ frappe.qb.from_(parent)
+ .inner_join(child)
+ .on(parent.name == child.parent)
+ .select(Sum(child.transfer_qty))
+ .where(conditions)
+ ).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 {3} {4}").format(
+ se_item.idx,
+ se_item.item_code,
+ total_allowed,
+ self.subcontract_data.order_doctype,
+ self.get(self.subcontract_data.order_field),
)
)
+ elif not se_item.get(self.subcontract_data.rm_detail_field):
+ filters = {
+ "parent": self.get(self.subcontract_data.order_field),
+ "docstatus": 1,
+ "rm_item_code": se_item.item_code,
+ "main_item_code": se_item.subcontracted_item,
+ }
+
+ order_rm_detail = frappe.db.get_value(
+ self.subcontract_data.order_supplied_items_field, filters, "name"
+ )
+ if order_rm_detail:
+ se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail)
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
for row in self.items:
if not row.subcontracted_item:
@@ -867,17 +926,19 @@
row.idx, frappe.bold(row.item_code)
)
)
- elif not row.po_detail:
+ elif not row.get(self.subcontract_data.rm_detail_field):
filters = {
- "parent": self.purchase_order,
+ "parent": self.get(self.subcontract_data.order_field),
"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)
+ order_rm_detail = frappe.db.get_value(
+ self.subcontract_data.order_supplied_items_field, filters, "name"
+ )
+ if order_rm_detail:
+ row.db_set(self.subcontract_data.rm_detail_field, order_rm_detail)
def validate_bom(self):
for d in self.get("items"):
@@ -1224,11 +1285,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(self.subcontract_data.order_field)
+ 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")},
+ self.subcontract_data.order_supplied_items_field,
+ {"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")},
"main_item_code",
)
@@ -1322,27 +1385,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 Subcontract Order Supplied Items Details
+ if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
+ # Get Subcontract Order Supplied Items Details
+ parent = frappe.qb.DocType(self.subcontract_data.order_doctype)
+ child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field)
+
+ 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.get(self.subcontract_data.order_field))
+ ).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 Subcontract Order
+ if self.get(self.subcontract_data.order_field) 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 ""
@@ -1478,7 +1541,9 @@
fetch_qty_in_stock_uom=False,
)
- used_alternative_items = get_used_alternative_items(work_order=self.work_order)
+ used_alternative_items = get_used_alternative_items(
+ subcontract_order_field=self.subcontract_data.order_field, work_order=self.work_order
+ )
for item in item_dict.values():
# if source warehouse presents in BOM set from_warehouse as bom source_warehouse
if item["allow_alternative_item"]:
@@ -1844,7 +1909,7 @@
se_child.is_process_loss = item_row.get("is_process_loss", 0)
for field in [
- "po_detail",
+ self.subcontract_data.rm_detail_field,
"original_item",
"expense_account",
"description",
@@ -1918,33 +1983,37 @@
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 (
+ def update_subcontract_order_supplied_items(self):
+ if self.get(self.subcontract_data.order_field) and (
self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
):
- # Get PO Supplied Items Details
- po_supplied_items = frappe.db.get_all(
- "Purchase Order Item Supplied",
- filters={"parent": self.purchase_order},
+ # Get Subcontract Order Supplied Items Details
+ order_supplied_items = frappe.db.get_all(
+ self.subcontract_data.order_supplied_items_field,
+ filters={"parent": self.get(self.subcontract_data.order_field)},
fields=["name", "rm_item_code", "reserve_warehouse"],
)
- # Get Items Supplied in Stock Entries against PO
- supplied_items = get_supplied_items(self.purchase_order)
+ # Get Items Supplied in Stock Entries against Subcontract Order
+ supplied_items = get_supplied_items(
+ self.get(self.subcontract_data.order_field),
+ self.subcontract_data.rm_detail_field,
+ self.subcontract_data.order_field,
+ )
- for row in po_supplied_items:
+ for row in order_supplied_items:
key, item = row.name, {}
if not supplied_items.get(key):
- # no stock transferred against PO Supplied Items row
+ # no stock transferred against Subcontract Order Supplied Items row
item = {"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}
else:
item = supplied_items.get(key)
- frappe.db.set_value("Purchase Order Item Supplied", row.name, item)
+ frappe.db.set_value(self.subcontract_data.order_supplied_items_field, row.name, item)
# RM Item-Reserve Warehouse Dict
- item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in po_supplied_items}
+ item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in order_supplied_items}
for d in self.get("items"):
# Update reserved sub contracted quantity in bin based on Supplied Item Details and
@@ -2145,6 +2214,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()
@@ -2293,13 +2370,13 @@
return operating_cost_per_unit
-def get_used_alternative_items(purchase_order=None, work_order=None):
+def get_used_alternative_items(
+ subcontract_order=None, subcontract_order_field="subcontracting_order", work_order=None
+):
cond = ""
- if purchase_order:
- cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(
- purchase_order
- )
+ if subcontract_order:
+ cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'"
elif work_order:
cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
work_order
@@ -2352,7 +2429,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")
@@ -2436,25 +2512,27 @@
return sample_quantity
-def get_supplied_items(purchase_order):
+def get_supplied_items(
+ subcontract_order, rm_detail_field="sco_rm_detail", subcontract_order_field="subcontracting_order"
+):
fields = [
"`tabStock Entry Detail`.`transfer_qty`",
"`tabStock Entry`.`is_return`",
- "`tabStock Entry Detail`.`po_detail`",
+ f"`tabStock Entry Detail`.`{rm_detail_field}`",
"`tabStock Entry Detail`.`item_code`",
]
filters = [
["Stock Entry", "docstatus", "=", 1],
- ["Stock Entry", "purchase_order", "=", purchase_order],
+ ["Stock Entry", subcontract_order_field, "=", subcontract_order],
]
supplied_item_details = {}
for row in frappe.get_all("Stock Entry", fields=fields, filters=filters):
- if not row.po_detail:
+ if not row.get(rm_detail_field):
continue
- key = row.po_detail
+ key = row.get(rm_detail_field)
if key not in supplied_item_details:
supplied_item_details.setdefault(
key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0})
@@ -2474,6 +2552,39 @@
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
+
+
def get_available_materials(work_order) -> dict:
data = get_stock_entry_data(work_order)
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 d758c8a..5fe11a2 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",
@@ -497,6 +498,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 f669e90..1410da5 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
@@ -409,61 +409,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 38ad662..e83182f 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -238,8 +238,13 @@
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"):
+ if args.get("is_old_subcontracting_flow"):
+ if item.is_sub_contracted_item != 1:
+ throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
+ else:
+ if 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 01c5aa9..b1842e7 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -630,6 +630,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 (
@@ -646,6 +647,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"
@@ -659,6 +662,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"
@@ -684,6 +689,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)
@@ -732,6 +739,14 @@
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)
actual_qty = flt(sle.actual_qty)
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..dbd337a
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js
@@ -0,0 +1,328 @@
+// 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,
+ is_old_subcontracting_flow: 0
+ }
+ };
+ });
+
+ 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.controllers.subcontracting_controller.get_materials_from_supplier',
+ freeze: true,
+ freeze_message: __('Creating Stock Entry'),
+ args: {
+ subcontract_order: frm.doc.name,
+ rm_details: sco_rm_details,
+ order_doctype: cur_frm.doc.doctype
+ },
+ 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.controllers.subcontracting_controller.make_rm_stock_entry',
+ args: {
+ subcontract_order: cur_frm.doc.name,
+ rm_items: rm_items,
+ order_doctype: cur_frm.doc.doctype
+ },
+ 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..71cdc94
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -0,0 +1,246 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.mapper import get_mapped_doc
+from frappe.utils import flt
+
+from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
+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:
+ if is_subcontracting_order_created(self.purchase_order):
+ frappe.throw(
+ _(
+ "Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one."
+ )
+ )
+
+ 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.is_old_subcontracting_flow:
+ frappe.throw(_("Please select a valid Purchase Order that has Service Items."))
+
+ 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"
+ else:
+ status = "Open"
+ elif self.docstatus == 2:
+ status = "Cancelled"
+
+ 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
+
+
+@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..94bb38e
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -0,0 +1,536 @@
+# 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.subcontracting_controller import make_rm_stock_entry
+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 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")
+
+ # Partially Received (scr cancelled)
+ scr.load_from_db()
+ scr.cancel()
+ sco.load_from_db()
+ self.assertEqual(sco.status, "Partially Received")
+
+ 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..0c4ec6f
--- /dev/null
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -0,0 +1,188 @@
+# 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, 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_subcontract_order()
+ 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_subcontract_order()
+ 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
+
+ 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..763e768
--- /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.controllers.subcontracting_controller 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.controllers.subcontracting_controller import make_rm_stock_entry
+ from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
+ 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/templates/pages/projects.html b/erpnext/templates/pages/projects.html
index 76eaf75..6d366c5 100644
--- a/erpnext/templates/pages/projects.html
+++ b/erpnext/templates/pages/projects.html
@@ -5,7 +5,7 @@
{% endblock %}
{% block head_include %}
- <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
+ <link rel="stylesheet" href="/assets/frappe/css/fonts/fontawesome/font-awesome.min.css">
{% endblock %}
{% block header %}
diff --git a/erpnext/templates/pages/search_help.py b/erpnext/templates/pages/search_help.py
index a6877ce..d158167 100644
--- a/erpnext/templates/pages/search_help.py
+++ b/erpnext/templates/pages/search_help.py
@@ -1,9 +1,9 @@
import frappe
import requests
from frappe import _
+from frappe.core.utils import html2text
from frappe.utils import sanitize_html
from frappe.utils.global_search import search
-from html2text import html2text
from jinja2 import utils
diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py
deleted file mode 100644
index 93d1c8e..0000000
--- a/erpnext/tests/test_subcontracting.py
+++ /dev/null
@@ -1,1114 +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 test_po_supplied_qty(self):
- """
- Check if 'Supplied Qty' in PO's Supplied Items table is reset on submit/cancel.
- """
- 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
-
- se = make_stock_transfer_entry(
- po_no=po.name, rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details)
- )
-
- po.reload()
- for row in po.get("supplied_items"):
- self.assertIn(row.supplied_qty, [5.0, 6.0])
-
- se.cancel()
- po.reload()
- for row in po.get("supplied_items"):
- self.assertEqual(row.supplied_qty, 0.0)
-
-
-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
- )
diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv
index a4bfb86..ac680da 100644
--- a/erpnext/translations/ru.csv
+++ b/erpnext/translations/ru.csv
@@ -408,7 +408,7 @@
Bonus Payment Date cannot be a past date,Дата выплаты бонуса не может быть прошлой датой,
Both Trial Period Start Date and Trial Period End Date must be set,"Должны быть установлены как дата начала пробного периода, так и дата окончания пробного периода",
Both Warehouse must belong to same Company,Оба Склад должены принадлежать одной Компании,
-Branch,Ветвь,
+Branch,Филиал,
Broadcasting,Вещание,
Brokerage,Посредничество,
Browse BOM,Просмотр спецификации,
@@ -563,7 +563,7 @@
Commission,Комиссионный сбор,
Commission Rate %,Ставка комиссии %,
Commission on Sales,Комиссия по продажам,
-Commission rate cannot be greater than 100,"Скорость Комиссия не может быть больше, чем 100",
+Commission rate cannot be greater than 100,"Стоимость комиссии не может быть больше, чем 100",
Community Forum,Форум,
Company (not Customer or Supplier) master.,Компания (не клиента или поставщика) хозяин.,
Company Abbreviation,Аббревиатура компании,
@@ -1066,7 +1066,7 @@
For Quantity (Manufactured Qty) is mandatory,Для Количество (Изготовитель Количество) является обязательным,
For Supplier,Для поставщиков,
For Warehouse,Для склада,
-For Warehouse is required before Submit,Для Склада является обязательным полем для проведения,
+For Warehouse is required before Submit,Для склада - обязательное полем для проводки,
"For an item {0}, quantity must be negative number",Для элемента {0} количество должно быть отрицательным числом,
"For an item {0}, quantity must be positive number",Для элемента {0} количество должно быть положительным числом,
"For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry",Для карты задания {0} вы можете только сделать запись запаса типа 'Передача материала для производства',
@@ -1498,7 +1498,7 @@
Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule',"График обслуживания не генерируется для всех элементов. Пожалуйста, нажмите на кнопку ""Generate Расписание""",
Maintenance Schedule {0} exists against {1},График обслуживания {0} существует против {1},
Maintenance Schedule {0} must be cancelled before cancelling this Sales Order,График Обслуживания {0} должен быть отменен до отмены этой Сделки,
-Maintenance Status has to be Cancelled or Completed to Submit,Статус обслуживания должен быть отменен или завершен для отправки,
+Maintenance Status has to be Cancelled or Completed to Submit,Статус обслуживания должен быть отменен или завершен для проводки,
Maintenance User,Сотрудник обслуживания,
Maintenance Visit,Заявки на техническое обслуживание,
Maintenance Visit {0} must be cancelled before cancelling this Sales Order,Посещение по Обслуживанию {0} должно быть отменено до отмены этой Сделки,
@@ -1683,7 +1683,7 @@
No Delivery Note selected for Customer {},Нет примечания о доставке для клиента {},
No Employee Found,Сотрудник не найден,
No Item with Barcode {0},Нет продукта со штрих-кодом {0},
-No Item with Serial No {0},Нет продукта с серийным № {0},
+No Item with Serial No {0},Нет продукта с серийным номером {0},
No Items available for transfer,Нет доступных продуктов для перемещения,
No Items selected for transfer,Не выбраны продукты для перемещения,
No Items to pack,Нет продуктов для упаковки,
@@ -2807,8 +2807,8 @@
Stock UOM,Единица измерения запасов,
Stock Value,Стоимость акций,
Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3},Для продукта {2} на складе {3} остатки запасов для партии {0} станут отрицательными {1},
-Stock cannot be updated against Delivery Note {0},Фото не могут быть обновлены против накладной {0},
-Stock cannot be updated against Purchase Receipt {0},Фото не может быть обновлен с квитанцией о покупке {0},
+Stock cannot be updated against Delivery Note {0},Запасы не могут быть обновлены против накладной {0},
+Stock cannot be updated against Purchase Receipt {0},Запасы не может быть обновлен с квитанцией о покупке {0},
Stock cannot exist for Item {0} since has variants,Запасов продукта {0} не существует с момента появления вариантов,
Stock transactions before {0} are frozen,Перемещения по складу до {0} заморожены,
Stop,Стоп,
@@ -2845,12 +2845,12 @@
Sub-contracting,Суб-контракты,
Subcontract,Субподряд,
Subject,Тема,
-Submit,Провести,
+Submit,Утвердить,
Submit Proof,Отправить подтверждение,
-Submit Salary Slip,Провести Зарплатную ведомость,
-Submit this Work Order for further processing.,Отправьте этот рабочий заказ для дальнейшей обработки.,
-Submit this to create the Employee record,"Отправьте это, чтобы создать запись сотрудника",
-Submitting Salary Slips...,Отправка зарплатных листов ...,
+Submit Salary Slip,Утведрдить зарплатную ведомость,
+Submit this Work Order for further processing.,Утвердите этот рабочий заказ для дальнейшей обработки.,
+Submit this to create the Employee record,"Утвердите это, чтобы создать запись сотрудника",
+Submitting Salary Slips...,Проводка зарплатных ведомостей...,
Subscription,Подписка,
Subscription Management,Управление подпиской,
Subscriptions,Подписки,
@@ -3187,7 +3187,7 @@
Update Response,Обновить ответ,
Update bank payment dates with journals.,Обновление банк платежные даты с журналов.,
Update in progress. It might take a while.,Идет обновление. Это может занять некоторое время.,
-Update rate as per last purchase,Скорость обновления согласно последней покупке,
+Update rate as per last purchase,Обновлять стоимость согласно последней покупке,
Update stock must be enable for the purchase invoice {0},Обновление запасов должно быть включено для счета на покупку {0},
Updating Variants...,Обновление вариантов...,
Upload your letter head and logo. (you can edit them later).,Загрузить шапку фирменного бланка и логотип. (Вы можете отредактировать их позднее).,
@@ -3310,7 +3310,7 @@
Work Order {0} must be submitted,Порядок работы {0} должен быть отправлен,
Work Orders Created: {0},Созданы рабочие задания: {0},
Work Summary for {0},Резюме работы для {0},
-Work-in-Progress Warehouse is required before Submit,Работа-в-Прогресс Склад требуется перед Отправить,
+Work-in-Progress Warehouse is required before Submit,Перед утверждением требуется склад незавершенного производства,
Workflow,Рабочий процесс,
Working,В работе,
Working Hours,Часы работы,
@@ -3843,7 +3843,7 @@
Mobile Number,Мобильный номер,
Month,Mесяц,
Name,Имя,
-Near you,Возле тебя,
+Near you,Возле вас,
Net Profit/Loss,Чистая прибыль / убыток,
New Expense,Новый расход,
New Invoice,Новый счет,
@@ -3851,8 +3851,8 @@
New release date should be in the future,Дата нового релиза должна быть в будущем,
Newsletter,Рассылка новостей,
No Account matched these filters: {},"Нет аккаунта, соответствующего этим фильтрам: {}",
-No Employee found for the given employee field value. '{}': {},Сотрудник не найден для данного значения поля сотрудника. '{}': {},
-No Leaves Allocated to Employee: {0} for Leave Type: {1},Сотрудникам не выделено ни одного листа: {0} для типа отпуска: {1},
+No Employee found for the given employee field value. '{}': {},Сотрудник не найден для данного значения поля сотрудника. '{}': {},
+No Leaves Allocated to Employee: {0} for Leave Type: {1},Сотруднику не назначен отпуск: {0} для типа отпуска: {1},
No communication found.,Связь не найдена.,
No correct answer is set for {0},Не указан правильный ответ для {0},
No description,Без описания,
@@ -3883,8 +3883,8 @@
Only users with the {0} role can create backdated leave applications,Только пользователи с ролью {0} могут создавать оставленные приложения с задним сроком действия,
Open,Открыт,
Open Contact,Открытый контакт,
-Open Lead,Открытое обращение,
-Opening and Closing,Открытие и Закрытие,
+Open Lead,Открытый лид,
+Opening and Closing,Открытие и закрытие,
Operating Cost as per Work Order / BOM,Эксплуатационные расходы согласно заказу на работу / спецификации,
Order Amount,Сумма заказа,
Page {0} of {1},Страница {0} из {1},
@@ -4072,7 +4072,7 @@
Stores - {0},Магазины - {0},
Student with email {0} does not exist,Студент с электронной почтой {0} не существует,
Submit Review,Добавить отзыв,
-Submitted,Проведенный,
+Submitted,Утвержден,
Supplier Addresses And Contacts,Адреса и контакты поставщика,
Synchronize this account,Синхронизировать этот аккаунт,
Tag,Тег,
@@ -4113,7 +4113,7 @@
Total Late Entries,Всего поздних заявок,
Total Payment Request amount cannot be greater than {0} amount,Общая сумма запроса платежа не может превышать сумму {0},
Total payments amount can't be greater than {},Общая сумма платежей не может быть больше {},
-Totals,Всего:,
+Totals,Всего,
Training Event:,Учебное мероприятие:,
Transactions already retreived from the statement,Транзакции уже получены из заявления,
Transfer Material to Supplier,Перевести Материал Поставщику,
@@ -4235,7 +4235,7 @@
Budget,Бюджет,
Chart of Accounts,План счетов,
Customer database.,База данных клиентов.,
-Days Since Last order,Дни с последнего Заказать,
+Days Since Last order,Дней с момента последнего заказа,
Download as JSON,Скачать как JSON,
End date can not be less than start date,"Дата окончания не может быть меньше, чем Дата начала",
For Default Supplier (Optional),Поставщик по умолчанию (необязательно),
@@ -4253,7 +4253,7 @@
Not in stock,Нет в наличии,
Not permitted,Не разрешено,
Open Issues ,Открытые вопросы ,
-Open Projects ,Открытые проекты,
+Open Projects ,Открытые проекты ,
Open To Do ,Открыть список задач ,
Operation Id,Код операции,
Partially ordered,Частично заказанно,
@@ -4304,9 +4304,9 @@
Assets not created for {0}. You will have to create asset manually.,Активы не созданы для {0}. Вам придется создать актив вручную.,
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} имеет бухгалтерские записи в валюте {2} для компании {3}. Выберите счет дебиторской или кредиторской задолженности с валютой {2}.,
Invalid Account,Неверный аккаунт,
-Purchase Order Required,"Покупка порядке, предусмотренном",
-Purchase Receipt Required,Покупка Получение необходимое,
-Account Missing,Аккаунт отсутствует,
+Purchase Order Required,Требуется заказ на покупку,
+Purchase Receipt Required,Требуется чек о покупке,
+Account Missing,Счет отсутствует,
Requested,Запрошено,
Partially Paid,Частично оплачено,
Invalid Account Currency,Неверная валюта счета,
@@ -4349,15 +4349,15 @@
Valid From date not in Fiscal Year {0},Дата начала действия не в финансовом году {0},
Valid Upto date not in Fiscal Year {0},Действительно до даты не в финансовом году {0},
Group Roll No,Групповой опрос №,
-Maintain Same Rate Throughout Sales Cycle,Поддержание же скоростью протяжении цикла продаж,
+Maintain Same Rate Throughout Sales Cycle,Поддержание одинаковой ставки на протяжении всего цикла продаж,
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.","Строка {1}: количество ({0}) не может быть дробью. Чтобы разрешить это, отключите '{2}' в единицах измерения {3}.",
Must be Whole Number,Должно быть целое число,
Please setup Razorpay Plan ID,Настройте идентификатор плана Razorpay,
Contact Creation Failed,Не удалось создать контакт,
{0} already exists for employee {1} and period {2},{0} уже существует для сотрудника {1} и периода {2},
-Leaves Allocated,Распределенные листья,
-Leaves Expired,Листья просрочены,
-Leave Without Pay does not match with approved {} records,"Leave Without Pay" не совпадает с утвержденными записями: {},
+Leaves Allocated,Распределенные отпуска,
+Leaves Expired,Отпуска просрочены,
+Leave Without Pay does not match with approved {} records,Leave Without Pay не совпадает с утвержденными записями: {},
Income Tax Slab not set in Salary Structure Assignment: {0},Плита подоходного налога не указана в назначении структуры заработной платы: {0},
Income Tax Slab: {0} is disabled,Плита подоходного налога: {0} отключена,
Income Tax Slab must be effective on or before Payroll Period Start Date: {0},Таблица подоходного налога должна вступить в силу не позднее даты начала периода расчета зарплаты: {0},
@@ -4375,7 +4375,7 @@
Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same,Строка {0}: Delivery Warehouse ({1}) и Customer Warehouse ({2}) не могут совпадать.,
Row {0}: Due Date in the Payment Terms table cannot be before Posting Date,Строка {0}: Дата платежа в таблице условий оплаты не может быть раньше даты публикации.,
Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.,Не удается найти {} для элемента {}. Установите то же самое в Мастер предметов или Настройки запасов.,
-Row #{0}: The batch {1} has already expired.,Строка № {0}: срок действия пакета {1} уже истек.,
+Row #{0}: The batch {1} has already expired.,Строка №{0}: срок действия пакета {1} уже истек.,
Start Year and End Year are mandatory,Год начала и год окончания являются обязательными,
GL Entry,БК запись,
Cannot allocate more than {0} against payment term {1},Невозможно выделить более {0} на срок платежа {1},
@@ -4529,7 +4529,7 @@
Show Payment Schedule in Print,Показать график платежей в печати,
Currency Exchange Settings,Настройки обмена валюты,
Allow Stale Exchange Rates,Разрешить статичные обменные курсы,
-Stale Days,Прошлые дни,
+Stale Days,Прошедшие дни,
Report Settings,Настройки отчета,
Use Custom Cash Flow Format,Использовать формат пользовательского денежного потока,
Allowed To Transact With,Разрешено спрятать,
@@ -4628,16 +4628,16 @@
Budget Accounts,Счета бюджета,
Budget Account,Бюджет аккаунта,
Budget Amount,Сумма бюджета,
-C-Form,C-образный,
+C-Form,C-Форма,
ACC-CF-.YYYY.-,ACC-CF-.YYYY.-,
-C-Form No,C-образный Нет,
+C-Form No,C-Форма №,
Received Date,Дата получения,
Quarter,Квартал,
I,I,
II,II,
III,III,
IV,IV,
-C-Form Invoice Detail,C-образный Счет Подробно,
+C-Form Invoice Detail,C-Форма детали счета,
Invoice No,Номер cчета,
Cash Flow Mapper,Диспетчер денежных потоков,
Section Name,Название раздела,
@@ -4739,7 +4739,7 @@
CGST Account,CGST счет,
SGST Account,SGST счет,
IGST Account,IGST счет,
-CESS Account,CESS-аккаунт,
+CESS Account,CESS-счет,
Loan Start Date,Дата начала займа,
Loan Period (Days),Срок кредитования (дни),
Loan End Date,Дата окончания займа,
@@ -4806,15 +4806,15 @@
Collection Rules,Правила сбора,
Redemption,Выплата,
Conversion Factor,Коэффициент конверсии,
-1 Loyalty Points = How much base currency?,1 Бонусные баллы = Сколько базовой валюты?,
+1 Loyalty Points = How much base currency?,1 балл лояльности = Сколько базовой валюты?,
Expiry Duration (in days),Продолжительность действия (в днях),
Help Section,Раздел справки,
Loyalty Program Help,Помощь в программе лояльности,
Loyalty Program Collection,Коллекция программы лояльности,
Tier Name,Название уровня,
Minimum Total Spent,Минимальные общие затраты,
-Collection Factor (=1 LP),Коэффициент сбора (=1 Балл),
-For how much spent = 1 Loyalty Point,За сколько потраченных = 1 Балл лояльности,
+Collection Factor (=1 LP),Коэффициент сбора (=1 БЛ),
+For how much spent = 1 Loyalty Point,За сколько потрачено = 1 Балл лояльности,
Mode of Payment Account,Форма оплаты счета,
Default Account,По умолчанию учетная запись,
Default account will be automatically updated in POS Invoice when this mode is selected.,"Учетная запись по умолчанию будет автоматически обновляться в POS-счете, если выбран этот режим.",
@@ -5159,7 +5159,7 @@
Qty as per Stock UOM,Кол-во в соответствии с ед.измерения запасов,
Discount and Margin,Скидка и маржа,
Rate With Margin,Оценить с маржой,
-Discount (%) on Price List Rate with Margin,Скидка (%) на цену Прейскурант с маржой,
+Discount (%) on Price List Rate with Margin,Скидка (%) на цену из прайс-листа с маржой,
Rate With Margin (Company Currency),Ставка с маржей (в валюте компании),
Delivered By Supplier,Доставлено поставщиком,
Deferred Revenue,Отложенный доход,
@@ -5505,9 +5505,9 @@
Purchase Order Item Supplied,Заказ товара Поставляется,
BOM Detail No,Подробности спецификации №,
Stock Uom,Единица измерения запасов,
-Raw Material Item Code,Код сырьевой позиции,
+Raw Material Item Code,Код исходного материала,
Supplied Qty,Поставляемое кол-во,
-Purchase Receipt Item Supplied,Покупка Получение товара Поставляется,
+Purchase Receipt Item Supplied,Квитанция о покупке предоставлена,
Current Stock,Наличие на складе,
PUR-RFQ-.YYYY.-,PUR-RFQ-.YYYY.-,
For individual supplier,Для индивидуального поставщика,
@@ -5541,7 +5541,7 @@
Mention if non-standard payable account,"Упомяните, если нестандартный подлежащий оплате счет",
Default Tax Withholding Config,Конфигурация удержания налога по умолчанию,
Supplier Details,Подробная информация о поставщике,
-Statutory info and other general information about your Supplier,Уставный информации и другие общие сведения о вашем Поставщик,
+Statutory info and other general information about your Supplier,Правовая информация и другие общие сведения о вашем поставщике,
PUR-SQTN-.YYYY.-,PUR-SQTN-.YYYY.-,
Supplier Address,Адрес поставщика,
Link to material requests,Ссылка на заявки на материалы,
@@ -5569,7 +5569,7 @@
Notify Employee,Уведомить сотрудника,
Supplier Scorecard Criteria,Критерии оценки поставщиков,
Criteria Name,Название критерия,
-Max Score,Макс. Оценка,
+Max Score,Макс. оценка,
Criteria Formula,Формула критериев,
Criteria Weight,Вес критериев,
Supplier Scorecard Period,Период оценки поставщика,
@@ -5752,7 +5752,7 @@
Examiner Name,Имя экзаменатора,
Supervisor,Руководитель,
Supervisor Name,Имя супервизора,
-Evaluate,оценивать,
+Evaluate,Оценивать,
Maximum Assessment Score,Максимальный балл оценки,
Assessment Plan Criteria,Критерии оценки плана,
Maximum Score,Максимальный балл,
@@ -6179,7 +6179,7 @@
HLC-PRAC-.YYYY.-,HLC-PRAC-.YYYY.-,
Mobile,Мобильный,
Phone (R),Телефон (R),
-Phone (Office),Телефон(офисный),
+Phone (Office),Телефон (офис),
Employee and User Details,Сведения о сотруднике и пользователе,
Hospital,Больница,
Appointments,Назначения,
@@ -6198,7 +6198,7 @@
Occupied,Занято,
Item Details,Детальная информация о продукте,
UOM Conversion in Hours,Преобразование UOM в часы,
-Rate / UOM,Скорость / UOM,
+Rate / UOM,Стоимость / UOM,
Change in Item,Изменение продукта,
Out Patient Settings,Настройки пациента,
Patient Name By,Имя пациента,
@@ -6266,7 +6266,7 @@
Lab Prescription,Лабораторный рецепт,
Lab Test Name,Название лабораторного теста,
Test Created,Тест создан,
-Submitted Date,Дата отправки,
+Submitted Date,Дата утверждения,
Approved Date,Утвержденная дата,
Sample ID,Образец,
Lab Technician,Лаборант,
@@ -6475,7 +6475,7 @@
Explanation,Объяснение,
Compensatory Leave Request,Компенсационный отпуск,
Leave Allocation,Распределение отпусков,
-Worked On Holiday,Работал на отдыхе,
+Worked On Holiday,Работал на праздниках,
Work From Date,Работа с даты,
Work End Date,Дата окончания работы,
Email Sent To,Email отправлен,
@@ -6486,7 +6486,7 @@
email,Эл. адрес,
Parent Department,Родительский отдел,
Leave Block List,Оставьте список есть,
-Days for which Holidays are blocked for this department.,"Дни, для которых Праздники заблокированные для этого отдела.",
+Days for which Holidays are blocked for this department.,"Дни, для которые праздники заблокированные для этого отдела.",
Leave Approver,Подтверждение отпусков,
Expense Approver,Подтверждающий расходы,
Department Approver,Подтверждение департамента,
@@ -6805,7 +6805,7 @@
Encashment Amount,Сумма инкассации,
Leave Ledger Entry,Выйти из книги,
Transaction Name,Название транзакции,
-Is Carry Forward,Является ли переносить,
+Is Carry Forward,Переносится вперед,
Is Expired,Истек,
Is Leave Without Pay,Отпуск без содержания,
Holiday List for Optional Leave,Список праздников для дополнительного отпуска,
@@ -6926,9 +6926,9 @@
Last Known Successful Sync of Employee Checkin. Reset this only if you are sure that all Logs are synced from all the locations. Please don't modify this if you are unsure.,"Последняя известная успешная синхронизация регистрации сотрудника. Сбрасывайте это, только если вы уверены, что все журналы синхронизированы из всех мест. Пожалуйста, не изменяйте это, если вы не уверены.",
Grace Period Settings For Auto Attendance,Настройки льготного периода для автоматической посещаемости,
Enable Entry Grace Period,Включить льготный период,
-Late Entry Grace Period,Льготный период позднего въезда,
+Late Entry Grace Period,Льготный период позднего входа,
The time after the shift start time when check-in is considered as late (in minutes).,"Время после начала смены, когда регистрация считается поздней (в минутах).",
-Enable Exit Grace Period,Включить Exit Grace Period,
+Enable Exit Grace Period,Разрешить выход из льготного периода,
Early Exit Grace Period,Льготный период раннего выхода,
The time before the shift end time when check-out is considered as early (in minutes).,Время до окончания смены при выезде считается ранним (в минутах).,
Skill Name,Название навыка,
@@ -7228,7 +7228,7 @@
Operation Time ,Время операции,
In minutes,В считанные минуты,
Batch Size,Размер партии,
-Base Hour Rate(Company Currency),Базовый час Rate (в валюте компании),
+Base Hour Rate(Company Currency),Базовая часовая ставка (в валюте компании),
Operating Cost(Company Currency),Эксплуатационные расходы (в валюте компании),
BOM Scrap Item,Спецификация отходов продукта,
Basic Amount (Company Currency),Базовая сумма (в валюте компании),
@@ -8304,7 +8304,7 @@
Source Warehouse Address,Адрес источника склада,
Default Target Warehouse,Цель по умолчанию Склад,
Target Warehouse Address,Адрес целевого склада,
-Update Rate and Availability,Скорость обновления и доступность,
+Update Rate and Availability,Обновить стоимость и доступность,
Total Incoming Value,Всего входное значение,
Total Outgoing Value,Всего исходящее значение,
Total Value Difference (Out - In),Общая стоимость Разница (Out - In),
@@ -8328,7 +8328,7 @@
Outgoing Rate,Исходящие Оценить,
Actual Qty After Transaction,Остаток после проведения,
Stock Value Difference,Расхождение стоимости запасов,
-Stock Queue (FIFO),Фото со Очередь (FIFO),
+Stock Queue (FIFO),Очередь запасов (FIFO),
Is Cancelled,Является отмененным,
Stock Reconciliation,Инвентаризация запасов,
This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Этот инструмент поможет вам обновить или исправить количество и оценку запасов в системе. Это, как правило, используется для синхронизации системных значений и то, что на самом деле существует в ваших складах.",
@@ -8888,7 +8888,7 @@
Enter a name for the Clinical Procedure Template,Введите имя для шаблона клинической процедуры,
Set the Item Code which will be used for billing the Clinical Procedure.,"Установите код товара, который будет использоваться для выставления счета за клиническую процедуру.",
Select an Item Group for the Clinical Procedure Item.,Выберите группу элементов для элемента клинической процедуры.,
-Clinical Procedure Rate,Скорость клинической процедуры,
+Clinical Procedure Rate,Стоимость клинической процедуры,
Check this if the Clinical Procedure is billable and also set the rate.,"Отметьте это, если клиническая процедура оплачивается, а также установите ставку.",
Check this if the Clinical Procedure utilises consumables. Click ,"Проверьте это, если в клинической процедуре используются расходные материалы. Нажмите",
to know more,узнать больше,
@@ -9067,7 +9067,7 @@
Monthly Eligible Amount,Ежемесячная приемлемая сумма,
Total Eligible HRA Exemption,Полное соответствие требованиям HRA,
Validating Employee Attendance...,Проверка явки сотрудников...,
-Submitting Salary Slips and creating Journal Entry...,Отправка ведомостей о заработной плате и создание записи в журнале ...,
+Submitting Salary Slips and creating Journal Entry...,Утверждение ведомостей о заработной плате и создание записи в журнале ...,
Calculate Payroll Working Days Based On,Расчет рабочих дней для расчета заработной платы на основе,
Consider Unmarked Attendance As,Считайте неотмеченную посещаемость как,
Fraction of Daily Salary for Half Day,Доля дневной заработной платы за полдня,
@@ -9109,7 +9109,7 @@
Track this Purchase Receipt against any Project,Отслеживайте эту квитанцию о покупке для любого проекта,
Please Select a Supplier,"Пожалуйста, выберите поставщика",
Add to Transit,Добавить в общественный транспорт,
-Set Basic Rate Manually,Установить базовую скорость вручную,
+Set Basic Rate Manually,Установить базовую стоимость вручную,
"By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ","По умолчанию имя элемента устанавливается в соответствии с введенным кодом элемента. Если вы хотите, чтобы элементы назывались",
Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.,Установите склад по умолчанию для складских операций. Он будет загружен в Хранилище по умолчанию в мастере предметов.,
"This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.","Это позволит отображать товары на складе с отрицательными значениями. Использование этой опции зависит от вашего варианта использования. Если этот параметр не отмечен, система предупреждает, прежде чем препятствовать транзакции, вызывающей отрицательный запас.",
@@ -9839,9 +9839,14 @@
Enable European Access,Включить европейский доступ,
Creating Purchase Order ...,Создание заказа на поставку ...,
"Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Выберите поставщика из списка поставщиков по умолчанию для позиций ниже. При выборе Заказ на поставку будет сделан в отношении товаров, принадлежащих только выбранному Поставщику.",
-Row #{}: You must select {} serial numbers for item {}.,Строка № {}: необходимо выбрать {} серийных номеров для позиции {}.,
+Row #{}: You must select {} serial numbers for item {}.,Строка №{}: необходимо выбрать {} серийных номеров для позиции {}.,
Items & Pricing,Продукты и цены,
Overdue,Просрочено,
Completed,Завершенно,
Total Tasks,Всего задач,
Build,Конструктор,
+Amend,Исправить,
+Role Allowed to Over Deliver/Receive,"Роль, разрешенная для сверхдоставки/получения",
+Unit of Measure (UOM),Единицы измерения (ЕИ),
+Bank Reconciliation Tool,Инструмент сверки банковских счетов,
+Delayed Tasks Summary,Сводка отложенных задач,
diff --git a/setup.py b/setup.py
index 0ea4d07..29fa1c7 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
# TODO: Remove this file when v15.0.0 is released
from setuptools import setup
-name = "frappe"
+name = "erpnext"
setup()