Merge branch 'develop' into hr-separation

This commit is contained in:
Rucha Mahabal 2022-07-12 19:27:19 +05:30 committed by GitHub
commit d8aa1c59f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 8155 additions and 3263 deletions

View File

@ -18,6 +18,7 @@
"automatically_fetch_payment_terms", "automatically_fetch_payment_terms",
"column_break_17", "column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"report_setting_section", "report_setting_section",
"use_custom_cash_flow", "use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
@ -339,6 +340,13 @@
"fieldname": "report_setting_section", "fieldname": "report_setting_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Report Setting" "label": "Report Setting"
},
{
"default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account "
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -346,7 +354,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-04-08 14:45:06.796418", "modified": "2022-07-11 13:37:50.605141",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -572,9 +572,10 @@ frappe.ui.form.on("Purchase Invoice", {
}, },
is_subcontracted: function(frm) { is_subcontracted: function(frm) {
if (frm.doc.is_subcontracted) { if (frm.doc.is_old_subcontracting_flow) {
erpnext.buying.get_default_bom(frm); erpnext.buying.get_default_bom(frm);
} }
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted); frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
}, },

View File

@ -169,7 +169,8 @@
"column_break_114", "column_break_114",
"auto_repeat", "auto_repeat",
"update_auto_repeat_reference", "update_auto_repeat_reference",
"per_received" "per_received",
"is_old_subcontracting_flow"
], ],
"fields": [ "fields": [
{ {
@ -547,7 +548,8 @@
"fieldname": "is_subcontracted", "fieldname": "is_subcontracted",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Subcontracted", "label": "Is Subcontracted",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "items_section", "fieldname": "items_section",
@ -1365,7 +1367,7 @@
"width": "50px" "width": "50px"
}, },
{ {
"depends_on": "eval:doc.update_stock && doc.is_subcontracted", "depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse", "fieldname": "supplier_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Supplier Warehouse", "label": "Supplier Warehouse",
@ -1416,13 +1418,21 @@
"label": "Advance Tax", "label": "Advance Tax",
"options": "Advance Tax", "options": "Advance Tax",
"read_only": 1 "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", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-25 13:31:02.716727", "modified": "2022-06-15 15:40:58.527065",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -502,7 +502,10 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO # because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() 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 from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
@ -1405,7 +1408,9 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.delete_auto_created_batches() 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() self.make_gl_entries_on_cancel()

View File

@ -470,37 +470,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.tax_amount, expected_values[i][1])
self.assertEqual(tax.total, expected_values[i][2]) 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): def test_purchase_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records, test_records as jv_test_records,
@ -961,30 +930,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi.cancel() pi.cancel()
self.assertEqual(actual_qty_0, get_qty_after_transaction()) 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): def test_rejected_serial_no(self):
pi = make_purchase_invoice( pi = make_purchase_invoice(
item_code="_Test Serialized Item With Series", item_code="_Test Serialized Item With Series",

View File

@ -619,10 +619,13 @@
"search_index": 1 "search_index": 1
}, },
{ {
"depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "BOM", "label": "BOM",
"options": "BOM" "options": "BOM",
"read_only": 1,
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
}, },
{ {
"default": "0", "default": "0",

View File

@ -8,15 +8,17 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Purchase Order", { frappe.ui.form.on("Purchase Order", {
setup: function(frm) { setup: function(frm) {
frm.set_query("reserve_warehouse", "supplied_items", function() { if (frm.doc.is_old_subcontracting_flow) {
return { frm.set_query("reserve_warehouse", "supplied_items", function() {
filters: { return {
"company": frm.doc.company, filters: {
"name": ['!=', frm.doc.supplier_warehouse], "company": frm.doc.company,
"is_group": 0 "name": ['!=', frm.doc.supplier_warehouse],
"is_group": 0
}
} }
} });
}); }
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" })
@ -28,12 +30,67 @@ frappe.ui.form.on("Purchase Order", {
} }
}); });
frm.set_query("fg_item", "items", function() {
return {
filters: {
'is_sub_contracted_item': 1,
'default_bom': ['!=', '']
}
}
});
}, },
company: function(frm) { company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); 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) { onload: function(frm) {
set_schedule_date(frm); set_schedule_date(frm);
if (!frm.doc.transaction_date){ if (!frm.doc.transaction_date){
@ -52,39 +109,6 @@ frappe.ui.form.on("Purchase Order", {
frm.set_value("tax_withholding_category", frm.supplier_tds); 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", { frappe.ui.form.on("Purchase Order Item", {
@ -97,6 +121,16 @@ frappe.ui.form.on("Purchase Order Item", {
set_schedule_date(frm); 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 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {
'Purchase Receipt': 'Purchase Receipt', 'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Purchase Invoice', 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment', 'Payment Entry': 'Payment',
'Subcontracting Order': 'Subcontracting Order',
'Stock Entry': 'Material to Supplier'
} }
super.setup(); super.setup();
} }
refresh(doc, cdt, cdn) { refresh(doc, cdt, cdn) {
@ -142,14 +176,17 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
if(!in_list(["Closed", "Delivered"], doc.status)) { 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) { 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'), () => { // Don't add Update Items button if the PO is following the new subcontracting flow.
erpnext.utils.update_child_items({ if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
frm: this.frm, this.frm.add_custom_button(__('Update Items'), () => {
child_docname: "items", erpnext.utils.update_child_items({
child_doctype: "Purchase Order Detail", frm: this.frm,
cannot_add_row: false, child_docname: "items",
}) child_doctype: "Purchase Order Detail",
}); cannot_add_row: false,
})
});
}
} }
if (this.frm.has_perm("submit")) { if (this.frm.has_perm("submit")) {
if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) { if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) {
@ -177,9 +214,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
if (doc.status != "On Hold") { if (doc.status != "On Hold") {
if(flt(doc.per_received) < 100 && allow_receipt) { if(flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
if(doc.is_subcontracted && me.has_unsupplied_items()) { if (doc.is_subcontracted) {
cur_frm.add_custom_button(__('Material to Supplier'), if (doc.is_old_subcontracting_flow) {
function() { me.make_stock_entry(); }, __("Transfer")); 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) if(flt(doc.per_billed) < 100)
@ -370,10 +413,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
_make_rm_stock_entry(rm_items) { _make_rm_stock_entry(rm_items) {
frappe.call({ frappe.call({
method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry", method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry",
args: { args: {
purchase_order: cur_frm.doc.name, subcontract_order: cur_frm.doc.name,
rm_items: rm_items rm_items: rm_items,
order_doctype: cur_frm.doc.doctype
} }
, ,
callback: function(r) { callback: function(r) {
@ -405,6 +449,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}) })
} }
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() { add_from_mappers() {
var me = this; var me = this;
this.frm.add_custom_button(__('Material Request'), this.frm.add_custom_button(__('Material Request'),
@ -613,15 +665,17 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
} }
} }
cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) { if (cur_frm.doc.is_old_subcontracting_flow) {
var d = locals[cdt][cdn] cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) {
return { var d = locals[cdt][cdn]
filters: [ return {
['BOM', 'item', '=', d.item_code], filters: [
['BOM', 'is_active', '=', '1'], ['BOM', 'item', '=', d.item_code],
['BOM', 'docstatus', '=', '1'], ['BOM', 'is_active', '=', '1'],
['BOM', 'company', '=', doc.company] ['BOM', 'docstatus', '=', '1'],
] ['BOM', 'company', '=', doc.company]
]
}
} }
} }
@ -634,7 +688,7 @@ function set_schedule_date(frm) {
frappe.provide("erpnext.buying"); frappe.provide("erpnext.buying");
frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) { 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); erpnext.buying.get_default_bom(frm);
} }
}); });

View File

@ -16,6 +16,8 @@
"supplier_name", "supplier_name",
"apply_tds", "apply_tds",
"tax_withholding_category", "tax_withholding_category",
"is_subcontracted",
"supplier_warehouse",
"column_break1", "column_break1",
"company", "company",
"transaction_date", "transaction_date",
@ -55,10 +57,7 @@
"price_list_currency", "price_list_currency",
"plc_conversion_rate", "plc_conversion_rate",
"ignore_pricing_rule", "ignore_pricing_rule",
"sec_warehouse", "section_break_45",
"is_subcontracted",
"col_break_warehouse",
"supplier_warehouse",
"before_items_section", "before_items_section",
"scan_barcode", "scan_barcode",
"items_col_break", "items_col_break",
@ -142,7 +141,8 @@
"party_account_currency", "party_account_currency",
"is_internal_supplier", "is_internal_supplier",
"represents_company", "represents_company",
"inter_company_order_reference" "inter_company_order_reference",
"is_old_subcontracting_flow"
], ],
"fields": [ "fields": [
{ {
@ -158,7 +158,8 @@
"hidden": 1, "hidden": 1,
"label": "Title", "label": "Title",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
"reqd": 1
}, },
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
@ -443,11 +444,6 @@
"permlevel": 1, "permlevel": 1,
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "sec_warehouse",
"fieldtype": "Section Break",
"label": "Subcontracting"
},
{ {
"description": "Sets 'Warehouse' in each row of the Items table.", "description": "Sets 'Warehouse' in each row of the Items table.",
"fieldname": "set_warehouse", "fieldname": "set_warehouse",
@ -456,15 +452,10 @@
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "col_break_warehouse",
"fieldtype": "Column Break"
},
{ {
"default": "0", "default": "0",
"fieldname": "is_subcontracted", "fieldname": "is_subcontracted",
"fieldtype": "Check", "fieldtype": "Check",
"in_standard_filter": 1,
"label": "Is Subcontracted", "label": "Is Subcontracted",
"print_hide": 1 "print_hide": 1
}, },
@ -1142,6 +1133,10 @@
"label": "Tax Withholding Category", "label": "Tax Withholding Category",
"options": "Tax Withholding Category" "options": "Tax Withholding Category"
}, },
{
"fieldname": "section_break_45",
"fieldtype": "Section Break"
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
@ -1163,13 +1158,21 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "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", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-04-26 12:16:38.694276", "modified": "2022-06-15 15:40:58.527065",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -69,8 +69,12 @@ class PurchaseOrder(BuyingController):
self.validate_with_previous_doc() self.validate_with_previous_doc()
self.validate_for_subcontracting() self.validate_for_subcontracting()
self.validate_minimum_order_qty() 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() self.set_received_qty_for_drop_ship_items()
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_order_reference self.doctype, self.supplier, self.company, self.inter_company_order_reference
@ -194,12 +198,38 @@ class PurchaseOrder(BuyingController):
) )
def validate_bom_for_subcontracting_items(self): 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: for item in self.items:
if not item.bom: if not item.fg_item:
frappe.throw( frappe.throw(
_("BOM is not specified for subcontracting item {0} at row {1}").format( _("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.item_code, item.idx 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 @@ class PurchaseOrder(BuyingController):
self.set_status(update=True, status=status) self.set_status(update=True, status=status)
self.update_requested_qty() self.update_requested_qty()
self.update_ordered_qty() self.update_ordered_qty()
if self.is_subcontracted: self.update_reserved_qty_for_subcontract()
self.update_reserved_qty_for_subcontract()
self.notify_update() self.notify_update()
clear_doctype_notifications(self) clear_doctype_notifications(self)
@ -310,9 +338,7 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty() self.update_requested_qty()
self.update_ordered_qty() self.update_ordered_qty()
self.validate_budget() self.validate_budget()
self.update_reserved_qty_for_subcontract()
if self.is_subcontracted:
self.update_reserved_qty_for_subcontract()
frappe.get_doc("Authorization Control").validate_approving_authority( frappe.get_doc("Authorization Control").validate_approving_authority(
self.doctype, self.company, self.base_grand_total self.doctype, self.company, self.base_grand_total
@ -332,9 +358,7 @@ class PurchaseOrder(BuyingController):
if self.has_drop_ship_item(): if self.has_drop_ship_item():
self.update_delivered_qty_in_sales_order() 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() self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled") frappe.db.set(self, "status", "Cancelled")
@ -405,10 +429,11 @@ class PurchaseOrder(BuyingController):
item.received_qty = item.qty item.received_qty = item.qty
def update_reserved_qty_for_subcontract(self): def update_reserved_qty_for_subcontract(self):
for d in self.supplied_items: if self.is_old_subcontracting_flow:
if d.rm_item_code: for d in self.supplied_items:
stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse) if d.rm_item_code:
stock_bin.update_reserved_qty_for_sub_contracting() 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): def update_receiving_percentage(self):
total_qty, received_qty = 0.0, 0.0 total_qty, received_qty = 0.0, 0.0
@ -587,80 +612,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
return doc 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): def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context from erpnext.controllers.website_list_for_contact import get_list_context
@ -691,61 +642,61 @@ def make_inter_company_sales_order(source_name, target_doc=None):
@frappe.whitelist() @frappe.whitelist()
def get_materials_from_supplier(purchase_order, po_details): def make_subcontracting_order(source_name, target_doc=None):
if isinstance(po_details, str): return get_mapped_subcontracting_order(source_name, target_doc)
po_details = json.loads(po_details)
doc = frappe.get_cached_doc("Purchase Order", purchase_order)
doc.initialized_fields()
doc.purchase_orders = [doc.name]
doc.get_available_materials()
if not doc.available_materials:
frappe.throw(
_("Materials are already received against the purchase order {0}").format(purchase_order)
)
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details): def get_mapped_subcontracting_order(source_name, target_doc=None):
ste_doc = frappe.new_doc("Stock Entry")
ste_doc.purpose = "Material Transfer"
ste_doc.purchase_order = po_doc.name
ste_doc.company = po_doc.company
ste_doc.is_return = 1
for key, value in available_materials.items(): if target_doc and isinstance(target_doc, str):
if not value.qty: target_doc = json.loads(target_doc)
continue 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: target_doc = get_mapped_doc(
for batch_no, qty in value.batch_no.items(): "Purchase Order",
if qty > 0: source_name,
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(
{ {
"qty": qty, "Purchase Order": {
"batch_no": batch_no, "doctype": "Subcontracting Order",
"basic_rate": row.item_details["rate"], "field_map": {},
"po_detail": po_detail[0] if po_detail else "", "field_no_map": ["total_qty", "total", "net_total"],
"s_warehouse": row.item_details["t_warehouse"], "validation": {
"t_warehouse": row.item_details["s_warehouse"], "docstatus": ["=", 1],
"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 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

View File

@ -22,6 +22,6 @@ def get_data():
"label": _("Reference"), "label": _("Reference"),
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"], "items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
}, },
{"label": _("Sub-contracting"), "items": ["Stock Entry"]}, {"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
], ],
} }

View File

@ -13,9 +13,6 @@ from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_pi_from_po, 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_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.controllers.accounts_controller import update_child_qty_rate
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
@ -24,7 +21,6 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr, make_purchase_invoice as make_pi_from_pr,
) )
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPurchaseOrder(FrappeTestCase): class TestPurchaseOrder(FrappeTestCase):
@ -140,43 +136,6 @@ class TestPurchaseOrder(FrappeTestCase):
# ordered qty decreases as ordered qty is 0 (deleted row) # ordered qty decreases as ordered qty is 0 (deleted row)
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 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): def test_update_child(self):
mr = make_material_request(qty=10) mr = make_material_request(qty=10)
po = make_purchase_order(mr.name) po = make_purchase_order(mr.name)
@ -426,31 +385,6 @@ class TestPurchaseOrder(FrappeTestCase):
new_item_with_tax.delete() new_item_with_tax.delete()
frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").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): def test_update_qty(self):
po = create_purchase_order() po = create_purchase_order()
@ -609,10 +543,6 @@ class TestPurchaseOrder(FrappeTestCase):
) )
automatically_fetch_payment_terms(enable=0) 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): def test_warehouse_company_validation(self):
from erpnext.stock.utils import InvalidWarehouseCompany from erpnext.stock.utils import InvalidWarehouseCompany
@ -777,379 +707,6 @@ class TestPurchaseOrder(FrappeTestCase):
pi.insert() pi.insert()
self.assertTrue(pi.get("payment_schedule")) 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): def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@ -1248,50 +805,6 @@ def make_pr_against_po(po, received_qty=0):
return pr 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(): def get_same_items():
return [ return [
{ {

View File

@ -1,38 +1,4 @@
[ [
{
"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": 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, "advance_paid": 0.0,
"buying_price_list": "_Test Price List", "buying_price_list": "_Test Price List",

View File

@ -11,6 +11,8 @@
"supplier_part_no", "supplier_part_no",
"item_name", "item_name",
"product_bundle", "product_bundle",
"fg_item",
"fg_item_qty",
"column_break_4", "column_break_4",
"schedule_date", "schedule_date",
"expected_delivery_date", "expected_delivery_date",
@ -574,16 +576,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.is_subcontracted", "depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "BOM", "label": "BOM",
"options": "BOM", "options": "BOM",
"print_hide": 1 "print_hide": 1,
"read_only": 1,
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:parent.is_subcontracted", "depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "include_exploded_items", "fieldname": "include_exploded_items",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include Exploded Items", "label": "Include Exploded Items",
@ -848,6 +852,22 @@
"label": "Sales Order Packed Item", "label": "Sales Order Packed Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 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, "idx": 1,

View File

@ -180,12 +180,20 @@ class RequestforQuotation(BuyingController):
doc_args = self.as_dict() doc_args = self.as_dict()
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")}) 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 = { args = {
"update_password_link": update_password_link, "update_password_link": update_password_link,
"message": frappe.render_template(self.message_for_supplier, doc_args), "message": frappe.render_template(self.message_for_supplier, doc_args),
"rfq_link": rfq_link, "rfq_link": rfq_link,
"user_fullname": full_name, "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.", "supplier_salutation": self.salutation or "Dear Mx.",
} }

View File

@ -14,32 +14,29 @@ frappe.query_reports["Subcontract Order Summary"] = {
}, },
{ {
label: __("From Date"), label: __("From Date"),
fieldname:"from_date", fieldname: "from_date",
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1 reqd: 1
}, },
{ {
label: __("To Date"), label: __("To Date"),
fieldname:"to_date", fieldname: "to_date",
fieldtype: "Date", fieldtype: "Date",
default: frappe.datetime.get_today(), default: frappe.datetime.get_today(),
reqd: 1 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", fieldname: "name",
fieldtype: "Link", fieldtype: "Data"
options: "Purchase Order",
get_query: function() {
return {
filters: {
docstatus: 1,
is_subcontracted: 1,
company: frappe.query_report.get_filter_value('company')
}
}
}
} }
] ]
}; };

View File

@ -15,7 +15,7 @@
"name": "Subcontract Order Summary", "name": "Subcontract Order Summary",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
"ref_doctype": "Purchase Order", "ref_doctype": "Subcontracting Order",
"report_name": "Subcontract Order Summary", "report_name": "Subcontract Order Summary",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [ "roles": [

View File

@ -8,7 +8,7 @@ from frappe import _
def execute(filters=None): def execute(filters=None):
columns, data = [], [] columns, data = [], []
columns = get_columns() columns = get_columns(filters)
data = get_data(filters) data = get_data(filters)
return columns, data return columns, data
@ -20,43 +20,45 @@ def get_data(report_filters):
if orders: if orders:
supplied_items = get_supplied_items(orders, report_filters) supplied_items = get_supplied_items(orders, report_filters)
po_details = prepare_subcontracted_data(orders, supplied_items) order_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(po_details, data) get_subcontracted_data(order_details, data)
return data return data
def get_subcontracted_orders(report_filters): def get_subcontracted_orders(report_filters):
fields = [ fields = [
"`tabPurchase Order Item`.`parent` as po_id", f"`tab{report_filters.order_type} Item`.`parent` as order_id",
"`tabPurchase Order Item`.`item_code`", f"`tab{report_filters.order_type} Item`.`item_code`",
"`tabPurchase Order Item`.`item_name`", f"`tab{report_filters.order_type} Item`.`item_name`",
"`tabPurchase Order Item`.`qty`", f"`tab{report_filters.order_type} Item`.`qty`",
"`tabPurchase Order Item`.`name`", f"`tab{report_filters.order_type} Item`.`name`",
"`tabPurchase Order Item`.`received_qty`", f"`tab{report_filters.order_type} Item`.`received_qty`",
"`tabPurchase Order`.`status`", f"`tab{report_filters.order_type}`.`status`",
] ]
filters = get_filters(report_filters) 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): def get_filters(report_filters):
filters = [ filters = [
["Purchase Order", "docstatus", "=", 1], [report_filters.order_type, "docstatus", "=", 1],
["Purchase Order", "is_subcontracted", "=", 1],
[ [
"Purchase Order", report_filters.order_type,
"transaction_date", "transaction_date",
"between", "between",
(report_filters.from_date, report_filters.to_date), (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"]: for field in ["name", "company"]:
if report_filters.get(field): 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 return filters
@ -77,10 +79,15 @@ def get_supplied_items(orders, report_filters):
"reference_name", "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 = {} 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) new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row) supplied_items.setdefault(new_key, []).append(row)
@ -89,24 +96,24 @@ def get_supplied_items(orders, report_filters):
def prepare_subcontracted_data(orders, supplied_items): def prepare_subcontracted_data(orders, supplied_items):
po_details = {} order_details = {}
for row in orders: for row in orders:
key = (row.po_id, row.name, row.item_code) key = (row.order_id, row.name, row.item_code)
if key not in po_details: if key not in order_details:
po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []})) order_details.setdefault(key, frappe._dict({"order_item": row, "supplied_items": []}))
details = po_details[key] details = order_details[key]
if supplied_items.get(key): if supplied_items.get(key):
for supplied_item in supplied_items[key]: for supplied_item in supplied_items[key]:
details["supplied_items"].append(supplied_item) details["supplied_items"].append(supplied_item)
return po_details return order_details
def get_subcontracted_data(po_details, data): def get_subcontracted_data(order_details, data):
for key, details in po_details.items(): for key, details in order_details.items():
res = details.po_item res = details.order_item
for index, row in enumerate(details.supplied_items): for index, row in enumerate(details.supplied_items):
if index != 0: if index != 0:
res = {} res = {}
@ -115,13 +122,13 @@ def get_subcontracted_data(po_details, data):
data.append(res) data.append(res)
def get_columns(): def get_columns(filters):
return [ return [
{ {
"label": _("Purchase Order"), "label": _("Subcontract Order"),
"fieldname": "po_id", "fieldname": "order_id",
"fieldtype": "Link", "fieldtype": "Link",
"options": "Purchase Order", "options": filters.order_type,
"width": 100, "width": 100,
}, },
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80},

View File

@ -4,6 +4,13 @@
frappe.query_reports["Subcontracted Item To Be Received"] = { frappe.query_reports["Subcontracted Item To Be Received"] = {
"filters": [ "filters": [
{
label: __("Order Type"),
fieldname: "order_type",
fieldtype: "Select",
options: ["Purchase Order", "Subcontracting Order"],
default: "Subcontracting Order"
},
{ {
fieldname: "supplier", fieldname: "supplier",
label: __("Supplier"), label: __("Supplier"),

View File

@ -13,7 +13,7 @@
"name": "Subcontracted Item To Be Received", "name": "Subcontracted Item To Be Received",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
"ref_doctype": "Purchase Order", "ref_doctype": "Subcontracting Order",
"report_name": "Subcontracted Item To Be Received", "report_name": "Subcontracted Item To Be Received",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [ "roles": [

View File

@ -11,18 +11,18 @@ def execute(filters=None):
frappe.msgprint(_("To Date must be greater than From Date")) frappe.msgprint(_("To Date must be greater than From Date"))
data = [] data = []
columns = get_columns() columns = get_columns(filters)
get_data(data, filters) get_data(data, filters)
return columns, data return columns, data
def get_columns(): def get_columns(filters):
return [ return [
{ {
"label": _("Purchase Order"), "label": _("Subcontract Order"),
"fieldtype": "Link", "fieldtype": "Link",
"fieldname": "purchase_order", "fieldname": "subcontract_order",
"options": "Purchase Order", "options": filters.order_type,
"width": 150, "width": 150,
}, },
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150}, {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150},
@ -57,14 +57,14 @@ def get_columns():
def get_data(data, filters): def get_data(data, filters):
po = get_po(filters) orders = get_subcontract_orders(filters)
po_name = [v.name for v in po] orders_name = [order.name for order in orders]
sub_items = get_purchase_order_item_supplied(po_name) subcontracted_items = get_subcontract_order_supplied_item(filters.order_type, orders_name)
for item in sub_items: for item in subcontracted_items:
for order in po: for order in orders:
if order.name == item.parent and item.received_qty < item.qty: if order.name == item.parent and item.received_qty < item.qty:
row = { row = {
"purchase_order": item.parent, "subcontract_order": item.parent,
"date": order.transaction_date, "date": order.transaction_date,
"supplier": order.supplier, "supplier": order.supplier,
"fg_item_code": item.item_code, "fg_item_code": item.item_code,
@ -76,22 +76,25 @@ def get_data(data, filters):
data.append(row) data.append(row)
def get_po(filters): def get_subcontract_orders(filters):
record_filters = [ record_filters = [
["is_subcontracted", "=", 1],
["supplier", "=", filters.supplier], ["supplier", "=", filters.supplier],
["transaction_date", "<=", filters.to_date], ["transaction_date", "<=", filters.to_date],
["transaction_date", ">=", filters.from_date], ["transaction_date", ">=", filters.from_date],
["docstatus", "=", 1], ["docstatus", "=", 1],
] ]
if filters.order_type == "Purchase Order":
record_filters.append(["is_old_subcontracting_flow", "=", 1])
return frappe.get_all( 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( return frappe.get_all(
"Purchase Order Item", f"{order_type} Item",
filters=[("parent", "IN", po)], filters=[("parent", "IN", orders)],
fields=["parent", "item_code", "item_name", "qty", "received_qty"], fields=["parent", "item_code", "item_name", "qty", "received_qty"],
) )

View File

@ -7,18 +7,35 @@
import frappe import frappe
from frappe.tests.utils import FrappeTestCase 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 ( from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_to_be_received import (
execute, 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.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): class TestSubcontractedItemToBeReceived(FrappeTestCase):
def test_pending_and_received_qty(self): def test_pending_and_received_qty(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1) make_service_item("Subcontracted Service Item 1")
transfer_param = [] 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( make_stock_entry(
item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
) )
@ -28,28 +45,28 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase):
qty=100, qty=100,
basic_rate=100, basic_rate=100,
) )
make_purchase_receipt_against_po(po.name) make_subcontracting_receipt_against_sco(sco.name)
po.reload() sco.reload()
col, data = execute( col, data = execute(
filters=frappe._dict( filters=frappe._dict(
{ {
"supplier": po.supplier, "order_type": "Subcontracting Order",
"supplier": sco.supplier,
"from_date": frappe.utils.get_datetime( "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]["pending_qty"], 5)
self.assertEqual(data[0]["received_qty"], 5) self.assertEqual(data[0]["received_qty"], 5)
self.assertEqual(data[0]["purchase_order"], po.name) self.assertEqual(data[0]["subcontract_order"], sco.name)
self.assertEqual(data[0]["supplier"], po.supplier) self.assertEqual(data[0]["supplier"], sco.supplier)
def make_purchase_receipt_against_po(po, quantity=5): def make_subcontracting_receipt_against_sco(sco, quantity=5):
pr = make_purchase_receipt(po) scr = make_subcontracting_receipt(sco)
pr.items[0].qty = quantity scr.items[0].qty = quantity
pr.supplier_warehouse = "_Test Warehouse 1 - _TC" scr.insert()
pr.insert() scr.submit()
pr.submit()

View File

@ -4,6 +4,13 @@
frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = { frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
"filters": [ "filters": [
{
label: __("Order Type"),
fieldname: "order_type",
fieldtype: "Select",
options: ["Purchase Order", "Subcontracting Order"],
default: "Subcontracting Order"
},
{ {
fieldname: "supplier", fieldname: "supplier",
label: __("Supplier"), label: __("Supplier"),

View File

@ -13,7 +13,7 @@
"name": "Subcontracted Raw Materials To Be Transferred", "name": "Subcontracted Raw Materials To Be Transferred",
"owner": "Administrator", "owner": "Administrator",
"prepared_report": 0, "prepared_report": 0,
"ref_doctype": "Purchase Order", "ref_doctype": "Subcontracting Order",
"report_name": "Subcontracted Raw Materials To Be Transferred", "report_name": "Subcontracted Raw Materials To Be Transferred",
"report_type": "Script Report", "report_type": "Script Report",
"roles": [ "roles": [

View File

@ -10,19 +10,19 @@ def execute(filters=None):
if filters.from_date >= filters.to_date: if filters.from_date >= filters.to_date:
frappe.msgprint(_("To Date must be greater than From Date")) frappe.msgprint(_("To Date must be greater than From Date"))
columns = get_columns() columns = get_columns(filters)
data = get_data(filters) data = get_data(filters)
return columns, data or [] return columns, data or []
def get_columns(): def get_columns(filters):
return [ return [
{ {
"label": _("Purchase Order"), "label": _("Subcontract Order"),
"fieldtype": "Link", "fieldtype": "Link",
"fieldname": "purchase_order", "fieldname": "subcontract_order",
"options": "Purchase Order", "options": filters.order_type,
"width": 200, "width": 200,
}, },
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150}, {"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150},
@ -46,10 +46,10 @@ def get_columns():
def get_data(filters): 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 = [] data = []
for row in po_rm_item_details: for row in order_rm_item_details:
transferred_qty = row.get("transferred_qty") or 0 transferred_qty = row.get("transferred_qty") or 0
if transferred_qty < row.get("reqd_qty", 0): if transferred_qty < row.get("reqd_qty", 0):
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty) pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
@ -59,23 +59,33 @@ def get_data(filters):
return data 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( return frappe.db.get_all(
"Purchase Order", filters.order_type,
fields=[ fields=[
"name as purchase_order", "name as subcontract_order",
"transaction_date as date", "transaction_date as date",
"supplier as supplier", "supplier as supplier",
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code", f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty", f"`tab{supplied_items_table}`.required_qty as reqd_qty",
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_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,
) )

View File

@ -3,24 +3,34 @@
# Compiled at: 2019-05-06 10:24:35 # Compiled at: 2019-05-06 10:24:35
# Decompiled by https://python-decompiler.com # Decompiled by https://python-decompiler.com
import json
import frappe import frappe
from frappe.tests.utils import FrappeTestCase 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 ( from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import (
execute, 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 from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestSubcontractedItemToBeTransferred(FrappeTestCase): class TestSubcontractedItemToBeTransferred(FrappeTestCase):
def test_pending_and_transferred_qty(self): def test_pending_and_transferred_qty(self):
po = create_purchase_order( make_service_item("Subcontracted Service Item 1")
item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" 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 # Material Receipt of RMs
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100) make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
@ -28,50 +38,48 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase):
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100 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( col, data = execute(
filters=frappe._dict( filters=frappe._dict(
{ {
"supplier": po.supplier, "order_type": "Subcontracting Order",
"supplier": sco.supplier,
"from_date": frappe.utils.get_datetime( "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 # 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(len(sco_data), 2)
self.assertEqual(po_data[0]["purchase_order"], po.name) self.assertEqual(sco_data[0]["subcontract_order"], sco.name)
self.assertEqual(po_data[0]["rm_item_code"], "_Test Item") self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item")
self.assertEqual(po_data[0]["p_qty"], 8) self.assertEqual(sco_data[0]["p_qty"], 8)
self.assertEqual(po_data[0]["transferred_qty"], 2) self.assertEqual(sco_data[0]["transferred_qty"], 2)
self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100") self.assertEqual(sco_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
self.assertEqual(po_data[1]["p_qty"], 19) self.assertEqual(sco_data[1]["p_qty"], 19)
self.assertEqual(po_data[1]["transferred_qty"], 1) self.assertEqual(sco_data[1]["transferred_qty"], 1)
se.cancel()
po.cancel()
def transfer_subcontracted_raw_materials(po): def transfer_subcontracted_raw_materials(sco):
# Order of supplied items fetched in PO is flaky # Order of supplied items fetched in SCO is flaky
transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1} transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1}
item_1 = po.supplied_items[0].rm_item_code item_1 = sco.supplied_items[0].rm_item_code
item_2 = po.supplied_items[1].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, "item_code": item_1,
"rm_item_code": item_1, "rm_item_code": item_1,
"item_name": item_1, "item_name": item_1,
@ -82,7 +90,7 @@ def transfer_subcontracted_raw_materials(po):
"stock_uom": "Nos", "stock_uom": "Nos",
}, },
{ {
"name": po.supplied_items[1].name, "name": sco.supplied_items[1].name,
"item_code": item_2, "item_code": item_2,
"rm_item_code": item_2, "rm_item_code": item_2,
"item_name": item_2, "item_name": item_2,
@ -93,8 +101,7 @@ def transfer_subcontracted_raw_materials(po):
"stock_uom": "Nos", "stock_uom": "Nos",
}, },
] ]
rm_item_string = json.dumps(rm_item) se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
se.from_warehouse = "_Test Warehouse - _TC" se.from_warehouse = "_Test Warehouse - _TC"
se.to_warehouse = "_Test Warehouse - _TC" se.to_warehouse = "_Test Warehouse - _TC"
se.stock_entry_type = "Send to Subcontractor" se.stock_entry_type = "Send to Subcontractor"

View File

@ -1472,8 +1472,15 @@ class AccountsController(TransactionBase):
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to") self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
) )
party_account_currency = get_account_currency(party_account) party_account_currency = get_account_currency(party_account)
allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
)
if not party_gle_currency and (party_account_currency != self.currency): if (
not party_gle_currency
and (party_account_currency != self.currency)
and not allow_multi_currency_invoices_against_single_party_account
):
frappe.throw( frappe.throw(
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format( _("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
frappe.bold(party_account), party_account_currency, self.currency frappe.bold(party_account), party_account_currency, self.currency
@ -2709,10 +2716,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_ordered_qty() parent.update_ordered_qty()
parent.update_ordered_and_reserved_qty() parent.update_ordered_and_reserved_qty()
parent.update_receiving_percentage() parent.update_receiving_percentage()
if parent.is_subcontracted: if parent.is_old_subcontracting_flow:
if should_update_supplied_items(parent): if should_update_supplied_items(parent):
parent.update_reserved_qty_for_subcontract() parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied("supplied_items") parent.create_raw_materials_supplied()
parent.save() parent.save()
else: # Sales Order else: # Sales Order
parent.validate_warehouse() parent.validate_warehouse()

View File

@ -11,8 +11,7 @@ from erpnext.accounts.doctype.budget.budget import validate_expense_against_budg
from erpnext.accounts.party import get_party_details from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items 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.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.controllers.subcontracting import Subcontracting
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@ -21,7 +20,7 @@ class QtyMismatchError(ValidationError):
pass pass
class BuyingController(StockController, Subcontracting): class BuyingController(SubcontractingController):
def __setup__(self): def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"] self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
@ -55,7 +54,8 @@ class BuyingController(StockController, Subcontracting):
# sub-contracting # sub-contracting
self.validate_for_subcontracting() 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() self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
@ -256,13 +256,18 @@ class BuyingController(StockController, Subcontracting):
) )
qty_in_stock_uom = flt(item.qty * item.conversion_factor) qty_in_stock_uom = flt(item.qty * item.conversion_factor)
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) if self.get("is_old_subcontracting_flow"):
item.valuation_rate = ( item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.base_net_amount item.valuation_rate = (
+ item.item_tax_amount item.base_net_amount
+ item.rm_supp_cost + item.item_tax_amount
+ flt(item.landed_cost_voucher_amount) + item.rm_supp_cost
) / qty_in_stock_uom + 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: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0
@ -317,76 +322,25 @@ class BuyingController(StockController, Subcontracting):
d.discount_amount = 0.0 d.discount_amount = 0.0
d.margin_rate_or_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): 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: 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)) frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"): for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom: 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)) frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype != "Purchase Order": if self.doctype != "Purchase Order":
return return
for row in self.get("supplied_items"): for row in self.get("supplied_items"):
if not row.reserve_warehouse: if not row.reserve_warehouse:
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied" msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
frappe.throw(_(msg)) frappe.throw(_(msg))
else: else:
for item in self.get("items"): for item in self.get("items"):
if item.bom: if item.get("bom"):
item.bom = None 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): def set_qty_as_per_stock_uom(self):
for d in self.get("items"): for d in self.get("items"):
if d.meta.get_field("stock_qty"): if d.meta.get_field("stock_qty"):
@ -510,7 +464,9 @@ class BuyingController(StockController, Subcontracting):
sle.update( sle.update(
{ {
"incoming_rate": incoming_rate, "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) sl_entries.append(sle)
@ -538,7 +494,8 @@ class BuyingController(StockController, Subcontracting):
) )
) )
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( self.make_sl_entries(
sl_entries, sl_entries,
allow_negative_stock=allow_negative_stock, allow_negative_stock=allow_negative_stock,
@ -565,26 +522,9 @@ class BuyingController(StockController, Subcontracting):
) )
po_obj.update_ordered_qty(po_item_rows) 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() 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): def on_submit(self):
if self.get("is_return"): if self.get("is_return"):
return return
@ -808,7 +748,7 @@ class BuyingController(StockController, Subcontracting):
if self.doctype == "Material Request": if self.doctype == "Material Request":
return 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") validate_item_type(self, "is_sub_contracted_item", "subcontracted")
else: else:
validate_item_type(self, "is_purchase_item", "purchase") validate_item_type(self, "is_purchase_item", "purchase")

View File

@ -77,7 +77,7 @@ def validate_returned_items(doc):
if doc.doctype != "Purchase Invoice": if doc.doctype != "Purchase Invoice":
select_fields += ",serial_no, batch_no" 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" select_fields += ",rejected_qty, received_qty"
for d in frappe.db.sql( for d in frappe.db.sql(
@ -161,7 +161,7 @@ def validate_returned_items(doc):
def validate_quantity(doc, args, ref, valid_items, already_returned_items): def validate_quantity(doc, args, ref, valid_items, already_returned_items):
fields = ["stock_qty"] 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"]) fields.extend(["received_qty", "rejected_qty"])
already_returned_data = already_returned_items.get(args.item_code) or {} already_returned_data = already_returned_items.get(args.item_code) or {}
@ -224,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
if ref_item_row.get("rate", 0) > item_dict["rate"]: if ref_item_row.get("rate", 0) > item_dict["rate"]:
item_dict["rate"] = ref_item_row.get("rate", 0) 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["received_qty"] += ref_item_row.received_qty
item_dict["rejected_qty"] += ref_item_row.rejected_qty item_dict["rejected_qty"] += ref_item_row.rejected_qty
@ -239,7 +239,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
def get_already_returned_items(doc): def get_already_returned_items(doc):
column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty" 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, column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty,
sum(abs(child.received_qty) * child.conversion_factor) as received_qty""" sum(abs(child.received_qty) * child.conversion_factor) as received_qty"""
@ -281,17 +281,21 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
child_doctype = doctype + " Item" child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) 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" party_type = "supplier"
else: else:
party_type = "customer" party_type = "customer"
fields = [ fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), "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 += [ fields += [
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype),
@ -342,7 +346,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
# look for Print Heading "Debit Note" # look for Print Heading "Debit Note"
doc.select_print_heading = frappe.db.get_value("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": if tax.charge_type == "Actual":
tax.tax_amount = -1 * tax.tax_amount tax.tax_amount = -1 * tax.tax_amount
@ -381,8 +385,11 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
for d in doc.get("packed_items"): for d in doc.get("packed_items"):
d.qty = d.qty * -1 d.qty = d.qty * -1
doc.discount_amount = -1 * source.discount_amount if doc.get("discount_amount"):
doc.run_method("calculate_taxes_and_totals") 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): def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty target_doc.qty = -1 * source_doc.qty
@ -393,7 +400,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if serial_nos: if serial_nos:
target_doc.serial_no = "\n".join(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( returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype source_parent.name, source_parent.supplier, source_doc.name, doctype
) )
@ -405,15 +412,24 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
) )
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0)) 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)) if hasattr(target_doc, "stock_qty"):
target_doc.received_stock_qty = -1 * flt( target_doc.stock_qty = -1 * flt(
source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0) 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 if doctype == "Subcontracting Receipt":
target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.subcontracting_order = source_doc.subcontracting_order
target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.subcontracting_order_item = source_doc.subcontracting_order_item
target_doc.purchase_receipt_item = source_doc.name 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": elif doctype == "Purchase Invoice":
returned_qty_map = get_returned_qty_map_for_row( returned_qty_map = get_returned_qty_map_for_row(
@ -525,7 +541,7 @@ def get_rate_for_return(
item_row, item_row,
) )
if voucher_type in ("Purchase Receipt", "Purchase Invoice"): if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
select_field = "incoming_rate" select_field = "incoming_rate"
else: else:
select_field = "abs(stock_value_difference / actual_qty)" select_field = "abs(stock_value_difference / actual_qty)"
@ -560,6 +576,7 @@ def get_return_against_item_fields(voucher_type):
"Purchase Invoice": "purchase_invoice_item", "Purchase Invoice": "purchase_invoice_item",
"Delivery Note": "dn_detail", "Delivery Note": "dn_detail",
"Sales Invoice": "sales_invoice_item", "Sales Invoice": "sales_invoice_item",
"Subcontracting Receipt": "subcontracting_receipt_item",
} }
return return_against_item_fields[voucher_type] return return_against_item_fields[voucher_type]

View File

@ -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"))

View File

@ -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
)

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,10 @@
"total_payment", "total_payment",
"total_principal_paid", "total_principal_paid",
"written_off_amount", "written_off_amount",
"refund_amount",
"debit_adjustment_amount",
"credit_adjustment_amount",
"is_npa",
"column_break_19", "column_break_19",
"total_interest_payable", "total_interest_payable",
"total_amount_paid", "total_amount_paid",
@ -371,12 +375,39 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "credit_adjustment_amount",
"fieldtype": "Currency",
"label": "Credit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "debit_adjustment_amount",
"fieldtype": "Currency",
"label": "Debit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"default": "0",
"description": "Mark Loan as a Nonperforming asset",
"fieldname": "is_npa",
"fieldtype": "Check",
"label": "Is NPA"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-06-21 11:50:31.957360", "modified": "2022-07-12 11:50:31.957360",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Balance Adjustment', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,189 @@
{
"actions": [],
"autoname": "LM-ADJ-.#####",
"creation": "2022-06-28 14:48:47.736269",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"adjustment_account",
"column_break_11",
"adjustment_type",
"amount",
"reference_number",
"remarks",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Adjustment Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "adjustment_account",
"fieldtype": "Link",
"label": "Adjustment Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "adjustment_type",
"fieldtype": "Select",
"label": "Adjustment Type",
"options": "Credit Adjustment\nDebit Adjustment",
"reqd": 1
},
{
"fieldname": "remarks",
"fieldtype": "Data",
"label": "Remarks"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-07-08 16:48:54.480066",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Balance Adjustment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,143 @@
# 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 add_days, nowdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
class LoanBalanceAdjustment(AccountsController):
"""
Add credit/debit adjustments to loan ledger.
"""
def validate(self):
if self.amount == 0:
frappe.throw(_("Amount cannot be zero"))
if self.amount < 0:
frappe.throw(_("Amount cannot be negative"))
self.set_missing_values()
def on_submit(self):
self.set_status_and_amounts()
self.make_gl_entries()
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_values(self):
if not self.posting_date:
self.posting_date = nowdate()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.db.get_value(
"Loan",
self.loan,
[
"loan_amount",
"credit_adjustment_amount",
"debit_adjustment_amount",
"total_payment",
"total_principal_paid",
"total_interest_payable",
"status",
"is_term_loan",
"is_secured_loan",
],
as_dict=1,
)
if cancel:
adjustment_amount = self.get_values_on_cancel(loan_details)
else:
adjustment_amount = self.get_values_on_submit(loan_details)
if self.adjustment_type == "Credit Adjustment":
adj_field = "credit_adjustment_amount"
elif self.adjustment_type == "Debit Adjustment":
adj_field = "debit_adjustment_amount"
frappe.db.set_value("Loan", self.loan, {adj_field: adjustment_amount})
def get_values_on_cancel(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount - self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount - self.amount
return adjustment_amount
def get_values_on_submit(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount + self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount + self.amount
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(
posting_date=add_days(self.posting_date, -1),
loan=self.loan,
accrual_type=self.adjustment_type,
)
return adjustment_amount
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number)
loan_entry = {
"account": loan_account,
"against": self.adjustment_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.posting_date,
}
company_entry = {
"account": self.adjustment_account,
"against": loan_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": self.posting_date,
}
if self.adjustment_type == "Credit Adjustment":
loan_entry["credit"] = self.amount
loan_entry["credit_in_account_currency"] = self.amount
company_entry["debit"] = self.amount
company_entry["debit_in_account_currency"] = self.amount
elif self.adjustment_type == "Debit Adjustment":
loan_entry["debit"] = self.amount
loan_entry["debit_in_account_currency"] = self.amount
company_entry["credit"] = self.amount
company_entry["credit_in_account_currency"] = self.amount
gle_map.append(self.get_gl_dict(loan_entry))
gle_map.append(self.get_gl_dict(company_entry))
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanBalanceAdjustment(FrappeTestCase):
pass

View File

@ -35,12 +35,15 @@
{ {
"fieldname": "loan", "fieldname": "loan",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan", "label": "Loan",
"options": "Loan" "options": "Loan"
}, },
{ {
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date" "label": "Posting Date"
}, },
{ {
@ -75,6 +78,8 @@
"fetch_from": "loan.applicant", "fetch_from": "loan.applicant",
"fieldname": "applicant", "fieldname": "applicant",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant", "label": "Applicant",
"options": "applicant_type" "options": "applicant_type"
}, },
@ -158,8 +163,11 @@
{ {
"fieldname": "accrual_type", "fieldname": "accrual_type",
"fieldtype": "Select", "fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Accrual Type", "label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement" "options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
}, },
{ {
"fieldname": "penalty_amount", "fieldname": "penalty_amount",
@ -185,10 +193,11 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:26:38.871889", "modified": "2022-06-30 11:51:31.911794",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Interest Accrual", "name": "Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -225,5 +234,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Refund', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,176 @@
{
"actions": [],
"autoname": "LM-RF-.#####",
"creation": "2022-06-24 15:51:03.165498",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"refund_account",
"column_break_11",
"refund_amount",
"reference_number",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Refund Details"
},
{
"fieldname": "refund_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Refund Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-06-24 16:13:48.793486",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Refund",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,97 @@
# 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 getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
class LoanRefund(AccountsController):
"""
Add refund if total repayment is more than that is owed.
"""
def validate(self):
self.set_missing_values()
self.validate_refund_amount()
def set_missing_values(self):
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def validate_refund_amount(self):
loan = frappe.get_doc("Loan", self.loan)
pending_amount = get_pending_principal_amount(loan)
if pending_amount >= 0:
frappe.throw(_("No excess amount to refund."))
else:
excess_amount = pending_amount * -1
if self.refund_amount > excess_amount:
frappe.throw(_("Refund amount cannot be greater than excess amount {0}").format(excess_amount))
def on_submit(self):
self.update_outstanding_amount()
self.make_gl_entries()
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
refund_amount = frappe.db.get_value("Loan", self.loan, "refund_amount")
if cancel:
refund_amount -= self.refund_amount
else:
refund_amount += self.refund_amount
frappe.db.set_value("Loan", self.loan, "refund_amount", refund_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
self.get_gl_dict(
{
"account": self.refund_account,
"against": loan_details.loan_account,
"credit": self.refund_amount,
"credit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": self.refund_account,
"debit": self.refund_amount,
"debit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanRefund(FrappeTestCase):
pass

View File

@ -386,15 +386,19 @@ class LoanRepayment(AccountsController):
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
if self.shortfall_amount and self.amount_paid > self.shortfall_amount: if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}.<br>Repayment against Loan: {1}").format( remarks = "Shortfall repayment of {0}.<br>Repayment against loan {1}".format(
self.shortfall_amount, self.against_loan self.shortfall_amount, self.against_loan
) )
elif self.shortfall_amount: elif self.shortfall_amount:
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount) remarks = "Shortfall repayment of {0} against loan {1}".format(
self.shortfall_amount, self.against_loan
)
else: else:
remarks = _("Repayment against Loan:") + " " + self.against_loan remarks = "Repayment against loan " + self.against_loan
if self.reference_number:
remarks += "with reference no. {}".format(self.reference_number)
if hasattr(self, "repay_from_salary") and self.repay_from_salary: if hasattr(self, "repay_from_salary") and self.repay_from_salary:
payment_account = self.payroll_payable_account payment_account = self.payroll_payable_account
@ -445,7 +449,7 @@ class LoanRepayment(AccountsController):
"debit_in_account_currency": self.amount_paid, "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": remarks, "remarks": _(remarks),
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date), "posting_date": getdate(self.posting_date),
} }
@ -463,7 +467,7 @@ class LoanRepayment(AccountsController):
"credit_in_account_currency": self.amount_paid, "credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": remarks, "remarks": _(remarks),
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date), "posting_date": getdate(self.posting_date),
} }
@ -623,16 +627,22 @@ def get_pending_principal_amount(loan):
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount: if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
pending_principal_amount = ( pending_principal_amount = (
flt(loan.total_payment) flt(loan.total_payment)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid) - flt(loan.total_principal_paid)
- flt(loan.total_interest_payable) - flt(loan.total_interest_payable)
- flt(loan.written_off_amount) - flt(loan.written_off_amount)
+ flt(loan.refund_amount)
) )
else: else:
pending_principal_amount = ( pending_principal_amount = (
flt(loan.disbursed_amount) flt(loan.disbursed_amount)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid) - flt(loan.total_principal_paid)
- flt(loan.total_interest_payable) - flt(loan.total_interest_payable)
- flt(loan.written_off_amount) - flt(loan.written_off_amount)
+ flt(loan.refund_amount)
) )
return pending_principal_amount return pending_principal_amount

View File

@ -7,6 +7,8 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"loan", "loan",
"applicant_type",
"applicant",
"status", "status",
"column_break_3", "column_break_3",
"shortfall_time", "shortfall_time",
@ -23,6 +25,8 @@
{ {
"fieldname": "loan", "fieldname": "loan",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan ", "label": "Loan ",
"options": "Loan", "options": "Loan",
"read_only": 1 "read_only": 1
@ -91,17 +95,35 @@
{ {
"fieldname": "shortfall_percentage", "fieldname": "shortfall_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"in_list_view": 1,
"label": "Shortfall Percentage", "label": "Shortfall Percentage",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer"
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-04-01 08:13:43.263772", "modified": "2022-06-30 11:57:09.378089",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Security Shortfall", "name": "Loan Security Shortfall",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -132,5 +154,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -54,17 +54,18 @@
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1, "hidden": 1,
"label": "Accrual Type", "label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement", "options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund",
"read_only": 1 "read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-06 13:28:51.478909", "modified": "2022-06-29 11:19:33.203088",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Process Loan Interest Accrual", "name": "Process Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -98,5 +99,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -9,7 +9,7 @@ import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import cstr, flt 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.bom import BOMRecursionError, item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import ( from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test, update_cost_in_all_boms_in_test,
@ -18,7 +18,6 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records("BOM") test_records = frappe.get_test_records("BOM")
test_dependencies = ["Item", "Quality Inspection Template"] test_dependencies = ["Item", "Quality Inspection Template"]
@ -256,12 +255,29 @@ class TestBOM(FrappeTestCase):
bom.submit() bom.submit()
# test that sourced_by_supplier rate is zero even after updating cost # test that sourced_by_supplier rate is zero even after updating cost
self.assertEqual(bom.items[2].rate, 0) self.assertEqual(bom.items[2].rate, 0)
# test in Purchase Order sourced_by_supplier is not added to Supplied Item
po = create_purchase_order( from erpnext.controllers.tests.test_subcontracting_controller import (
item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC" 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]) 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) self.assertEqual(bom_items, supplied_items)
def test_bom_tree_representation(self): def test_bom_tree_representation(self):

View File

@ -508,7 +508,7 @@ class ProductionPlan(Document):
po.is_subcontracted = 1 po.is_subcontracted = 1
for row in po_list: for row in po_list:
po_data = { po_data = {
"item_code": row.production_item, "fg_item": row.production_item,
"warehouse": row.fg_warehouse, "warehouse": row.fg_warehouse,
"production_plan_sub_assembly_item": row.name, "production_plan_sub_assembly_item": row.name,
"bom": row.bom_no, "bom": row.bom_no,
@ -518,9 +518,6 @@ class ProductionPlan(Document):
for field in [ for field in [
"schedule_date", "schedule_date",
"qty", "qty",
"uom",
"stock_uom",
"item_name",
"description", "description",
"production_plan_item", "production_plan_item",
]: ]:

View File

@ -36,7 +36,7 @@ def get_data(filters):
"total_time_in_mins", "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): if filters.get(field):
query_filters[field] = ("in", filters.get(field)) query_filters[field] = ("in", filters.get(field))

View File

@ -19,3 +19,4 @@ Loan Management
Telephony Telephony
Bulk Transaction Bulk Transaction
E-commerce E-commerce
Subcontracting

View File

@ -301,6 +301,7 @@ erpnext.patches.v13_0.create_accounting_dimensions_in_orders
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
execute:frappe.delete_doc("DocType", "Naming Series") execute:frappe.delete_doc("DocType", "Naming Series")
erpnext.patches.v13_0.job_card_status_on_hold 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.migrate_gl_to_payment_ledger
erpnext.patches.v14_0.crm_ux_cleanup erpnext.patches.v14_0.crm_ux_cleanup
erpnext.patches.v14_0.remove_india_localisation erpnext.patches.v14_0.remove_india_localisation

View File

@ -64,4 +64,8 @@ def delete_and_patch_duplicate_bins():
bin.update(qty_dict) bin.update(qty_dict)
bin.update_reserved_qty_for_production() bin.update_reserved_qty_for_production()
bin.update_reserved_qty_for_sub_contracting() 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() bin.db_update()

View File

@ -15,6 +15,8 @@ def execute():
("accounts", "sales_invoice_item"), ("accounts", "sales_invoice_item"),
("accounts", "purchase_invoice_item"), ("accounts", "purchase_invoice_item"),
("buying", "purchase_receipt_item_supplied"), ("buying", "purchase_receipt_item_supplied"),
("subcontracting", "subcontracting_receipt_item"),
("subcontracting", "subcontracting_receipt_supplied_item"),
] ]
for module, doctype in doctypes_to_reload: for module, doctype in doctypes_to_reload:

View File

@ -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()

View File

@ -83,9 +83,17 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
this.frm.set_query("item_code", "items", function() { this.frm.set_query("item_code", "items", function() {
if (me.frm.doc.is_subcontracted) { 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{ return{
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 } filters: filters
} }
} }
else { else {

View File

@ -470,7 +470,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
cost_center: item.cost_center, cost_center: item.cost_center,
tax_category: me.frm.doc.tax_category, tax_category: me.frm.doc.tax_category,
item_tax_template: item.item_tax_template, 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,
} }
}, },

View File

@ -475,7 +475,11 @@ erpnext.utils.update_child_items = function(opts) {
filters = {"is_sales_item": 1}; filters = {"is_sales_item": 1};
} else if (frm.doc.doctype == 'Purchase Order') { } else if (frm.doc.doctype == 'Purchase Order') {
if (frm.doc.is_subcontracted) { 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 { } else {
filters = {"is_purchase_item": 1}; filters = {"is_purchase_item": 1};
} }

View File

@ -4,7 +4,7 @@
"doctype": "Form Tour", "doctype": "Form Tour",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"modified": "2021-06-29 20:49:01.359489", "modified": "2022-07-11 20:49:01.359489",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",
@ -33,7 +33,7 @@
"is_table_field": 0, "is_table_field": 0,
"label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?", "label": "Is Sales Order Required for Sales Invoice & Delivery Note Creation?",
"parent_field": "", "parent_field": "",
"position": "Left", "position": "Right",
"title": "Sales Order Required for Sales Invoice & Delivery Note Creation" "title": "Sales Order Required for Sales Invoice & Delivery Note Creation"
}, },
{ {
@ -45,7 +45,7 @@
"is_table_field": 0, "is_table_field": 0,
"label": "Is Delivery Note Required for Sales Invoice Creation?", "label": "Is Delivery Note Required for Sales Invoice Creation?",
"parent_field": "", "parent_field": "",
"position": "Left", "position": "Right",
"title": "Delivery Note Required for Sales Invoice Creation" "title": "Delivery Note Required for Sales Invoice Creation"
}, },
{ {

View File

@ -40,25 +40,37 @@ class Bin(Document):
self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production)) self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production))
self.db_set("projected_qty", self.projected_qty) 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 # reserved qty
po = frappe.qb.DocType("Purchase Order") subcontract_order = frappe.qb.DocType(subcontract_doctype)
supplied_item = frappe.qb.DocType("Purchase Order Item Supplied") 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 = ( reserved_qty_for_sub_contract = (
frappe.qb.from_(po) frappe.qb.from_(subcontract_order)
.from_(supplied_item) .from_(supplied_item)
.select(Sum(Coalesce(supplied_item.required_qty, 0))) .select(Sum(Coalesce(supplied_item.required_qty, 0)))
.where( .where(conditions)
(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)
)
).run()[0][0] or 0.0 ).run()[0][0] or 0.0
se = frappe.qb.DocType("Stock Entry") se = frappe.qb.DocType("Stock Entry")
@ -71,23 +83,34 @@ class Bin(Document):
else: else:
qty_field = se_item.transfer_qty 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 = ( materials_transferred = (
frappe.qb.from_(se) frappe.qb.from_(se)
.from_(se_item) .from_(se_item)
.from_(po) .from_(subcontract_order)
.select(Sum(qty_field)) .select(Sum(qty_field))
.where( .where(conditions)
(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)
)
).run()[0][0] or 0.0 ).run()[0][0] or 0.0
if reserved_qty_for_sub_contract > materials_transferred: if reserved_qty_for_sub_contract > materials_transferred:

View File

@ -1,17 +1,16 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import json
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import ( from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
make_purchase_receipt, from erpnext.controllers.tests.test_subcontracting_controller import (
make_rm_stock_entry, 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.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.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
@ -22,6 +21,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt,
)
class TestItemAlternative(FrappeTestCase): class TestItemAlternative(FrappeTestCase):
@ -30,9 +32,7 @@ class TestItemAlternative(FrappeTestCase):
make_items() make_items()
def test_alternative_item_for_subcontract_rm(self): def test_alternative_item_for_subcontract_rm(self):
frappe.db.set_value( set_backflush_based_on("BOM")
"Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
)
create_stock_reconciliation( create_stock_reconciliation(
item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000 item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
@ -42,15 +42,22 @@ class TestItemAlternative(FrappeTestCase):
) )
supplier_warehouse = "Test Supplier Warehouse - _TC" 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", "item_code": "Test Finished Goods - A",
"rm_item_code": "Test FG A RW 1", "rm_item_code": "Test FG A RW 1",
@ -73,14 +80,13 @@ class TestItemAlternative(FrappeTestCase):
}, },
] ]
rm_item_string = json.dumps(rm_item)
reserved_qty_for_sub_contract = frappe.db.get_value( reserved_qty_for_sub_contract = frappe.db.get_value(
"Bin", "Bin",
{"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"}, {"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
"reserved_qty_for_sub_contract", "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.to_warehouse = supplier_warehouse
se.insert() se.insert()
@ -104,22 +110,17 @@ class TestItemAlternative(FrappeTestCase):
after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5) after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)
) )
pr = make_purchase_receipt(po.name) scr = make_subcontracting_receipt(sco.name)
pr.save() scr.save()
pr = frappe.get_doc("Purchase Receipt", pr.name) scr = frappe.get_doc("Subcontracting Receipt", scr.name)
status = False status = False
for d in pr.supplied_items: for item in scr.supplied_items:
if d.rm_item_code == "Alternate Item For A RW 1": if item.rm_item_code == "Alternate Item For A RW 1":
status = True status = True
self.assertEqual(status, True) self.assertEqual(status, True)
frappe.db.set_value( set_backflush_based_on("Material Transferred for Subcontract")
"Buying Settings",
None,
"backflush_raw_materials_of_subcontract_based_on",
"Material Transferred for Subcontract",
)
def test_alternative_item_for_production_rm(self): def test_alternative_item_for_production_rm(self):
create_stock_reconciliation( create_stock_reconciliation(

View File

@ -198,7 +198,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status")) 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() { make_purchase_invoice() {
@ -296,10 +296,11 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt
frappe.provide("erpnext.buying"); frappe.provide("erpnext.buying");
frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) { 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); 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', { frappe.ui.form.on('Purchase Receipt Item', {

View File

@ -133,7 +133,8 @@
"transporter_name", "transporter_name",
"column_break5", "column_break5",
"lr_no", "lr_no",
"lr_date" "lr_date",
"is_old_subcontracting_flow"
], ],
"fields": [ "fields": [
{ {
@ -442,7 +443,8 @@
"label": "Is Subcontracted", "label": "Is Subcontracted",
"oldfieldname": "is_subcontracted", "oldfieldname": "is_subcontracted",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"depends_on": "eval:doc.is_subcontracted", "depends_on": "eval:doc.is_subcontracted",
@ -1142,13 +1144,21 @@
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column 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", "icon": "fa fa-truck",
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-05-27 15:59:18.550583", "modified": "2022-06-15 15:43:40.664382",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@ -123,6 +123,7 @@ class PurchaseReceipt(BuyingController):
if getdate(self.posting_date) > getdate(nowdate()): if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date")) 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("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
@ -234,7 +235,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.set_consumed_qty_in_po() self.set_consumed_qty_in_subcontract_order()
def check_next_docstatus(self): def check_next_docstatus(self):
submit_rv = frappe.db.sql( submit_rv = frappe.db.sql(
@ -270,18 +271,7 @@ class PurchaseReceipt(BuyingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_po() self.set_consumed_qty_in_subcontract_order()
@frappe.whitelist()
def get_current_stock(self):
for d in self.get("supplied_items"):
if self.supplier_warehouse:
bin = frappe.db.sql(
"select actual_qty from `tabBin` where item_code = %s and warehouse = %s",
(d.rm_item_code, self.supplier_warehouse),
as_dict=1,
)
d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0
def get_gl_entries(self, warehouse_account=None): def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map

View File

@ -2,10 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import json
import unittest
from collections import defaultdict
import frappe import frappe
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today from frappe.utils import add_days, cint, cstr, flt, today
@ -311,142 +307,6 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.cancel() pr.cancel()
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name)) 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): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
pr_row_1_serial_no = pr.get("items")[0].serial_no pr_row_1_serial_no = pr.get("items")[0].serial_no
@ -1133,103 +993,6 @@ class TestPurchaseReceipt(FrappeTestCase):
pr.cancel() pr.cancel()
pr1.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): def test_po_to_pi_and_po_to_pr_worflow_full(self):
"""Test following behaviour: """Test following behaviour:
- Create PO - Create PO
@ -1568,43 +1331,5 @@ def make_purchase_receipt(**args):
return pr 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_dependencies = ["BOM", "Item Price", "Location"]
test_records = frappe.get_test_records("Purchase Receipt") test_records = frappe.get_test_records("Purchase Receipt")

View File

@ -83,37 +83,5 @@
} }
], ],
"supplier": "_Test Supplier" "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"
} }
] ]

View File

@ -645,12 +645,15 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom", "fieldname": "bom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "BOM", "label": "BOM",
"no_copy": 1, "no_copy": 1,
"options": "BOM", "options": "BOM",
"print_hide": 1 "print_hide": 1,
"read_only": 1,
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
}, },
{ {
"default": "0", "default": "0",

View File

@ -138,6 +138,11 @@ def repost(doc):
doc.set_status("Completed") doc.set_status("Completed")
except Exception as e: 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() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
doc.log_error("Unable to repost item valuation") doc.log_error("Unable to repost item valuation")

View File

@ -687,7 +687,10 @@ def update_serial_nos_after_submit(controller, parentfield):
update_rejected_serial_nos = ( update_rejected_serial_nos = (
True 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 else False
) )
accepted_serial_nos_updated = False accepted_serial_nos_updated = False
@ -700,7 +703,11 @@ def update_serial_nos_after_submit(controller, parentfield):
qty = d.stock_qty qty = d.stock_qty
else: else:
warehouse = d.warehouse 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: for sle in stock_ledger_entries:
if sle.voucher_detail_no == d.name: if sle.voucher_detail_no == d.name:
if ( if (

View File

@ -613,7 +613,25 @@ frappe.ui.form.on('Stock Entry', {
apply_putaway_rule: function (frm) { apply_putaway_rule: function (frm) {
if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose); 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', { frappe.ui.form.on('Stock Entry Detail', {
@ -780,7 +798,16 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
return { return {
"filters": { "filters": {
"docstatus": 1, "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 "company": me.frm.doc.company
} }
}; };
@ -801,7 +828,12 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
} }
} }
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' } frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' }
this.frm.set_query("supplier_address", erpnext.queries.address_query) this.frm.set_query("supplier_address", erpnext.queries.address_query)

View File

@ -15,6 +15,7 @@
"add_to_transit", "add_to_transit",
"work_order", "work_order",
"purchase_order", "purchase_order",
"subcontracting_order",
"delivery_note_no", "delivery_note_no",
"sales_invoice_no", "sales_invoice_no",
"pick_list", "pick_list",
@ -147,12 +148,19 @@
"search_index": 1 "search_index": 1
}, },
{ {
"depends_on": "eval:doc.purpose==\"Send to Subcontractor\"", "depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
"fieldname": "purchase_order", "fieldname": "purchase_order",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Order", "label": "Purchase Order",
"options": "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\"", "depends_on": "eval:doc.purpose==\"Sales Return\"",
"fieldname": "delivery_note_no", "fieldname": "delivery_note_no",

View File

@ -62,6 +62,27 @@ form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
class StockEntry(StockController): 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): def get_feed(self):
return self.stock_entry_type return self.stock_entry_type
@ -134,8 +155,9 @@ class StockEntry(StockController):
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
self.update_work_order() self.update_work_order()
self.validate_purchase_order() self.validate_subcontract_order()
self.update_purchase_order_supplied_items() self.update_subcontract_order_supplied_items()
self.update_subcontracting_order_status()
self.make_gl_entries() self.make_gl_entries()
@ -154,7 +176,8 @@ class StockEntry(StockController):
self.set_material_request_transfer_status("Completed") self.set_material_request_transfer_status("Completed")
def on_cancel(self): 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": if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status() self.validate_work_order_status()
@ -792,8 +815,8 @@ class StockEntry(StockController):
serial_nos.append(sn) serial_nos.append(sn)
def validate_purchase_order(self): def validate_subcontract_order(self):
"""Throw exception if more raw material is transferred against Purchase Order than in """Throw exception if more raw material is transferred against Subcontract Order than in
the raw materials supplied table""" the raw materials supplied table"""
backflush_raw_materials_based_on = frappe.db.get_single_value( backflush_raw_materials_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on" "Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
@ -801,24 +824,29 @@ class StockEntry(StockController):
qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance")) 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 return
if backflush_raw_materials_based_on == "BOM": 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: for se_item in self.items:
item_code = se_item.original_item or se_item.item_code item_code = se_item.original_item or se_item.item_code
precision = cint(frappe.db.get_default("float_precision")) or 3 precision = cint(frappe.db.get_default("float_precision")) or 3
required_qty = sum( 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)) total_allowed = required_qty + (required_qty * (qty_allowance / 100))
if not required_qty: if not required_qty:
bom_no = frappe.db.get_value( bom_no = frappe.db.get_value(
"Purchase Order Item", f"{self.subcontract_data.order_doctype} Item",
{"parent": self.purchase_order, "item_code": se_item.subcontracted_item}, {
"parent": self.get(self.subcontract_data.order_field),
"item_code": se_item.subcontracted_item,
},
"bom", "bom",
) )
@ -830,7 +858,7 @@ class StockEntry(StockController):
required_qty = sum( required_qty = sum(
[ [
flt(d.required_qty) 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 if d.rm_item_code == original_item_code
] ]
) )
@ -839,26 +867,57 @@ class StockEntry(StockController):
if not required_qty: if not required_qty:
frappe.throw( frappe.throw(
_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format( _("Item {0} not found in 'Raw Materials Supplied' table in {1} {2}").format(
se_item.item_code, self.purchase_order 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) parent = frappe.qb.DocType("Stock Entry")
from `tabStock Entry Detail`, `tabStock Entry` child = frappe.qb.DocType("Stock Entry Detail")
where `tabStock Entry`.purchase_order = %s
and `tabStock Entry`.docstatus = 1 conditions = (
and `tabStock Entry Detail`.item_code = %s (parent.docstatus == 1)
and `tabStock Entry Detail`.parent = `tabStock Entry`.name""", & (child.item_code == se_item.item_code)
(self.purchase_order, se_item.item_code), & (
)[0][0] (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): if flt(total_supplied, precision) > flt(total_allowed, precision):
frappe.throw( frappe.throw(
_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format( _("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format(
se_item.idx, se_item.item_code, total_allowed, self.purchase_order 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": elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
for row in self.items: for row in self.items:
if not row.subcontracted_item: if not row.subcontracted_item:
@ -867,17 +926,19 @@ class StockEntry(StockController):
row.idx, frappe.bold(row.item_code) row.idx, frappe.bold(row.item_code)
) )
) )
elif not row.po_detail: elif not row.get(self.subcontract_data.rm_detail_field):
filters = { filters = {
"parent": self.purchase_order, "parent": self.get(self.subcontract_data.order_field),
"docstatus": 1, "docstatus": 1,
"rm_item_code": row.item_code, "rm_item_code": row.item_code,
"main_item_code": row.subcontracted_item, "main_item_code": row.subcontracted_item,
} }
po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name") order_rm_detail = frappe.db.get_value(
if po_detail: self.subcontract_data.order_supplied_items_field, filters, "name"
row.db_set("po_detail", po_detail) )
if order_rm_detail:
row.db_set(self.subcontract_data.rm_detail_field, order_rm_detail)
def validate_bom(self): def validate_bom(self):
for d in self.get("items"): for d in self.get("items"):
@ -1224,11 +1285,13 @@ class StockEntry(StockController):
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"]) args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
if ( 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( subcontract_items = frappe.get_all(
"Purchase Order Item Supplied", self.subcontract_data.order_supplied_items_field,
{"parent": self.purchase_order, "rm_item_code": args.get("item_code")}, {"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")},
"main_item_code", "main_item_code",
) )
@ -1322,27 +1385,27 @@ class StockEntry(StockController):
item_dict = self.get_bom_raw_materials(self.fg_completed_qty) item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
# Get PO Supplied Items Details # Get Subcontract Order Supplied Items Details
if self.purchase_order and self.purpose == "Send to Subcontractor": if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
# Get PO Supplied Items Details # Get Subcontract Order Supplied Items Details
item_wh = frappe._dict( parent = frappe.qb.DocType(self.subcontract_data.order_doctype)
frappe.db.sql( child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field)
"""
SELECT item_wh = (
rm_item_code, reserve_warehouse frappe.qb.from_(parent)
FROM .inner_join(child)
`tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup .on(parent.name == child.parent)
WHERE .select(child.rm_item_code, child.reserve_warehouse)
po.name = poitemsup.parent and po.name = %s """, .where(parent.name == self.get(self.subcontract_data.order_field))
self.purchase_order, ).run(as_list=True)
)
) item_wh = frappe._dict(item_wh)
for item in item_dict.values(): for item in item_dict.values():
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
item["from_warehouse"] = self.pro_doc.wip_warehouse item["from_warehouse"] = self.pro_doc.wip_warehouse
# Get Reserve Warehouse from PO # Get Reserve Warehouse from Subcontract Order
if self.purchase_order and self.purpose == "Send to Subcontractor": if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
item["from_warehouse"] = item_wh.get(item.item_code) item["from_warehouse"] = item_wh.get(item.item_code)
item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else "" item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
@ -1478,7 +1541,9 @@ class StockEntry(StockController):
fetch_qty_in_stock_uom=False, 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(): for item in item_dict.values():
# if source warehouse presents in BOM set from_warehouse as bom source_warehouse # if source warehouse presents in BOM set from_warehouse as bom source_warehouse
if item["allow_alternative_item"]: if item["allow_alternative_item"]:
@ -1844,7 +1909,7 @@ class StockEntry(StockController):
se_child.is_process_loss = item_row.get("is_process_loss", 0) se_child.is_process_loss = item_row.get("is_process_loss", 0)
for field in [ for field in [
"po_detail", self.subcontract_data.rm_detail_field,
"original_item", "original_item",
"expense_account", "expense_account",
"description", "description",
@ -1918,33 +1983,37 @@ class StockEntry(StockController):
else: else:
frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code)) frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code))
def update_purchase_order_supplied_items(self): def update_subcontract_order_supplied_items(self):
if self.purchase_order and ( if self.get(self.subcontract_data.order_field) and (
self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
): ):
# Get PO Supplied Items Details # Get Subcontract Order Supplied Items Details
po_supplied_items = frappe.db.get_all( order_supplied_items = frappe.db.get_all(
"Purchase Order Item Supplied", self.subcontract_data.order_supplied_items_field,
filters={"parent": self.purchase_order}, filters={"parent": self.get(self.subcontract_data.order_field)},
fields=["name", "rm_item_code", "reserve_warehouse"], fields=["name", "rm_item_code", "reserve_warehouse"],
) )
# Get Items Supplied in Stock Entries against PO # Get Items Supplied in Stock Entries against Subcontract Order
supplied_items = get_supplied_items(self.purchase_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, {} key, item = row.name, {}
if not supplied_items.get(key): 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} item = {"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}
else: else:
item = supplied_items.get(key) 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 # 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"): for d in self.get("items"):
# Update reserved sub contracted quantity in bin based on Supplied Item Details and # Update reserved sub contracted quantity in bin based on Supplied Item Details and
@ -2145,6 +2214,14 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) 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): def set_missing_values(self):
"Updates rate and availability of all the items of mapped doc." "Updates rate and availability of all the items of mapped doc."
self.set_transfer_qty() self.set_transfer_qty()
@ -2293,13 +2370,13 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
return operating_cost_per_unit 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 = "" cond = ""
if purchase_order: if subcontract_order:
cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format( cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'"
purchase_order
)
elif work_order: elif work_order:
cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format( cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
work_order work_order
@ -2352,7 +2429,6 @@ def get_valuation_rate_for_finished_good_entry(work_order):
@frappe.whitelist() @frappe.whitelist()
def get_uom_details(item_code, uom, qty): def get_uom_details(item_code, uom, qty):
"""Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}` """Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}`
:param args: dict with `item_code`, `uom` and `qty`""" :param args: dict with `item_code`, `uom` and `qty`"""
conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor") conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor")
@ -2436,25 +2512,27 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None):
return sample_quantity 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 = [ fields = [
"`tabStock Entry Detail`.`transfer_qty`", "`tabStock Entry Detail`.`transfer_qty`",
"`tabStock Entry`.`is_return`", "`tabStock Entry`.`is_return`",
"`tabStock Entry Detail`.`po_detail`", f"`tabStock Entry Detail`.`{rm_detail_field}`",
"`tabStock Entry Detail`.`item_code`", "`tabStock Entry Detail`.`item_code`",
] ]
filters = [ filters = [
["Stock Entry", "docstatus", "=", 1], ["Stock Entry", "docstatus", "=", 1],
["Stock Entry", "purchase_order", "=", purchase_order], ["Stock Entry", subcontract_order_field, "=", subcontract_order],
] ]
supplied_item_details = {} supplied_item_details = {}
for row in frappe.get_all("Stock Entry", fields=fields, filters=filters): 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 continue
key = row.po_detail key = row.get(rm_detail_field)
if key not in supplied_item_details: if key not in supplied_item_details:
supplied_item_details.setdefault( supplied_item_details.setdefault(
key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}) key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0})
@ -2474,6 +2552,39 @@ def get_supplied_items(purchase_order):
return supplied_item_details 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: def get_available_materials(work_order) -> dict:
data = get_stock_entry_data(work_order) data = get_stock_entry_data(work_order)

View File

@ -68,6 +68,7 @@
"against_stock_entry", "against_stock_entry",
"ste_detail", "ste_detail",
"po_detail", "po_detail",
"sco_rm_detail",
"putaway_rule", "putaway_rule",
"column_break_51", "column_break_51",
"reference_purchase_receipt", "reference_purchase_receipt",
@ -496,6 +497,15 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "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", "default": "0",
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse", "depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",

View File

@ -409,61 +409,6 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
lcv.cancel() lcv.cancel()
pr.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): def test_back_dated_entry_not_allowed(self):
# Back dated stock transactions are only allowed to stock managers # Back dated stock transactions are only allowed to stock managers
frappe.db.set_value( frappe.db.set_value(

View File

@ -238,8 +238,13 @@ def validate_item_details(args, item):
throw(_("Item {0} is a template, please select one of its variants").format(item.name)) 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": elif args.transaction_type == "buying" and args.doctype != "Material Request":
if args.get("is_subcontracted") and item.is_sub_contracted_item != 1: if args.get("is_subcontracted"):
throw(_("Item {0} must be a Sub-contracted Item").format(item.name)) 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): def get_basic_details(args, item, overwrite_warehouse=True):

View File

@ -7,7 +7,7 @@
- `controllers/stock_controller.py` - `controllers/stock_controller.py`
- `stock/valuation.py` - `stock/valuation.py`
## What is in an Stock Ledger Entry (SLE)? ## What is in a Stock Ledger Entry (SLE)?
Stock Ledger Entry is a single row in the Stock Ledger. It signifies some Stock Ledger Entry is a single row in the Stock Ledger. It signifies some
modification of stock for a particular Item in the specified warehouse. modification of stock for a particular Item in the specified warehouse.

View File

@ -630,6 +630,7 @@ class update_entries_after(object):
"Purchase Invoice", "Purchase Invoice",
"Delivery Note", "Delivery Note",
"Sales Invoice", "Sales Invoice",
"Subcontracting Receipt",
): ):
if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
from erpnext.controllers.sales_and_purchase_return import ( from erpnext.controllers.sales_and_purchase_return import (
@ -646,6 +647,8 @@ class update_entries_after(object):
else: else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate" rate_field = "valuation_rate"
elif sle.voucher_type == "Subcontracting Receipt":
rate_field = "rate"
else: else:
rate_field = "incoming_rate" rate_field = "incoming_rate"
@ -659,6 +662,8 @@ class update_entries_after(object):
else: else:
if sle.voucher_type in ("Delivery Note", "Sales Invoice"): if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
ref_doctype = "Packed Item" ref_doctype = "Packed Item"
elif sle == "Subcontracting Receipt":
ref_doctype = "Subcontracting Receipt Supplied Item"
else: else:
ref_doctype = "Purchase Receipt Item Supplied" ref_doctype = "Purchase Receipt Item Supplied"
@ -684,6 +689,8 @@ class update_entries_after(object):
self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate) 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"): elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
self.update_rate_on_purchase_receipt(sle, outgoing_rate) 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): 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) frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
@ -732,6 +739,14 @@ class update_entries_after(object):
for d in doc.items + doc.supplied_items: for d in doc.items + doc.supplied_items:
d.db_update() 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): def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate) incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty) actual_qty = flt(sle.actual_qty)

View File

View File

@ -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 }));

View File

@ -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
}

View File

@ -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()

View File

@ -0,0 +1,8 @@
from frappe import _
def get_data():
return {
"fieldname": "subcontracting_order",
"transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
}

View File

@ -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];
},
};

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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": []
}

View File

@ -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

View File

@ -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": []
}

View File

@ -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

View File

@ -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();
},
});
};

View File

@ -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
}

View File

@ -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)

View File

@ -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"]},
],
}

View File

@ -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];
},
};

Some files were not shown because too many files have changed in this diff Show More