Merge pull request #30955 from s-aga-r/subcontracting
feat: subcontracting module
This commit is contained in:
commit
8b69a3ec7a
@ -572,9 +572,10 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
},
|
||||
|
||||
is_subcontracted: function(frm) {
|
||||
if (frm.doc.is_subcontracted) {
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
erpnext.buying.get_default_bom(frm);
|
||||
}
|
||||
|
||||
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
|
||||
},
|
||||
|
||||
|
@ -169,7 +169,8 @@
|
||||
"column_break_114",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
"per_received"
|
||||
"per_received",
|
||||
"is_old_subcontracting_flow"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -547,7 +548,8 @@
|
||||
"fieldname": "is_subcontracted",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Subcontracted",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
@ -1365,7 +1367,7 @@
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.update_stock && doc.is_subcontracted",
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Warehouse",
|
||||
@ -1416,13 +1418,21 @@
|
||||
"label": "Advance Tax",
|
||||
"options": "Advance Tax",
|
||||
"read_only": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_old_subcontracting_flow",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Old Subcontracting Flow",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-25 13:31:02.716727",
|
||||
"modified": "2022-06-15 15:40:58.527065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
@ -502,7 +502,10 @@ class PurchaseInvoice(BuyingController):
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
|
||||
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
@ -1405,7 +1408,9 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
|
@ -470,37 +470,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
||||
self.assertEqual(tax.total, expected_values[i][2])
|
||||
|
||||
def test_purchase_invoice_with_subcontracted_item(self):
|
||||
wrapper = frappe.copy_doc(test_records[0])
|
||||
wrapper.get("items")[0].item_code = "_Test FG Item"
|
||||
wrapper.insert()
|
||||
wrapper.load_from_db()
|
||||
|
||||
expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]]
|
||||
for i, item in enumerate(wrapper.get("items")):
|
||||
self.assertEqual(item.item_code, expected_values[i][0])
|
||||
self.assertEqual(item.item_tax_amount, expected_values[i][1])
|
||||
self.assertEqual(item.valuation_rate, expected_values[i][2])
|
||||
|
||||
self.assertEqual(wrapper.base_net_total, 1250)
|
||||
|
||||
# tax amounts
|
||||
expected_values = [
|
||||
["_Test Account Shipping Charges - _TC", 100, 1350],
|
||||
["_Test Account Customs Duty - _TC", 125, 1350],
|
||||
["_Test Account Excise Duty - _TC", 140, 1490],
|
||||
["_Test Account Education Cess - _TC", 2.8, 1492.8],
|
||||
["_Test Account S&H Education Cess - _TC", 1.4, 1494.2],
|
||||
["_Test Account CST - _TC", 29.88, 1524.08],
|
||||
["_Test Account VAT - _TC", 156.25, 1680.33],
|
||||
["_Test Account Discount - _TC", 168.03, 1512.30],
|
||||
]
|
||||
|
||||
for i, tax in enumerate(wrapper.get("taxes")):
|
||||
self.assertEqual(tax.account_head, expected_values[i][0])
|
||||
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
||||
self.assertEqual(tax.total, expected_values[i][2])
|
||||
|
||||
def test_purchase_invoice_with_advance(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@ -961,30 +930,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
pi.cancel()
|
||||
self.assertEqual(actual_qty_0, get_qty_after_transaction())
|
||||
|
||||
def test_subcontracting_via_purchase_invoice(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
make_stock_entry(
|
||||
item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1
|
||||
)
|
||||
|
||||
self.assertEqual(len(pi.get("supplied_items")), 2)
|
||||
|
||||
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
|
||||
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
|
||||
|
||||
def test_rejected_serial_no(self):
|
||||
pi = make_purchase_invoice(
|
||||
item_code="_Test Serialized Item With Series",
|
||||
|
@ -619,10 +619,13 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_old_subcontracting_flow",
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"label": "BOM",
|
||||
"options": "BOM"
|
||||
"options": "BOM",
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
@ -8,15 +8,17 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
frappe.ui.form.on("Purchase Order", {
|
||||
setup: function(frm) {
|
||||
|
||||
frm.set_query("reserve_warehouse", "supplied_items", function() {
|
||||
return {
|
||||
filters: {
|
||||
"company": frm.doc.company,
|
||||
"name": ['!=', frm.doc.supplier_warehouse],
|
||||
"is_group": 0
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
frm.set_query("reserve_warehouse", "supplied_items", function() {
|
||||
return {
|
||||
filters: {
|
||||
"company": frm.doc.company,
|
||||
"name": ['!=', frm.doc.supplier_warehouse],
|
||||
"is_group": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
frm.set_indicator_formatter('item_code',
|
||||
function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" })
|
||||
@ -28,12 +30,67 @@ 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) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.is_old_subcontracting_flow) {
|
||||
frm.trigger('get_materials_from_supplier');
|
||||
|
||||
$('a.grey-link').each(function () {
|
||||
var id = $(this).children(':first-child').attr('data-label');
|
||||
if (id == 'Duplicate') {
|
||||
$(this).remove();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function(frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (po_details && po_details.length) {
|
||||
frm.add_custom_button(__('Return of Components'), () => {
|
||||
frm.call({
|
||||
method: 'erpnext.controllers.subcontracting_controller.get_materials_from_supplier',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating Stock Entry'),
|
||||
args: {
|
||||
subcontract_order: frm.doc.name,
|
||||
rm_details: po_details,
|
||||
order_doctype: cur_frm.doc.doctype
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, __('Create'));
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
set_schedule_date(frm);
|
||||
if (!frm.doc.transaction_date){
|
||||
@ -52,39 +109,6 @@ frappe.ui.form.on("Purchase Order", {
|
||||
frm.set_value("tax_withholding_category", frm.supplier_tds);
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.trigger('get_materials_from_supplier');
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function(frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (po_details && po_details.length) {
|
||||
frm.add_custom_button(__('Return of Components'), () => {
|
||||
frm.call({
|
||||
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating Stock Entry'),
|
||||
args: { purchase_order: frm.doc.name, po_details: po_details },
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Purchase Order Item", {
|
||||
@ -97,6 +121,16 @@ frappe.ui.form.on("Purchase Order Item", {
|
||||
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 = {
|
||||
'Purchase Receipt': 'Purchase Receipt',
|
||||
'Purchase Invoice': 'Purchase Invoice',
|
||||
'Stock Entry': 'Material to Supplier',
|
||||
'Payment Entry': 'Payment',
|
||||
'Subcontracting Order': 'Subcontracting Order',
|
||||
'Stock Entry': 'Material to Supplier'
|
||||
}
|
||||
|
||||
super.setup();
|
||||
|
||||
}
|
||||
|
||||
refresh(doc, cdt, cdn) {
|
||||
@ -142,14 +176,17 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
|
||||
if(!in_list(["Closed", "Delivered"], doc.status)) {
|
||||
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
|
||||
this.frm.add_custom_button(__('Update Items'), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
child_docname: "items",
|
||||
child_doctype: "Purchase Order Detail",
|
||||
cannot_add_row: false,
|
||||
})
|
||||
});
|
||||
// Don't add Update Items button if the PO is following the new subcontracting flow.
|
||||
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
|
||||
this.frm.add_custom_button(__('Update Items'), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
child_docname: "items",
|
||||
child_doctype: "Purchase Order Detail",
|
||||
cannot_add_row: false,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
if (this.frm.has_perm("submit")) {
|
||||
if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) {
|
||||
@ -177,9 +214,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
if (doc.status != "On Hold") {
|
||||
if(flt(doc.per_received) < 100 && allow_receipt) {
|
||||
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
|
||||
if(doc.is_subcontracted && me.has_unsupplied_items()) {
|
||||
cur_frm.add_custom_button(__('Material to Supplier'),
|
||||
function() { me.make_stock_entry(); }, __("Transfer"));
|
||||
if (doc.is_subcontracted) {
|
||||
if (doc.is_old_subcontracting_flow) {
|
||||
if (me.has_unsupplied_items()) {
|
||||
cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer"));
|
||||
}
|
||||
}
|
||||
else {
|
||||
cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create'));
|
||||
}
|
||||
}
|
||||
}
|
||||
if(flt(doc.per_billed) < 100)
|
||||
@ -370,10 +413,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
|
||||
_make_rm_stock_entry(rm_items) {
|
||||
frappe.call({
|
||||
method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry",
|
||||
method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry",
|
||||
args: {
|
||||
purchase_order: cur_frm.doc.name,
|
||||
rm_items: rm_items
|
||||
subcontract_order: cur_frm.doc.name,
|
||||
rm_items: rm_items,
|
||||
order_doctype: cur_frm.doc.doctype
|
||||
}
|
||||
,
|
||||
callback: function(r) {
|
||||
@ -405,6 +449,14 @@ 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() {
|
||||
var me = this;
|
||||
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) {
|
||||
var d = locals[cdt][cdn]
|
||||
return {
|
||||
filters: [
|
||||
['BOM', 'item', '=', d.item_code],
|
||||
['BOM', 'is_active', '=', '1'],
|
||||
['BOM', 'docstatus', '=', '1'],
|
||||
['BOM', 'company', '=', doc.company]
|
||||
]
|
||||
if (cur_frm.doc.is_old_subcontracting_flow) {
|
||||
cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn]
|
||||
return {
|
||||
filters: [
|
||||
['BOM', 'item', '=', d.item_code],
|
||||
['BOM', 'is_active', '=', '1'],
|
||||
['BOM', 'docstatus', '=', '1'],
|
||||
['BOM', 'company', '=', doc.company]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,7 +688,7 @@ function set_schedule_date(frm) {
|
||||
frappe.provide("erpnext.buying");
|
||||
|
||||
frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) {
|
||||
if (frm.doc.is_subcontracted) {
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
erpnext.buying.get_default_bom(frm);
|
||||
}
|
||||
});
|
||||
});
|
@ -16,6 +16,8 @@
|
||||
"supplier_name",
|
||||
"apply_tds",
|
||||
"tax_withholding_category",
|
||||
"is_subcontracted",
|
||||
"supplier_warehouse",
|
||||
"column_break1",
|
||||
"company",
|
||||
"transaction_date",
|
||||
@ -55,10 +57,7 @@
|
||||
"price_list_currency",
|
||||
"plc_conversion_rate",
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"is_subcontracted",
|
||||
"col_break_warehouse",
|
||||
"supplier_warehouse",
|
||||
"section_break_45",
|
||||
"before_items_section",
|
||||
"scan_barcode",
|
||||
"items_col_break",
|
||||
@ -142,7 +141,8 @@
|
||||
"party_account_currency",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"inter_company_order_reference"
|
||||
"inter_company_order_reference",
|
||||
"is_old_subcontracting_flow"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -158,7 +158,8 @@
|
||||
"hidden": 1,
|
||||
"label": "Title",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
@ -443,11 +444,6 @@
|
||||
"permlevel": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sec_warehouse",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Subcontracting"
|
||||
},
|
||||
{
|
||||
"description": "Sets 'Warehouse' in each row of the Items table.",
|
||||
"fieldname": "set_warehouse",
|
||||
@ -456,15 +452,10 @@
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_warehouse",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_subcontracted",
|
||||
"fieldtype": "Check",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Is Subcontracted",
|
||||
"print_hide": 1
|
||||
},
|
||||
@ -1142,6 +1133,10 @@
|
||||
"label": "Tax Withholding Category",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_45",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
@ -1163,13 +1158,21 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_old_subcontracting_flow",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Old Subcontracting Flow",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-26 12:16:38.694276",
|
||||
"modified": "2022-06-15 15:40:58.527065",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
@ -69,8 +69,12 @@ class PurchaseOrder(BuyingController):
|
||||
self.validate_with_previous_doc()
|
||||
self.validate_for_subcontracting()
|
||||
self.validate_minimum_order_qty()
|
||||
self.validate_bom_for_subcontracting_items()
|
||||
self.create_raw_materials_supplied("supplied_items")
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
self.validate_bom_for_subcontracting_items()
|
||||
self.create_raw_materials_supplied()
|
||||
|
||||
self.validate_fg_item_for_subcontracting()
|
||||
self.set_received_qty_for_drop_ship_items()
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.supplier, self.company, self.inter_company_order_reference
|
||||
@ -194,12 +198,38 @@ class PurchaseOrder(BuyingController):
|
||||
)
|
||||
|
||||
def validate_bom_for_subcontracting_items(self):
|
||||
if self.is_subcontracted:
|
||||
for item in self.items:
|
||||
if not item.bom:
|
||||
frappe.throw(
|
||||
_("Row #{0}: BOM is not specified for subcontracting item {0}").format(
|
||||
item.idx, item.item_code
|
||||
)
|
||||
)
|
||||
|
||||
def validate_fg_item_for_subcontracting(self):
|
||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||
for item in self.items:
|
||||
if not item.bom:
|
||||
if not item.fg_item:
|
||||
frappe.throw(
|
||||
_("BOM is not specified for subcontracting item {0} at row {1}").format(
|
||||
item.item_code, item.idx
|
||||
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
|
||||
item.idx, item.item_code
|
||||
)
|
||||
)
|
||||
else:
|
||||
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
|
||||
).format(item.idx, item.fg_item, item.item_code)
|
||||
)
|
||||
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
|
||||
)
|
||||
if not item.fg_item_qty:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
|
||||
item.idx, item.item_code
|
||||
)
|
||||
)
|
||||
|
||||
@ -294,9 +324,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.set_status(update=True, status=status)
|
||||
self.update_requested_qty()
|
||||
self.update_ordered_qty()
|
||||
if self.is_subcontracted:
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
|
||||
@ -310,9 +338,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.update_requested_qty()
|
||||
self.update_ordered_qty()
|
||||
self.validate_budget()
|
||||
|
||||
if self.is_subcontracted:
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
|
||||
frappe.get_doc("Authorization Control").validate_approving_authority(
|
||||
self.doctype, self.company, self.base_grand_total
|
||||
@ -332,9 +358,7 @@ class PurchaseOrder(BuyingController):
|
||||
if self.has_drop_ship_item():
|
||||
self.update_delivered_qty_in_sales_order()
|
||||
|
||||
if self.is_subcontracted:
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
|
||||
self.update_reserved_qty_for_subcontract()
|
||||
self.check_on_hold_or_closed_status()
|
||||
|
||||
frappe.db.set(self, "status", "Cancelled")
|
||||
@ -405,10 +429,11 @@ class PurchaseOrder(BuyingController):
|
||||
item.received_qty = item.qty
|
||||
|
||||
def update_reserved_qty_for_subcontract(self):
|
||||
for d in self.supplied_items:
|
||||
if d.rm_item_code:
|
||||
stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
if self.is_old_subcontracting_flow:
|
||||
for d in self.supplied_items:
|
||||
if d.rm_item_code:
|
||||
stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order")
|
||||
|
||||
def update_receiving_percentage(self):
|
||||
total_qty, received_qty = 0.0, 0.0
|
||||
@ -587,80 +612,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(purchase_order, rm_items):
|
||||
rm_items_list = rm_items
|
||||
|
||||
if isinstance(rm_items, str):
|
||||
rm_items_list = json.loads(rm_items)
|
||||
elif not rm_items:
|
||||
frappe.throw(_("No Items available for transfer"))
|
||||
|
||||
if rm_items_list:
|
||||
fg_items = list(set(d["item_code"] for d in rm_items_list))
|
||||
else:
|
||||
frappe.throw(_("No Items selected for transfer"))
|
||||
|
||||
if purchase_order:
|
||||
purchase_order = frappe.get_doc("Purchase Order", purchase_order)
|
||||
|
||||
if fg_items:
|
||||
items = tuple(set(d["rm_item_code"] for d in rm_items_list))
|
||||
item_wh = get_item_details(items)
|
||||
|
||||
stock_entry = frappe.new_doc("Stock Entry")
|
||||
stock_entry.purpose = "Send to Subcontractor"
|
||||
stock_entry.purchase_order = purchase_order.name
|
||||
stock_entry.supplier = purchase_order.supplier
|
||||
stock_entry.supplier_name = purchase_order.supplier_name
|
||||
stock_entry.supplier_address = purchase_order.supplier_address
|
||||
stock_entry.address_display = purchase_order.address_display
|
||||
stock_entry.company = purchase_order.company
|
||||
stock_entry.to_warehouse = purchase_order.supplier_warehouse
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
for item_code in fg_items:
|
||||
for rm_item_data in rm_items_list:
|
||||
if rm_item_data["item_code"] == item_code:
|
||||
rm_item_code = rm_item_data["rm_item_code"]
|
||||
items_dict = {
|
||||
rm_item_code: {
|
||||
"po_detail": rm_item_data.get("name"),
|
||||
"item_name": rm_item_data["item_name"],
|
||||
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
||||
"qty": rm_item_data["qty"],
|
||||
"from_warehouse": rm_item_data["warehouse"],
|
||||
"stock_uom": rm_item_data["stock_uom"],
|
||||
"serial_no": rm_item_data.get("serial_no"),
|
||||
"batch_no": rm_item_data.get("batch_no"),
|
||||
"main_item_code": rm_item_data["item_code"],
|
||||
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
|
||||
}
|
||||
}
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
stock_entry.set_missing_values()
|
||||
return stock_entry.as_dict()
|
||||
else:
|
||||
frappe.throw(_("No Items selected for transfer"))
|
||||
return purchase_order.name
|
||||
|
||||
|
||||
def get_item_details(items):
|
||||
item_details = {}
|
||||
for d in frappe.db.sql(
|
||||
"""select item_code, description, allow_alternative_item from `tabItem`
|
||||
where name in ({0})""".format(
|
||||
", ".join(["%s"] * len(items))
|
||||
),
|
||||
items,
|
||||
as_dict=1,
|
||||
):
|
||||
item_details[d.item_code] = d
|
||||
|
||||
return item_details
|
||||
|
||||
|
||||
def get_list_context(context=None):
|
||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||
|
||||
@ -691,61 +642,61 @@ def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_materials_from_supplier(purchase_order, po_details):
|
||||
if isinstance(po_details, str):
|
||||
po_details = json.loads(po_details)
|
||||
|
||||
doc = frappe.get_cached_doc("Purchase Order", purchase_order)
|
||||
doc.initialized_fields()
|
||||
doc.purchase_orders = [doc.name]
|
||||
doc.get_available_materials()
|
||||
|
||||
if not doc.available_materials:
|
||||
frappe.throw(
|
||||
_("Materials are already received against the purchase order {0}").format(purchase_order)
|
||||
)
|
||||
|
||||
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
|
||||
def make_subcontracting_order(source_name, target_doc=None):
|
||||
return get_mapped_subcontracting_order(source_name, target_doc)
|
||||
|
||||
|
||||
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
|
||||
ste_doc = frappe.new_doc("Stock Entry")
|
||||
ste_doc.purpose = "Material Transfer"
|
||||
ste_doc.purchase_order = po_doc.name
|
||||
ste_doc.company = po_doc.company
|
||||
ste_doc.is_return = 1
|
||||
def get_mapped_subcontracting_order(source_name, target_doc=None):
|
||||
|
||||
for key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
if target_doc and isinstance(target_doc, str):
|
||||
target_doc = json.loads(target_doc)
|
||||
for key in ["service_items", "items", "supplied_items"]:
|
||||
if key in target_doc:
|
||||
del target_doc[key]
|
||||
target_doc = json.dumps(target_doc)
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
ste_doc.set_missing_values()
|
||||
|
||||
return ste_doc
|
||||
|
||||
|
||||
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
|
||||
item = ste_doc.append("items", row.item_details)
|
||||
|
||||
po_detail = list(set(row.po_details).intersection(po_details))
|
||||
item.update(
|
||||
target_doc = get_mapped_doc(
|
||||
"Purchase Order",
|
||||
source_name,
|
||||
{
|
||||
"qty": qty,
|
||||
"batch_no": batch_no,
|
||||
"basic_rate": row.item_details["rate"],
|
||||
"po_detail": po_detail[0] if po_detail else "",
|
||||
"s_warehouse": row.item_details["t_warehouse"],
|
||||
"t_warehouse": row.item_details["s_warehouse"],
|
||||
"item_code": row.item_details["rm_item_code"],
|
||||
"subcontracted_item": row.item_details["main_item_code"],
|
||||
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
|
||||
}
|
||||
"Purchase Order": {
|
||||
"doctype": "Subcontracting Order",
|
||||
"field_map": {},
|
||||
"field_no_map": ["total_qty", "total", "net_total"],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1],
|
||||
},
|
||||
},
|
||||
"Purchase Order Item": {
|
||||
"doctype": "Subcontracting Order Service Item",
|
||||
"field_map": {},
|
||||
"field_no_map": [],
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
)
|
||||
|
||||
target_doc.populate_items_table()
|
||||
|
||||
if target_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = target_doc.set_warehouse
|
||||
else:
|
||||
source_doc = frappe.get_doc("Purchase Order", source_name)
|
||||
if source_doc.set_warehouse:
|
||||
for item in target_doc.items:
|
||||
item.warehouse = source_doc.set_warehouse
|
||||
else:
|
||||
for idx, item in enumerate(target_doc.items):
|
||||
item.warehouse = source_doc.items[idx].warehouse
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_subcontracting_order_created(po_name) -> bool:
|
||||
count = frappe.db.count(
|
||||
"Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]}
|
||||
)
|
||||
|
||||
return True if count else False
|
||||
|
@ -22,6 +22,6 @@ def get_data():
|
||||
"label": _("Reference"),
|
||||
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
|
||||
},
|
||||
{"label": _("Sub-contracting"), "items": ["Stock Entry"]},
|
||||
{"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
|
||||
],
|
||||
}
|
||||
|
@ -13,9 +13,6 @@ from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as make_pi_from_po,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@ -24,7 +21,6 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
@ -140,43 +136,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# ordered qty decreases as ordered qty is 0 (deleted row)
|
||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
|
||||
|
||||
def test_supplied_items_validations_on_po_update_after_submit(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100)
|
||||
item = po.items[0]
|
||||
|
||||
original_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
|
||||
# Just update rate
|
||||
trans_item = [
|
||||
{
|
||||
"item_code": "_Test FG Item",
|
||||
"rate": 20,
|
||||
"qty": 5,
|
||||
"conversion_factor": 1.0,
|
||||
"docname": item.name,
|
||||
}
|
||||
]
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys()))
|
||||
|
||||
# Update qty to 2x
|
||||
trans_item[0]["qty"] *= 2
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values()))
|
||||
|
||||
# Set transfer qty and attempt to update qty, shouldn't be allowed
|
||||
po.supplied_items[0].supplied_qty = 2
|
||||
po.supplied_items[0].db_update()
|
||||
trans_item[0]["qty"] *= 2
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
|
||||
def test_update_child(self):
|
||||
mr = make_material_request(qty=10)
|
||||
po = make_purchase_order(mr.name)
|
||||
@ -426,31 +385,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
new_item_with_tax.delete()
|
||||
frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
|
||||
|
||||
def test_update_child_uom_conv_factor_change(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
|
||||
total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": po.get("items")[0].item_code,
|
||||
"rate": po.get("items")[0].rate,
|
||||
"qty": po.get("items")[0].qty,
|
||||
"uom": "_Test UOM 1",
|
||||
"conversion_factor": 2,
|
||||
"docname": po.get("items")[0].name,
|
||||
}
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Purchase Order", trans_item, po.name)
|
||||
po.reload()
|
||||
|
||||
total_reqd_qty_after_change = sum(
|
||||
d.get("required_qty") for d in po.as_dict().get("supplied_items")
|
||||
)
|
||||
|
||||
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
|
||||
|
||||
def test_update_qty(self):
|
||||
po = create_purchase_order()
|
||||
|
||||
@ -609,10 +543,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_subcontracting(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
|
||||
self.assertEqual(len(po.get("supplied_items")), 2)
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
from erpnext.stock.utils import InvalidWarehouseCompany
|
||||
|
||||
@ -777,379 +707,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
pi.insert()
|
||||
self.assertTrue(pi.get("payment_schedule"))
|
||||
|
||||
def test_reserved_qty_subcontract_po(self):
|
||||
# Make stock available for raw materials
|
||||
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
qty=30,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
bin1 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
# Submit PO
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
|
||||
|
||||
bin2 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
||||
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
|
||||
self.assertNotEqual(bin1.modified, bin2.modified)
|
||||
|
||||
# Create stock transfer
|
||||
rm_item = [
|
||||
{
|
||||
"item_code": "_Test FG Item",
|
||||
"rm_item_code": "_Test Item",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 6,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"rate": 100,
|
||||
"amount": 600,
|
||||
"stock_uom": "Nos",
|
||||
}
|
||||
]
|
||||
rm_item_string = json.dumps(rm_item)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
bin3 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||
|
||||
# close PO
|
||||
po.update_status("Closed")
|
||||
bin4 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
# Re-open PO
|
||||
po.update_status("Submitted")
|
||||
bin5 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
qty=40,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
# make Purchase Receipt against PO
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
bin6 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
# Cancel PR
|
||||
pr.cancel()
|
||||
bin7 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||
|
||||
# Make Purchase Invoice
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.update_stock = 1
|
||||
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
|
||||
pi.insert()
|
||||
pi.submit()
|
||||
bin8 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
# Cancel PR
|
||||
pi.cancel()
|
||||
bin9 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
|
||||
|
||||
# Cancel Stock Entry
|
||||
se.cancel()
|
||||
bin10 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
|
||||
|
||||
# Cancel PO
|
||||
po.reload()
|
||||
po.cancel()
|
||||
bin11 = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
|
||||
fieldname="reserved_qty_for_sub_contract",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 11"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
include_exploded_items=1,
|
||||
)
|
||||
|
||||
name = frappe.db.get_value("BOM", {"item": item_code}, "name")
|
||||
bom = frappe.get_doc("BOM", name)
|
||||
|
||||
exploded_items = sorted(
|
||||
[d.item_code for d in bom.exploded_items if not d.get("sourced_by_supplier")]
|
||||
)
|
||||
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
||||
self.assertEqual(exploded_items, supplied_items)
|
||||
|
||||
po1 = create_purchase_order(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
include_exploded_items=0,
|
||||
)
|
||||
|
||||
supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items])
|
||||
bom_items = sorted([d.item_code for d in bom.items if not d.get("sourced_by_supplier")])
|
||||
|
||||
self.assertEqual(supplied_items1, bom_items)
|
||||
|
||||
def test_backflush_based_on_stock_entry(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1})
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 5
|
||||
po = create_purchase_order(
|
||||
item_code=item_code,
|
||||
qty=order_qty,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="Sub Contracted Raw Material 1",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 1",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 10,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "_Test Item Home Desktop 100",
|
||||
"item_name": "_Test Item Home Desktop 100",
|
||||
"qty": 20,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Test Extra Item 1",
|
||||
"item_name": "Test Extra Item 1",
|
||||
"qty": 10,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Test Extra Item 2",
|
||||
"stock_uom": "Nos",
|
||||
"qty": 10,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_name": "Test Extra Item 2",
|
||||
},
|
||||
]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
received_qty = 2
|
||||
# partial receipt
|
||||
pr.get("items")[0].qty = received_qty
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
transferred_items = sorted(
|
||||
[d.item_code for d in se.get("items") if se.purchase_order == po.name]
|
||||
)
|
||||
issued_items = sorted([d.rm_item_code for d in pr.get("supplied_items")])
|
||||
|
||||
self.assertEqual(transferred_items, issued_items)
|
||||
self.assertEqual(pr.get("items")[0].rm_supp_cost, 2000)
|
||||
|
||||
transferred_rm_map = frappe._dict()
|
||||
for item in rm_items:
|
||||
transferred_rm_map[item.get("rm_item_code")] = item
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_supplied_qty_against_subcontracted_po(self):
|
||||
item_code = "_Test Subcontracted FG Item 5"
|
||||
make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 250
|
||||
po = create_purchase_order(
|
||||
item_code=item_code,
|
||||
qty=order_qty,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
# Add same subcontracted items multiple times
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"qty": order_qty,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
po.set_missing_values()
|
||||
po.submit()
|
||||
|
||||
# Material receipt entry for the raw materials which will be send to supplier
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="Sub Contracted Raw Material 4",
|
||||
qty=500,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 4",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 250,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
"name": po.supplied_items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 4",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 250,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
]
|
||||
|
||||
# Raw Materials transfer entry from stores to supplier's warehouse
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
# Test po_detail field has value or not
|
||||
for item_row in se.items:
|
||||
self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name)
|
||||
|
||||
po_doc = frappe.get_doc("Purchase Order", po.name)
|
||||
for row in po_doc.supplied_items:
|
||||
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
|
||||
self.assertEqual(row.supplied_qty, 250.0)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_advance_payment_entry_unlink_against_purchase_order(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@ -1248,50 +805,6 @@ def make_pr_against_po(po, received_qty=0):
|
||||
return pr
|
||||
|
||||
|
||||
def make_subcontracted_item(**args):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not frappe.db.exists("Item", args.item_code):
|
||||
make_item(
|
||||
args.item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"is_sub_contracted_item": 1,
|
||||
"has_batch_no": args.get("has_batch_no") or 0,
|
||||
},
|
||||
)
|
||||
|
||||
if not args.raw_materials:
|
||||
if not frappe.db.exists("Item", "Test Extra Item 1"):
|
||||
make_item(
|
||||
"Test Extra Item 1",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Item", "Test Extra Item 2"):
|
||||
make_item(
|
||||
"Test Extra Item 2",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
|
||||
|
||||
if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
|
||||
make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
|
||||
|
||||
|
||||
def update_backflush_based_on(based_on):
|
||||
doc = frappe.get_doc("Buying Settings")
|
||||
doc.backflush_raw_materials_of_subcontract_based_on = based_on
|
||||
doc.save()
|
||||
|
||||
|
||||
def get_same_items():
|
||||
return [
|
||||
{
|
||||
|
@ -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,
|
||||
"buying_price_list": "_Test Price List",
|
||||
|
@ -11,6 +11,8 @@
|
||||
"supplier_part_no",
|
||||
"item_name",
|
||||
"product_bundle",
|
||||
"fg_item",
|
||||
"fg_item_qty",
|
||||
"column_break_4",
|
||||
"schedule_date",
|
||||
"expected_delivery_date",
|
||||
@ -574,16 +576,18 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_subcontracted",
|
||||
"depends_on": "eval:parent.is_old_subcontracting_flow",
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"label": "BOM",
|
||||
"options": "BOM",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.is_subcontracted",
|
||||
"depends_on": "eval:parent.is_old_subcontracting_flow",
|
||||
"fieldname": "include_exploded_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include Exploded Items",
|
||||
@ -848,6 +852,22 @@
|
||||
"label": "Sales Order Packed Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
|
||||
"fieldname": "fg_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finished Good Item",
|
||||
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
|
||||
"fieldname": "fg_item_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Finished Good Item Qty",
|
||||
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
|
@ -14,32 +14,29 @@ frappe.query_reports["Subcontract Order Summary"] = {
|
||||
},
|
||||
{
|
||||
label: __("From Date"),
|
||||
fieldname:"from_date",
|
||||
fieldname: "from_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Date"),
|
||||
fieldname:"to_date",
|
||||
fieldname: "to_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Purchase Order"),
|
||||
label: __("Order Type"),
|
||||
fieldname: "order_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Purchase Order", "Subcontracting Order"],
|
||||
default: "Subcontracting Order"
|
||||
},
|
||||
{
|
||||
label: __("Subcontract Order"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Order",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
is_subcontracted: 1,
|
||||
company: frappe.query_report.get_filter_value('company')
|
||||
}
|
||||
}
|
||||
}
|
||||
fieldtype: "Data"
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -15,7 +15,7 @@
|
||||
"name": "Subcontract Order Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"ref_doctype": "Subcontracting Order",
|
||||
"report_name": "Subcontract Order Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
|
@ -8,7 +8,7 @@ from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
columns = get_columns()
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
@ -20,43 +20,45 @@ def get_data(report_filters):
|
||||
|
||||
if orders:
|
||||
supplied_items = get_supplied_items(orders, report_filters)
|
||||
po_details = prepare_subcontracted_data(orders, supplied_items)
|
||||
get_subcontracted_data(po_details, data)
|
||||
order_details = prepare_subcontracted_data(orders, supplied_items)
|
||||
get_subcontracted_data(order_details, data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def get_subcontracted_orders(report_filters):
|
||||
fields = [
|
||||
"`tabPurchase Order Item`.`parent` as po_id",
|
||||
"`tabPurchase Order Item`.`item_code`",
|
||||
"`tabPurchase Order Item`.`item_name`",
|
||||
"`tabPurchase Order Item`.`qty`",
|
||||
"`tabPurchase Order Item`.`name`",
|
||||
"`tabPurchase Order Item`.`received_qty`",
|
||||
"`tabPurchase Order`.`status`",
|
||||
f"`tab{report_filters.order_type} Item`.`parent` as order_id",
|
||||
f"`tab{report_filters.order_type} Item`.`item_code`",
|
||||
f"`tab{report_filters.order_type} Item`.`item_name`",
|
||||
f"`tab{report_filters.order_type} Item`.`qty`",
|
||||
f"`tab{report_filters.order_type} Item`.`name`",
|
||||
f"`tab{report_filters.order_type} Item`.`received_qty`",
|
||||
f"`tab{report_filters.order_type}`.`status`",
|
||||
]
|
||||
|
||||
filters = get_filters(report_filters)
|
||||
|
||||
return frappe.get_all("Purchase Order", fields=fields, filters=filters) or []
|
||||
return frappe.get_all(report_filters.order_type, fields=fields, filters=filters) or []
|
||||
|
||||
|
||||
def get_filters(report_filters):
|
||||
filters = [
|
||||
["Purchase Order", "docstatus", "=", 1],
|
||||
["Purchase Order", "is_subcontracted", "=", 1],
|
||||
[report_filters.order_type, "docstatus", "=", 1],
|
||||
[
|
||||
"Purchase Order",
|
||||
report_filters.order_type,
|
||||
"transaction_date",
|
||||
"between",
|
||||
(report_filters.from_date, report_filters.to_date),
|
||||
],
|
||||
]
|
||||
|
||||
if report_filters.order_type == "Purchase Order":
|
||||
filters.append(["Purchase Order", "is_old_subcontracting_flow", "=", 1])
|
||||
|
||||
for field in ["name", "company"]:
|
||||
if report_filters.get(field):
|
||||
filters.append(["Purchase Order", field, "=", report_filters.get(field)])
|
||||
filters.append([report_filters.order_type, field, "=", report_filters.get(field)])
|
||||
|
||||
return filters
|
||||
|
||||
@ -77,10 +79,15 @@ def get_supplied_items(orders, report_filters):
|
||||
"reference_name",
|
||||
]
|
||||
|
||||
filters = {"parent": ("in", [d.po_id for d in orders]), "docstatus": 1}
|
||||
filters = {"parent": ("in", [d.order_id for d in orders]), "docstatus": 1}
|
||||
|
||||
supplied_items = {}
|
||||
for row in frappe.get_all("Purchase Order Item Supplied", fields=fields, filters=filters):
|
||||
supplied_items_table = (
|
||||
"Purchase Order Item Supplied"
|
||||
if report_filters.order_type == "Purchase Order"
|
||||
else "Subcontracting Order Supplied Item"
|
||||
)
|
||||
for row in frappe.get_all(supplied_items_table, fields=fields, filters=filters):
|
||||
new_key = (row.parent, row.reference_name, row.main_item_code)
|
||||
|
||||
supplied_items.setdefault(new_key, []).append(row)
|
||||
@ -89,24 +96,24 @@ def get_supplied_items(orders, report_filters):
|
||||
|
||||
|
||||
def prepare_subcontracted_data(orders, supplied_items):
|
||||
po_details = {}
|
||||
order_details = {}
|
||||
for row in orders:
|
||||
key = (row.po_id, row.name, row.item_code)
|
||||
if key not in po_details:
|
||||
po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []}))
|
||||
key = (row.order_id, row.name, row.item_code)
|
||||
if key not in order_details:
|
||||
order_details.setdefault(key, frappe._dict({"order_item": row, "supplied_items": []}))
|
||||
|
||||
details = po_details[key]
|
||||
details = order_details[key]
|
||||
|
||||
if supplied_items.get(key):
|
||||
for supplied_item in supplied_items[key]:
|
||||
details["supplied_items"].append(supplied_item)
|
||||
|
||||
return po_details
|
||||
return order_details
|
||||
|
||||
|
||||
def get_subcontracted_data(po_details, data):
|
||||
for key, details in po_details.items():
|
||||
res = details.po_item
|
||||
def get_subcontracted_data(order_details, data):
|
||||
for key, details in order_details.items():
|
||||
res = details.order_item
|
||||
for index, row in enumerate(details.supplied_items):
|
||||
if index != 0:
|
||||
res = {}
|
||||
@ -115,13 +122,13 @@ def get_subcontracted_data(po_details, data):
|
||||
data.append(res)
|
||||
|
||||
|
||||
def get_columns():
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"fieldname": "po_id",
|
||||
"label": _("Subcontract Order"),
|
||||
"fieldname": "order_id",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"options": filters.order_type,
|
||||
"width": 100,
|
||||
},
|
||||
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80},
|
||||
|
@ -4,6 +4,13 @@
|
||||
|
||||
frappe.query_reports["Subcontracted Item To Be Received"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Order Type"),
|
||||
fieldname: "order_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Purchase Order", "Subcontracting Order"],
|
||||
default: "Subcontracting Order"
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
|
@ -13,7 +13,7 @@
|
||||
"name": "Subcontracted Item To Be Received",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"ref_doctype": "Subcontracting Order",
|
||||
"report_name": "Subcontracted Item To Be Received",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
|
@ -11,18 +11,18 @@ def execute(filters=None):
|
||||
frappe.msgprint(_("To Date must be greater than From Date"))
|
||||
|
||||
data = []
|
||||
columns = get_columns()
|
||||
columns = get_columns(filters)
|
||||
get_data(data, filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns():
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"label": _("Subcontract Order"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"fieldname": "subcontract_order",
|
||||
"options": filters.order_type,
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150},
|
||||
@ -57,14 +57,14 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(data, filters):
|
||||
po = get_po(filters)
|
||||
po_name = [v.name for v in po]
|
||||
sub_items = get_purchase_order_item_supplied(po_name)
|
||||
for item in sub_items:
|
||||
for order in po:
|
||||
orders = get_subcontract_orders(filters)
|
||||
orders_name = [order.name for order in orders]
|
||||
subcontracted_items = get_subcontract_order_supplied_item(filters.order_type, orders_name)
|
||||
for item in subcontracted_items:
|
||||
for order in orders:
|
||||
if order.name == item.parent and item.received_qty < item.qty:
|
||||
row = {
|
||||
"purchase_order": item.parent,
|
||||
"subcontract_order": item.parent,
|
||||
"date": order.transaction_date,
|
||||
"supplier": order.supplier,
|
||||
"fg_item_code": item.item_code,
|
||||
@ -76,22 +76,25 @@ def get_data(data, filters):
|
||||
data.append(row)
|
||||
|
||||
|
||||
def get_po(filters):
|
||||
def get_subcontract_orders(filters):
|
||||
record_filters = [
|
||||
["is_subcontracted", "=", 1],
|
||||
["supplier", "=", filters.supplier],
|
||||
["transaction_date", "<=", filters.to_date],
|
||||
["transaction_date", ">=", filters.from_date],
|
||||
["docstatus", "=", 1],
|
||||
]
|
||||
|
||||
if filters.order_type == "Purchase Order":
|
||||
record_filters.append(["is_old_subcontracting_flow", "=", 1])
|
||||
|
||||
return frappe.get_all(
|
||||
"Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]
|
||||
filters.order_type, filters=record_filters, fields=["name", "transaction_date", "supplier"]
|
||||
)
|
||||
|
||||
|
||||
def get_purchase_order_item_supplied(po):
|
||||
def get_subcontract_order_supplied_item(order_type, orders):
|
||||
return frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters=[("parent", "IN", po)],
|
||||
f"{order_type} Item",
|
||||
filters=[("parent", "IN", orders)],
|
||||
fields=["parent", "item_code", "item_name", "qty", "received_qty"],
|
||||
)
|
||||
|
@ -7,18 +7,35 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_to_be_received import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_service_item,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
|
||||
|
||||
class TestSubcontractedItemToBeReceived(FrappeTestCase):
|
||||
def test_pending_and_received_qty(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
|
||||
transfer_param = []
|
||||
make_service_item("Subcontracted Service Item 1")
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 500,
|
||||
"fg_item": "_Test FG Item",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC"
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
|
||||
)
|
||||
@ -28,28 +45,28 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase):
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
)
|
||||
make_purchase_receipt_against_po(po.name)
|
||||
po.reload()
|
||||
make_subcontracting_receipt_against_sco(sco.name)
|
||||
sco.reload()
|
||||
col, data = execute(
|
||||
filters=frappe._dict(
|
||||
{
|
||||
"supplier": po.supplier,
|
||||
"order_type": "Subcontracting Order",
|
||||
"supplier": sco.supplier,
|
||||
"from_date": frappe.utils.get_datetime(
|
||||
frappe.utils.add_to_date(po.transaction_date, days=-10)
|
||||
frappe.utils.add_to_date(sco.transaction_date, days=-10)
|
||||
),
|
||||
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)),
|
||||
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)),
|
||||
}
|
||||
)
|
||||
)
|
||||
self.assertEqual(data[0]["pending_qty"], 5)
|
||||
self.assertEqual(data[0]["received_qty"], 5)
|
||||
self.assertEqual(data[0]["purchase_order"], po.name)
|
||||
self.assertEqual(data[0]["supplier"], po.supplier)
|
||||
self.assertEqual(data[0]["subcontract_order"], sco.name)
|
||||
self.assertEqual(data[0]["supplier"], sco.supplier)
|
||||
|
||||
|
||||
def make_purchase_receipt_against_po(po, quantity=5):
|
||||
pr = make_purchase_receipt(po)
|
||||
pr.items[0].qty = quantity
|
||||
pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
|
||||
pr.insert()
|
||||
pr.submit()
|
||||
def make_subcontracting_receipt_against_sco(sco, quantity=5):
|
||||
scr = make_subcontracting_receipt(sco)
|
||||
scr.items[0].qty = quantity
|
||||
scr.insert()
|
||||
scr.submit()
|
||||
|
@ -4,6 +4,13 @@
|
||||
|
||||
frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Order Type"),
|
||||
fieldname: "order_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Purchase Order", "Subcontracting Order"],
|
||||
default: "Subcontracting Order"
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
|
@ -13,7 +13,7 @@
|
||||
"name": "Subcontracted Raw Materials To Be Transferred",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"ref_doctype": "Subcontracting Order",
|
||||
"report_name": "Subcontracted Raw Materials To Be Transferred",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
|
@ -10,19 +10,19 @@ def execute(filters=None):
|
||||
if filters.from_date >= filters.to_date:
|
||||
frappe.msgprint(_("To Date must be greater than From Date"))
|
||||
|
||||
columns = get_columns()
|
||||
columns = get_columns(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data or []
|
||||
|
||||
|
||||
def get_columns():
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"label": _("Subcontract Order"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"fieldname": "subcontract_order",
|
||||
"options": filters.order_type,
|
||||
"width": 200,
|
||||
},
|
||||
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150},
|
||||
@ -46,10 +46,10 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
po_rm_item_details = get_po_items_to_supply(filters)
|
||||
order_rm_item_details = get_order_items_to_supply(filters)
|
||||
|
||||
data = []
|
||||
for row in po_rm_item_details:
|
||||
for row in order_rm_item_details:
|
||||
transferred_qty = row.get("transferred_qty") or 0
|
||||
if transferred_qty < row.get("reqd_qty", 0):
|
||||
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
||||
@ -59,23 +59,33 @@ def get_data(filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_po_items_to_supply(filters):
|
||||
def get_order_items_to_supply(filters):
|
||||
supplied_items_table = (
|
||||
"Purchase Order Item Supplied"
|
||||
if filters.order_type == "Purchase Order"
|
||||
else "Subcontracting Order Supplied Item"
|
||||
)
|
||||
|
||||
record_filters = [
|
||||
[filters.order_type, "per_received", "<", "100"],
|
||||
[filters.order_type, "supplier", "=", filters.supplier],
|
||||
[filters.order_type, "transaction_date", "<=", filters.to_date],
|
||||
[filters.order_type, "transaction_date", ">=", filters.from_date],
|
||||
[filters.order_type, "docstatus", "=", 1],
|
||||
]
|
||||
|
||||
if filters.order_type == "Purchase Order":
|
||||
record_filters.append([filters.order_type, "is_old_subcontracting_flow", "=", 1])
|
||||
|
||||
return frappe.db.get_all(
|
||||
"Purchase Order",
|
||||
filters.order_type,
|
||||
fields=[
|
||||
"name as purchase_order",
|
||||
"name as subcontract_order",
|
||||
"transaction_date as date",
|
||||
"supplier as supplier",
|
||||
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
|
||||
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
|
||||
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty",
|
||||
],
|
||||
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],
|
||||
f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
|
||||
f"`tab{supplied_items_table}`.required_qty as reqd_qty",
|
||||
f"`tab{supplied_items_table}`.supplied_qty as transferred_qty",
|
||||
],
|
||||
filters=record_filters,
|
||||
)
|
||||
|
@ -3,24 +3,34 @@
|
||||
# Compiled at: 2019-05-06 10:24:35
|
||||
# Decompiled by https://python-decompiler.com
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_service_item,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
|
||||
def test_pending_and_transferred_qty(self):
|
||||
po = create_purchase_order(
|
||||
item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
|
||||
)
|
||||
make_service_item("Subcontracted Service Item 1")
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 500,
|
||||
"fg_item": "_Test FG Item",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
|
||||
# Material Receipt of RMs
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
|
||||
@ -28,50 +38,48 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase):
|
||||
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100
|
||||
)
|
||||
|
||||
se = transfer_subcontracted_raw_materials(po)
|
||||
transfer_subcontracted_raw_materials(sco)
|
||||
|
||||
col, data = execute(
|
||||
filters=frappe._dict(
|
||||
{
|
||||
"supplier": po.supplier,
|
||||
"order_type": "Subcontracting Order",
|
||||
"supplier": sco.supplier,
|
||||
"from_date": frappe.utils.get_datetime(
|
||||
frappe.utils.add_to_date(po.transaction_date, days=-10)
|
||||
frappe.utils.add_to_date(sco.transaction_date, days=-10)
|
||||
),
|
||||
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)),
|
||||
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)),
|
||||
}
|
||||
)
|
||||
)
|
||||
po.reload()
|
||||
sco.reload()
|
||||
|
||||
po_data = [row for row in data if row.get("purchase_order") == po.name]
|
||||
sco_data = [row for row in data if row.get("subcontract_order") == sco.name]
|
||||
# Alphabetically sort to be certain of order
|
||||
po_data = sorted(po_data, key=lambda i: i["rm_item_code"])
|
||||
sco_data = sorted(sco_data, key=lambda i: i["rm_item_code"])
|
||||
|
||||
self.assertEqual(len(po_data), 2)
|
||||
self.assertEqual(po_data[0]["purchase_order"], po.name)
|
||||
self.assertEqual(len(sco_data), 2)
|
||||
self.assertEqual(sco_data[0]["subcontract_order"], sco.name)
|
||||
|
||||
self.assertEqual(po_data[0]["rm_item_code"], "_Test Item")
|
||||
self.assertEqual(po_data[0]["p_qty"], 8)
|
||||
self.assertEqual(po_data[0]["transferred_qty"], 2)
|
||||
self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item")
|
||||
self.assertEqual(sco_data[0]["p_qty"], 8)
|
||||
self.assertEqual(sco_data[0]["transferred_qty"], 2)
|
||||
|
||||
self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
|
||||
self.assertEqual(po_data[1]["p_qty"], 19)
|
||||
self.assertEqual(po_data[1]["transferred_qty"], 1)
|
||||
|
||||
se.cancel()
|
||||
po.cancel()
|
||||
self.assertEqual(sco_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
|
||||
self.assertEqual(sco_data[1]["p_qty"], 19)
|
||||
self.assertEqual(sco_data[1]["transferred_qty"], 1)
|
||||
|
||||
|
||||
def transfer_subcontracted_raw_materials(po):
|
||||
# Order of supplied items fetched in PO is flaky
|
||||
def transfer_subcontracted_raw_materials(sco):
|
||||
# Order of supplied items fetched in SCO is flaky
|
||||
transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1}
|
||||
|
||||
item_1 = po.supplied_items[0].rm_item_code
|
||||
item_2 = po.supplied_items[1].rm_item_code
|
||||
item_1 = sco.supplied_items[0].rm_item_code
|
||||
item_2 = sco.supplied_items[1].rm_item_code
|
||||
|
||||
rm_item = [
|
||||
rm_items = [
|
||||
{
|
||||
"name": po.supplied_items[0].name,
|
||||
"name": sco.supplied_items[0].name,
|
||||
"item_code": item_1,
|
||||
"rm_item_code": item_1,
|
||||
"item_name": item_1,
|
||||
@ -82,7 +90,7 @@ def transfer_subcontracted_raw_materials(po):
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
{
|
||||
"name": po.supplied_items[1].name,
|
||||
"name": sco.supplied_items[1].name,
|
||||
"item_code": item_2,
|
||||
"rm_item_code": item_2,
|
||||
"item_name": item_2,
|
||||
@ -93,8 +101,7 @@ def transfer_subcontracted_raw_materials(po):
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
]
|
||||
rm_item_string = json.dumps(rm_item)
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||
se.from_warehouse = "_Test Warehouse - _TC"
|
||||
se.to_warehouse = "_Test Warehouse - _TC"
|
||||
se.stock_entry_type = "Send to Subcontractor"
|
||||
|
@ -2709,10 +2709,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
if parent.is_subcontracted:
|
||||
if parent.is_old_subcontracting_flow:
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
parent.create_raw_materials_supplied()
|
||||
parent.save()
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
|
@ -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.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.subcontracting import Subcontracting
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
@ -21,7 +20,7 @@ class QtyMismatchError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BuyingController(StockController, Subcontracting):
|
||||
class BuyingController(SubcontractingController):
|
||||
def __setup__(self):
|
||||
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
|
||||
|
||||
@ -55,7 +54,8 @@ class BuyingController(StockController, Subcontracting):
|
||||
|
||||
# sub-contracting
|
||||
self.validate_for_subcontracting()
|
||||
self.create_raw_materials_supplied("supplied_items")
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
self.create_raw_materials_supplied()
|
||||
self.set_landed_cost_voucher_amount()
|
||||
|
||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
@ -256,13 +256,18 @@ class BuyingController(StockController, Subcontracting):
|
||||
)
|
||||
|
||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount
|
||||
+ item.item_tax_amount
|
||||
+ item.rm_supp_cost
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
) / qty_in_stock_uom
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount
|
||||
+ item.item_tax_amount
|
||||
+ item.rm_supp_cost
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = (
|
||||
item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
) / qty_in_stock_uom
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
||||
@ -317,76 +322,25 @@ class BuyingController(StockController, Subcontracting):
|
||||
d.discount_amount = 0.0
|
||||
d.margin_rate_or_amount = 0.0
|
||||
|
||||
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
|
||||
supplied_items_cost = 0.0
|
||||
for d in self.get("supplied_items"):
|
||||
if d.reference_name == item_row_id:
|
||||
if reset_outgoing_rate and frappe.get_cached_value("Item", d.rm_item_code, "is_stock_item"):
|
||||
rate = get_incoming_rate(
|
||||
{
|
||||
"item_code": d.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": -1 * d.consumed_qty,
|
||||
"serial_no": d.serial_no,
|
||||
"batch_no": d.batch_no,
|
||||
}
|
||||
)
|
||||
|
||||
if rate > 0:
|
||||
d.rate = rate
|
||||
|
||||
d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
|
||||
supplied_items_cost += flt(d.amount)
|
||||
|
||||
return supplied_items_cost
|
||||
|
||||
def validate_for_subcontracting(self):
|
||||
if self.is_subcontracted:
|
||||
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
|
||||
frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
|
||||
|
||||
for item in self.get("items"):
|
||||
if item in self.sub_contracted_items and not item.bom:
|
||||
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
|
||||
|
||||
if self.doctype != "Purchase Order":
|
||||
return
|
||||
|
||||
for row in self.get("supplied_items"):
|
||||
if not row.reserve_warehouse:
|
||||
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
|
||||
frappe.throw(_(msg))
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if item.bom:
|
||||
if item.get("bom"):
|
||||
item.bom = None
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table):
|
||||
if self.is_subcontracted:
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
for item in self.get("items"):
|
||||
item.rm_supp_cost = 0.0
|
||||
|
||||
if not self.is_subcontracted and self.get("supplied_items"):
|
||||
self.set("supplied_items", [])
|
||||
|
||||
@property
|
||||
def sub_contracted_items(self):
|
||||
if not hasattr(self, "_sub_contracted_items"):
|
||||
self._sub_contracted_items = []
|
||||
item_codes = list(set(item.item_code for item in self.get("items")))
|
||||
if item_codes:
|
||||
items = frappe.get_all(
|
||||
"Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1}
|
||||
)
|
||||
self._sub_contracted_items = [item.name for item in items]
|
||||
|
||||
return self._sub_contracted_items
|
||||
|
||||
def set_qty_as_per_stock_uom(self):
|
||||
for d in self.get("items"):
|
||||
if d.meta.get_field("stock_qty"):
|
||||
@ -510,7 +464,9 @@ class BuyingController(StockController, Subcontracting):
|
||||
sle.update(
|
||||
{
|
||||
"incoming_rate": incoming_rate,
|
||||
"recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0,
|
||||
"recalculate_rate": 1
|
||||
if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
@ -538,7 +494,8 @@ 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(
|
||||
sl_entries,
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
@ -565,26 +522,9 @@ class BuyingController(StockController, Subcontracting):
|
||||
)
|
||||
|
||||
po_obj.update_ordered_qty(po_item_rows)
|
||||
if self.is_subcontracted:
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
po_obj.update_reserved_qty_for_subcontract()
|
||||
|
||||
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
|
||||
if hasattr(self, "supplied_items"):
|
||||
for d in self.get("supplied_items"):
|
||||
# negative quantity is passed, as raw material qty has to be decreased
|
||||
# when PR is submitted and it has to be increased when PR is cancelled
|
||||
sl_entries.append(
|
||||
self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"item_code": d.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"actual_qty": -1 * flt(d.consumed_qty),
|
||||
"dependant_sle_voucher_detail_no": d.reference_name,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
if self.get("is_return"):
|
||||
return
|
||||
@ -808,7 +748,7 @@ class BuyingController(StockController, Subcontracting):
|
||||
if self.doctype == "Material Request":
|
||||
return
|
||||
|
||||
if hasattr(self, "is_subcontracted") and self.is_subcontracted:
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
validate_item_type(self, "is_sub_contracted_item", "subcontracted")
|
||||
else:
|
||||
validate_item_type(self, "is_purchase_item", "purchase")
|
||||
|
@ -77,7 +77,7 @@ def validate_returned_items(doc):
|
||||
if doc.doctype != "Purchase Invoice":
|
||||
select_fields += ",serial_no, batch_no"
|
||||
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
select_fields += ",rejected_qty, received_qty"
|
||||
|
||||
for d in frappe.db.sql(
|
||||
@ -161,7 +161,7 @@ def validate_returned_items(doc):
|
||||
|
||||
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
fields = ["stock_qty"]
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
|
||||
fields.extend(["received_qty", "rejected_qty"])
|
||||
|
||||
already_returned_data = already_returned_items.get(args.item_code) or {}
|
||||
@ -224,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
|
||||
if ref_item_row.get("rate", 0) > item_dict["rate"]:
|
||||
item_dict["rate"] = ref_item_row.get("rate", 0)
|
||||
|
||||
if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
item_dict["received_qty"] += ref_item_row.received_qty
|
||||
item_dict["rejected_qty"] += ref_item_row.rejected_qty
|
||||
|
||||
@ -239,7 +239,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
|
||||
|
||||
def get_already_returned_items(doc):
|
||||
column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty"
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty,
|
||||
sum(abs(child.received_qty) * child.conversion_factor) as received_qty"""
|
||||
|
||||
@ -281,17 +281,21 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
||||
child_doctype = doctype + " Item"
|
||||
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
|
||||
|
||||
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
|
||||
party_type = "supplier"
|
||||
else:
|
||||
party_type = "customer"
|
||||
|
||||
fields = [
|
||||
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
|
||||
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype),
|
||||
]
|
||||
|
||||
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
if doctype != "Subcontracting Receipt":
|
||||
fields += [
|
||||
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype),
|
||||
]
|
||||
|
||||
if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
|
||||
fields += [
|
||||
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
|
||||
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype),
|
||||
@ -342,7 +346,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
# look for Print Heading "Debit Note"
|
||||
doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note"))
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
for tax in doc.get("taxes") or []:
|
||||
if tax.charge_type == "Actual":
|
||||
tax.tax_amount = -1 * tax.tax_amount
|
||||
|
||||
@ -381,8 +385,11 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
for d in doc.get("packed_items"):
|
||||
d.qty = d.qty * -1
|
||||
|
||||
doc.discount_amount = -1 * source.discount_amount
|
||||
doc.run_method("calculate_taxes_and_totals")
|
||||
if doc.get("discount_amount"):
|
||||
doc.discount_amount = -1 * source.discount_amount
|
||||
|
||||
if doctype != "Subcontracting Receipt":
|
||||
doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
@ -393,7 +400,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
if serial_nos:
|
||||
target_doc.serial_no = "\n".join(serial_nos)
|
||||
|
||||
if doctype == "Purchase Receipt":
|
||||
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
source_parent.name, source_parent.supplier, source_doc.name, doctype
|
||||
)
|
||||
@ -405,15 +412,24 @@ 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.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0))
|
||||
target_doc.received_stock_qty = -1 * flt(
|
||||
source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0)
|
||||
)
|
||||
if hasattr(target_doc, "stock_qty"):
|
||||
target_doc.stock_qty = -1 * flt(
|
||||
source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)
|
||||
)
|
||||
target_doc.received_stock_qty = -1 * flt(
|
||||
source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0)
|
||||
)
|
||||
|
||||
target_doc.purchase_order = source_doc.purchase_order
|
||||
target_doc.purchase_order_item = source_doc.purchase_order_item
|
||||
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
||||
target_doc.purchase_receipt_item = source_doc.name
|
||||
if doctype == "Subcontracting Receipt":
|
||||
target_doc.subcontracting_order = source_doc.subcontracting_order
|
||||
target_doc.subcontracting_order_item = source_doc.subcontracting_order_item
|
||||
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
||||
target_doc.subcontracting_receipt_item = source_doc.name
|
||||
else:
|
||||
target_doc.purchase_order = source_doc.purchase_order
|
||||
target_doc.purchase_order_item = source_doc.purchase_order_item
|
||||
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
||||
target_doc.purchase_receipt_item = source_doc.name
|
||||
|
||||
elif doctype == "Purchase Invoice":
|
||||
returned_qty_map = get_returned_qty_map_for_row(
|
||||
@ -525,7 +541,7 @@ def get_rate_for_return(
|
||||
item_row,
|
||||
)
|
||||
|
||||
if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||
if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
|
||||
select_field = "incoming_rate"
|
||||
else:
|
||||
select_field = "abs(stock_value_difference / actual_qty)"
|
||||
@ -560,6 +576,7 @@ def get_return_against_item_fields(voucher_type):
|
||||
"Purchase Invoice": "purchase_invoice_item",
|
||||
"Delivery Note": "dn_detail",
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
"Subcontracting Receipt": "subcontracting_receipt_item",
|
||||
}
|
||||
return return_against_item_fields[voucher_type]
|
||||
|
||||
|
@ -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"))
|
902
erpnext/controllers/subcontracting_controller.py
Normal file
902
erpnext/controllers/subcontracting_controller.py
Normal 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
|
||||
)
|
1077
erpnext/controllers/tests/test_subcontracting_controller.py
Normal file
1077
erpnext/controllers/tests/test_subcontracting_controller.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@ import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import set_backflush_based_on
|
||||
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
|
||||
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||
update_cost_in_all_boms_in_test,
|
||||
@ -18,7 +18,6 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||
|
||||
test_records = frappe.get_test_records("BOM")
|
||||
test_dependencies = ["Item", "Quality Inspection Template"]
|
||||
@ -256,12 +255,29 @@ class TestBOM(FrappeTestCase):
|
||||
bom.submit()
|
||||
# test that sourced_by_supplier rate is zero even after updating cost
|
||||
self.assertEqual(bom.items[2].rate, 0)
|
||||
# test in Purchase Order sourced_by_supplier is not added to Supplied Item
|
||||
po = create_purchase_order(
|
||||
item_code=item_code, qty=1, is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
|
||||
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_service_item,
|
||||
)
|
||||
|
||||
make_service_item("Subcontracted Service Item 1")
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"fg_item": item_code,
|
||||
"fg_item_qty": 1,
|
||||
},
|
||||
]
|
||||
# test in Subcontracting Order sourced_by_supplier is not added to Supplied Item
|
||||
sco = get_subcontracting_order(
|
||||
service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC"
|
||||
)
|
||||
bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1])
|
||||
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
||||
supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
|
||||
self.assertEqual(bom_items, supplied_items)
|
||||
|
||||
def test_bom_tree_representation(self):
|
||||
|
@ -508,7 +508,7 @@ class ProductionPlan(Document):
|
||||
po.is_subcontracted = 1
|
||||
for row in po_list:
|
||||
po_data = {
|
||||
"item_code": row.production_item,
|
||||
"fg_item": row.production_item,
|
||||
"warehouse": row.fg_warehouse,
|
||||
"production_plan_sub_assembly_item": row.name,
|
||||
"bom": row.bom_no,
|
||||
@ -518,9 +518,6 @@ class ProductionPlan(Document):
|
||||
for field in [
|
||||
"schedule_date",
|
||||
"qty",
|
||||
"uom",
|
||||
"stock_uom",
|
||||
"item_name",
|
||||
"description",
|
||||
"production_plan_item",
|
||||
]:
|
||||
|
@ -21,3 +21,4 @@ Payroll
|
||||
Telephony
|
||||
Bulk Transaction
|
||||
E-commerce
|
||||
Subcontracting
|
@ -343,6 +343,7 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
execute:frappe.delete_doc("DocType", "Naming Series")
|
||||
erpnext.patches.v13_0.set_payroll_entry_status
|
||||
erpnext.patches.v13_0.job_card_status_on_hold
|
||||
erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.patches.v14_0.crm_ux_cleanup
|
||||
erpnext.patches.v14_0.remove_india_localisation
|
||||
|
@ -64,4 +64,8 @@ def delete_and_patch_duplicate_bins():
|
||||
bin.update(qty_dict)
|
||||
bin.update_reserved_qty_for_production()
|
||||
bin.update_reserved_qty_for_sub_contracting()
|
||||
if frappe.db.count(
|
||||
"Purchase Order", {"status": ["!=", "Completed"], "is_old_subcontracting_flow": 1}
|
||||
):
|
||||
bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order")
|
||||
bin.db_update()
|
||||
|
@ -15,6 +15,8 @@ def execute():
|
||||
("accounts", "sales_invoice_item"),
|
||||
("accounts", "purchase_invoice_item"),
|
||||
("buying", "purchase_receipt_item_supplied"),
|
||||
("subcontracting", "subcontracting_receipt_item"),
|
||||
("subcontracting", "subcontracting_receipt_supplied_item"),
|
||||
]
|
||||
|
||||
for module, doctype in doctypes_to_reload:
|
||||
|
@ -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()
|
@ -83,9 +83,17 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
if (me.frm.doc.is_subcontracted) {
|
||||
var filters = {'supplier': me.frm.doc.supplier};
|
||||
if (me.frm.doc.is_old_subcontracting_flow) {
|
||||
filters["is_sub_contracted_item"] = 1;
|
||||
}
|
||||
else {
|
||||
filters["is_stock_item"] = 0;
|
||||
}
|
||||
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 }
|
||||
filters: filters
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
@ -470,7 +470,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
cost_center: item.cost_center,
|
||||
tax_category: me.frm.doc.tax_category,
|
||||
item_tax_template: item.item_tax_template,
|
||||
child_docname: item.name
|
||||
child_docname: item.name,
|
||||
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -486,7 +486,11 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
filters = {"is_sales_item": 1};
|
||||
} else if (frm.doc.doctype == 'Purchase Order') {
|
||||
if (frm.doc.is_subcontracted) {
|
||||
filters = {"is_sub_contracted_item": 1};
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
filters = {"is_sub_contracted_item": 1};
|
||||
} else {
|
||||
filters = {"is_stock_item": 0};
|
||||
}
|
||||
} else {
|
||||
filters = {"is_purchase_item": 1};
|
||||
}
|
||||
|
@ -40,25 +40,37 @@ class Bin(Document):
|
||||
self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production))
|
||||
self.db_set("projected_qty", self.projected_qty)
|
||||
|
||||
def update_reserved_qty_for_sub_contracting(self):
|
||||
def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"):
|
||||
# reserved qty
|
||||
|
||||
po = frappe.qb.DocType("Purchase Order")
|
||||
supplied_item = frappe.qb.DocType("Purchase Order Item Supplied")
|
||||
subcontract_order = frappe.qb.DocType(subcontract_doctype)
|
||||
supplied_item = frappe.qb.DocType(
|
||||
"Purchase Order Item Supplied"
|
||||
if subcontract_doctype == "Purchase Order"
|
||||
else "Subcontracting Order Supplied Item"
|
||||
)
|
||||
|
||||
conditions = (
|
||||
(supplied_item.rm_item_code == self.item_code)
|
||||
& (subcontract_order.name == supplied_item.parent)
|
||||
& (subcontract_order.per_received < 100)
|
||||
& (supplied_item.reserve_warehouse == self.warehouse)
|
||||
& (
|
||||
(
|
||||
(subcontract_order.is_old_subcontracting_flow == 1)
|
||||
& (subcontract_order.status != "Closed")
|
||||
& (subcontract_order.docstatus == 1)
|
||||
)
|
||||
if subcontract_doctype == "Purchase Order"
|
||||
else (subcontract_order.docstatus == 1)
|
||||
)
|
||||
)
|
||||
|
||||
reserved_qty_for_sub_contract = (
|
||||
frappe.qb.from_(po)
|
||||
frappe.qb.from_(subcontract_order)
|
||||
.from_(supplied_item)
|
||||
.select(Sum(Coalesce(supplied_item.required_qty, 0)))
|
||||
.where(
|
||||
(supplied_item.rm_item_code == self.item_code)
|
||||
& (po.name == supplied_item.parent)
|
||||
& (po.docstatus == 1)
|
||||
& (po.is_subcontracted)
|
||||
& (po.status != "Closed")
|
||||
& (po.per_received < 100)
|
||||
& (supplied_item.reserve_warehouse == self.warehouse)
|
||||
)
|
||||
.where(conditions)
|
||||
).run()[0][0] or 0.0
|
||||
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
@ -71,23 +83,34 @@ class Bin(Document):
|
||||
else:
|
||||
qty_field = se_item.transfer_qty
|
||||
|
||||
conditions = (
|
||||
(se.docstatus == 1)
|
||||
& (se.purpose == "Send to Subcontractor")
|
||||
& ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code))
|
||||
& (se.name == se_item.parent)
|
||||
& (subcontract_order.docstatus == 1)
|
||||
& (subcontract_order.per_received < 100)
|
||||
& (
|
||||
(
|
||||
(Coalesce(se.purchase_order, "") != "")
|
||||
& (subcontract_order.name == se.purchase_order)
|
||||
& (subcontract_order.is_old_subcontracting_flow == 1)
|
||||
& (subcontract_order.status != "Closed")
|
||||
)
|
||||
if subcontract_doctype == "Purchase Order"
|
||||
else (
|
||||
(Coalesce(se.subcontracting_order, "") != "")
|
||||
& (subcontract_order.name == se.subcontracting_order)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
materials_transferred = (
|
||||
frappe.qb.from_(se)
|
||||
.from_(se_item)
|
||||
.from_(po)
|
||||
.from_(subcontract_order)
|
||||
.select(Sum(qty_field))
|
||||
.where(
|
||||
(se.docstatus == 1)
|
||||
& (se.purpose == "Send to Subcontractor")
|
||||
& (Coalesce(se.purchase_order, "") != "")
|
||||
& ((se_item.item_code == self.item_code) | (se_item.original_item == self.item_code))
|
||||
& (se.name == se_item.parent)
|
||||
& (po.name == se.purchase_order)
|
||||
& (po.docstatus == 1)
|
||||
& (po.is_subcontracted == 1)
|
||||
& (po.status != "Closed")
|
||||
& (po.per_received < 100)
|
||||
)
|
||||
.where(conditions)
|
||||
).run()[0][0] or 0.0
|
||||
|
||||
if reserved_qty_for_sub_contract > materials_transferred:
|
||||
|
@ -1,17 +1,16 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_receipt,
|
||||
make_rm_stock_entry,
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_service_item,
|
||||
set_backflush_based_on,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry
|
||||
@ -22,6 +21,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
|
||||
|
||||
class TestItemAlternative(FrappeTestCase):
|
||||
@ -30,9 +32,7 @@ class TestItemAlternative(FrappeTestCase):
|
||||
make_items()
|
||||
|
||||
def test_alternative_item_for_subcontract_rm(self):
|
||||
frappe.db.set_value(
|
||||
"Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
|
||||
)
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
create_stock_reconciliation(
|
||||
item_code="Alternate Item For A RW 1", warehouse="_Test Warehouse - _TC", qty=5, rate=2000
|
||||
@ -42,15 +42,22 @@ class TestItemAlternative(FrappeTestCase):
|
||||
)
|
||||
|
||||
supplier_warehouse = "Test Supplier Warehouse - _TC"
|
||||
po = create_purchase_order(
|
||||
item="Test Finished Goods - A",
|
||||
is_subcontracted=1,
|
||||
qty=5,
|
||||
rate=3000,
|
||||
supplier_warehouse=supplier_warehouse,
|
||||
)
|
||||
|
||||
rm_item = [
|
||||
make_service_item("Subcontracted Service Item 1")
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 5,
|
||||
"rate": 3000,
|
||||
"fg_item": "Test Finished Goods - A",
|
||||
"fg_item_qty": 5,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
service_items=service_items, supplier_warehouse=supplier_warehouse
|
||||
)
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": "Test Finished Goods - A",
|
||||
"rm_item_code": "Test FG A RW 1",
|
||||
@ -73,14 +80,13 @@ class TestItemAlternative(FrappeTestCase):
|
||||
},
|
||||
]
|
||||
|
||||
rm_item_string = json.dumps(rm_item)
|
||||
reserved_qty_for_sub_contract = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "Test FG A RW 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
"reserved_qty_for_sub_contract",
|
||||
)
|
||||
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||
se.to_warehouse = supplier_warehouse
|
||||
se.insert()
|
||||
|
||||
@ -104,22 +110,17 @@ class TestItemAlternative(FrappeTestCase):
|
||||
after_transfer_reserved_qty_for_sub_contract, flt(reserved_qty_for_sub_contract - 5)
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.save()
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
|
||||
pr = frappe.get_doc("Purchase Receipt", pr.name)
|
||||
scr = frappe.get_doc("Subcontracting Receipt", scr.name)
|
||||
status = False
|
||||
for d in pr.supplied_items:
|
||||
if d.rm_item_code == "Alternate Item For A RW 1":
|
||||
for item in scr.supplied_items:
|
||||
if item.rm_item_code == "Alternate Item For A RW 1":
|
||||
status = True
|
||||
|
||||
self.assertEqual(status, True)
|
||||
frappe.db.set_value(
|
||||
"Buying Settings",
|
||||
None,
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"Material Transferred for Subcontract",
|
||||
)
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
def test_alternative_item_for_production_rm(self):
|
||||
create_stock_reconciliation(
|
||||
|
@ -198,7 +198,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
|
||||
cur_frm.add_custom_button(__('Reopen'), this.reopen_purchase_receipt, __("Status"))
|
||||
}
|
||||
|
||||
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);
|
||||
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_old_subcontracting_flow);
|
||||
}
|
||||
|
||||
make_purchase_invoice() {
|
||||
@ -296,10 +296,11 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt
|
||||
frappe.provide("erpnext.buying");
|
||||
|
||||
frappe.ui.form.on("Purchase Receipt", "is_subcontracted", function(frm) {
|
||||
if (frm.doc.is_subcontracted) {
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
erpnext.buying.get_default_bom(frm);
|
||||
}
|
||||
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
|
||||
|
||||
frm.toggle_reqd("supplier_warehouse", frm.doc.is_old_subcontracting_flow);
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Purchase Receipt Item', {
|
||||
|
@ -133,7 +133,8 @@
|
||||
"transporter_name",
|
||||
"column_break5",
|
||||
"lr_no",
|
||||
"lr_date"
|
||||
"lr_date",
|
||||
"is_old_subcontracting_flow"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -442,7 +443,8 @@
|
||||
"label": "Is Subcontracted",
|
||||
"oldfieldname": "is_subcontracted",
|
||||
"oldfieldtype": "Select",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
@ -1142,13 +1144,21 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_old_subcontracting_flow",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Is Old Subcontracting Flow",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-27 15:59:18.550583",
|
||||
"modified": "2022-06-15 15:43:40.664382",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
@ -123,6 +123,7 @@ class PurchaseReceipt(BuyingController):
|
||||
if getdate(self.posting_date) > getdate(nowdate()):
|
||||
throw(_("Posting Date cannot be future date"))
|
||||
|
||||
self.get_current_stock()
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
@ -234,7 +235,7 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.set_consumed_qty_in_po()
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.db.sql(
|
||||
@ -270,18 +271,7 @@ class PurchaseReceipt(BuyingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_current_stock(self):
|
||||
for d in self.get("supplied_items"):
|
||||
if self.supplier_warehouse:
|
||||
bin = frappe.db.sql(
|
||||
"select actual_qty from `tabBin` where item_code = %s and warehouse = %s",
|
||||
(d.rm_item_code, self.supplier_warehouse),
|
||||
as_dict=1,
|
||||
)
|
||||
d.current_stock = bin and flt(bin[0]["actual_qty"]) or 0
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
@ -2,10 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, cstr, flt, today
|
||||
@ -311,142 +307,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.cancel()
|
||||
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
|
||||
|
||||
def test_subcontracting(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
qty=100,
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
basic_rate=100,
|
||||
)
|
||||
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=500, is_subcontracted=1)
|
||||
self.assertEqual(len(pr.get("supplied_items")), 2)
|
||||
|
||||
rm_supp_cost = sum(d.amount for d in pr.get("supplied_items"))
|
||||
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
|
||||
|
||||
pr.cancel()
|
||||
|
||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
|
||||
)
|
||||
|
||||
se1 = make_stock_entry(
|
||||
item_code="_Test Item",
|
||||
target="Work In Progress - TCP1",
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
se2 = make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
target="Work In Progress - TCP1",
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test FG Item",
|
||||
qty=10,
|
||||
rate=0,
|
||||
is_subcontracted=1,
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse="Stores - TCP1",
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
)
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
|
||||
|
||||
self.assertFalse(gl_entries)
|
||||
|
||||
pr.cancel()
|
||||
se1.cancel()
|
||||
se2.cancel()
|
||||
|
||||
def test_subcontracting_over_receipt(self):
|
||||
"""
|
||||
Behaviour: Raise multiple PRs against one PO that in total
|
||||
receive more than the required qty in the PO.
|
||||
Expected Result: Error Raised for Over Receipt against PO.
|
||||
"""
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
make_subcontracted_item,
|
||||
update_backflush_based_on,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
include_exploded_items=0,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
# stock raw materials in a warehouse before transfer
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100
|
||||
)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": po.supplied_items[0].rm_item_code,
|
||||
"item_name": "_Test FG Item",
|
||||
"qty": po.supplied_items[0].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": po.supplied_items[1].rm_item_code,
|
||||
"item_name": "Test Extra Item 1",
|
||||
"qty": po.supplied_items[1].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
]
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr2 = make_purchase_receipt(po.name)
|
||||
|
||||
pr1.submit()
|
||||
self.assertRaises(frappe.ValidationError, pr2.submit)
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_serial_no_supplier(self):
|
||||
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
|
||||
pr_row_1_serial_no = pr.get("items")[0].serial_no
|
||||
@ -1133,103 +993,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
def test_subcontracted_pr_for_multi_transfer_batches(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_receipt,
|
||||
make_rm_stock_entry,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
update_backflush_based_on,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
item_code = "_Test Subcontracted FG Item 3"
|
||||
|
||||
make_item(
|
||||
"Sub Contracted Raw Material 3",
|
||||
{"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1},
|
||||
)
|
||||
|
||||
create_subcontracted_item(
|
||||
item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"]
|
||||
)
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(
|
||||
item_code=item_code,
|
||||
qty=order_qty,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
ste1 = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="Sub Contracted Raw Material 3",
|
||||
qty=300,
|
||||
basic_rate=100,
|
||||
)
|
||||
ste2 = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="Sub Contracted Raw Material 3",
|
||||
qty=200,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200}
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 3",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 300,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
"name": po.supplied_items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 3",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 200,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
"name": po.supplied_items[0].name,
|
||||
},
|
||||
]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
self.assertEqual(len(se.items), 2)
|
||||
se.items[0].batch_no = ste1.items[0].batch_no
|
||||
se.items[1].batch_no = ste2.items[0].batch_no
|
||||
se.submit()
|
||||
|
||||
supplied_qty = frappe.db.get_value(
|
||||
"Purchase Order Item Supplied",
|
||||
{"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"},
|
||||
"supplied_qty",
|
||||
)
|
||||
|
||||
self.assertEqual(supplied_qty, 500.00)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.save()
|
||||
self.assertEqual(len(pr.supplied_items), 2)
|
||||
|
||||
for row in pr.supplied_items:
|
||||
self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
pr.delete()
|
||||
se.cancel()
|
||||
ste2.cancel()
|
||||
ste1.cancel()
|
||||
po.cancel()
|
||||
|
||||
def test_po_to_pi_and_po_to_pr_worflow_full(self):
|
||||
"""Test following behaviour:
|
||||
- Create PO
|
||||
@ -1568,43 +1331,5 @@ def make_purchase_receipt(**args):
|
||||
return pr
|
||||
|
||||
|
||||
def create_subcontracted_item(**args):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not frappe.db.exists("Item", args.item_code):
|
||||
make_item(
|
||||
args.item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"is_sub_contracted_item": 1,
|
||||
"has_batch_no": args.get("has_batch_no") or 0,
|
||||
},
|
||||
)
|
||||
|
||||
if not args.raw_materials:
|
||||
if not frappe.db.exists("Item", "Test Extra Item 1"):
|
||||
make_item(
|
||||
"Test Extra Item 1",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Item", "Test Extra Item 2"):
|
||||
make_item(
|
||||
"Test Extra Item 2",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
|
||||
|
||||
if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
|
||||
make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
|
||||
|
||||
|
||||
test_dependencies = ["BOM", "Item Price", "Location"]
|
||||
test_records = frappe.get_test_records("Purchase Receipt")
|
||||
|
@ -83,37 +83,5 @@
|
||||
}
|
||||
],
|
||||
"supplier": "_Test Supplier"
|
||||
},
|
||||
|
||||
{
|
||||
"buying_price_list": "_Test Price List",
|
||||
"company": "_Test Company",
|
||||
"conversion_rate": 1.0,
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Receipt",
|
||||
"base_grand_total": 5000.0,
|
||||
"is_subcontracted": 1,
|
||||
"base_net_total": 5000.0,
|
||||
"items": [
|
||||
{
|
||||
"base_amount": 5000.0,
|
||||
"conversion_factor": 1.0,
|
||||
"description": "_Test FG Item",
|
||||
"doctype": "Purchase Receipt Item",
|
||||
"item_code": "_Test FG Item",
|
||||
"item_name": "_Test FG Item",
|
||||
"parentfield": "items",
|
||||
"qty": 10.0,
|
||||
"rate": 500.0,
|
||||
"received_qty": 10.0,
|
||||
"rejected_qty": 0.0,
|
||||
"stock_uom": "_Test UOM",
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"cost_center": "Main - _TC"
|
||||
}
|
||||
],
|
||||
"supplier": "_Test Supplier",
|
||||
"supplier_warehouse": "_Test Warehouse - _TC"
|
||||
}
|
||||
]
|
@ -645,12 +645,15 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.is_old_subcontracting_flow",
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"label": "BOM",
|
||||
"no_copy": 1,
|
||||
"options": "BOM",
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
@ -687,7 +687,10 @@ def update_serial_nos_after_submit(controller, parentfield):
|
||||
|
||||
update_rejected_serial_nos = (
|
||||
True
|
||||
if (controller.doctype in ("Purchase Receipt", "Purchase Invoice") and d.rejected_qty)
|
||||
if (
|
||||
controller.doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt")
|
||||
and d.rejected_qty
|
||||
)
|
||||
else False
|
||||
)
|
||||
accepted_serial_nos_updated = False
|
||||
@ -700,7 +703,11 @@ def update_serial_nos_after_submit(controller, parentfield):
|
||||
qty = d.stock_qty
|
||||
else:
|
||||
warehouse = d.warehouse
|
||||
qty = d.qty if controller.doctype == "Stock Reconciliation" else d.stock_qty
|
||||
qty = (
|
||||
d.qty
|
||||
if controller.doctype in ["Stock Reconciliation", "Subcontracting Receipt"]
|
||||
else d.stock_qty
|
||||
)
|
||||
for sle in stock_ledger_entries:
|
||||
if sle.voucher_detail_no == d.name:
|
||||
if (
|
||||
|
@ -613,7 +613,25 @@ frappe.ui.form.on('Stock Entry', {
|
||||
|
||||
apply_putaway_rule: function (frm) {
|
||||
if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose);
|
||||
}
|
||||
},
|
||||
|
||||
purchase_order: (frm) => {
|
||||
if (frm.doc.purchase_order) {
|
||||
frm.set_value("subcontracting_order", "");
|
||||
}
|
||||
},
|
||||
|
||||
subcontracting_order: (frm) => {
|
||||
if (frm.doc.subcontracting_order) {
|
||||
frm.set_value("purchase_order", "");
|
||||
erpnext.utils.map_current_doc({
|
||||
method: 'erpnext.stock.doctype.stock_entry.stock_entry.get_items_from_subcontracting_order',
|
||||
source_name: frm.doc.subcontracting_order,
|
||||
target_doc: frm,
|
||||
freeze: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Stock Entry Detail', {
|
||||
@ -780,7 +798,16 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
return {
|
||||
"filters": {
|
||||
"docstatus": 1,
|
||||
"is_subcontracted": 1,
|
||||
"is_old_subcontracting_flow": 1,
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query("subcontracting_order", function() {
|
||||
return {
|
||||
"filters": {
|
||||
"docstatus": 1,
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
@ -801,7 +828,12 @@ 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' }
|
||||
this.frm.set_query("supplier_address", erpnext.queries.address_query)
|
||||
|
@ -15,6 +15,7 @@
|
||||
"add_to_transit",
|
||||
"work_order",
|
||||
"purchase_order",
|
||||
"subcontracting_order",
|
||||
"delivery_note_no",
|
||||
"sales_invoice_no",
|
||||
"pick_list",
|
||||
@ -147,12 +148,19 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order"
|
||||
"depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Order",
|
||||
"options": "Purchase Order"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.purpose==\"Send to Subcontractor\"",
|
||||
"fieldname": "subcontracting_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subcontracting Order",
|
||||
"options": "Subcontracting Order"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.purpose==\"Sales Return\"",
|
||||
"fieldname": "delivery_note_no",
|
||||
|
@ -62,6 +62,27 @@ form_grid_templates = {"items": "templates/form_grid/stock_entry_grid.html"}
|
||||
|
||||
|
||||
class StockEntry(StockController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StockEntry, self).__init__(*args, **kwargs)
|
||||
if self.purchase_order:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
"order_doctype": "Purchase Order",
|
||||
"order_field": "purchase_order",
|
||||
"rm_detail_field": "po_detail",
|
||||
"order_supplied_items_field": "Purchase Order Item Supplied",
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
"order_doctype": "Subcontracting Order",
|
||||
"order_field": "subcontracting_order",
|
||||
"rm_detail_field": "sco_rm_detail",
|
||||
"order_supplied_items_field": "Subcontracting Order Supplied Item",
|
||||
}
|
||||
)
|
||||
|
||||
def get_feed(self):
|
||||
return self.stock_entry_type
|
||||
|
||||
@ -134,8 +155,9 @@ class StockEntry(StockController):
|
||||
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
self.update_work_order()
|
||||
self.validate_purchase_order()
|
||||
self.update_purchase_order_supplied_items()
|
||||
self.validate_subcontract_order()
|
||||
self.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
|
||||
self.make_gl_entries()
|
||||
|
||||
@ -154,7 +176,8 @@ class StockEntry(StockController):
|
||||
self.set_material_request_transfer_status("Completed")
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_purchase_order_supplied_items()
|
||||
self.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
|
||||
if self.work_order and self.purpose == "Material Consumption for Manufacture":
|
||||
self.validate_work_order_status()
|
||||
@ -792,8 +815,8 @@ class StockEntry(StockController):
|
||||
|
||||
serial_nos.append(sn)
|
||||
|
||||
def validate_purchase_order(self):
|
||||
"""Throw exception if more raw material is transferred against Purchase Order than in
|
||||
def validate_subcontract_order(self):
|
||||
"""Throw exception if more raw material is transferred against Subcontract Order than in
|
||||
the raw materials supplied table"""
|
||||
backflush_raw_materials_based_on = frappe.db.get_single_value(
|
||||
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
|
||||
@ -801,24 +824,29 @@ class StockEntry(StockController):
|
||||
|
||||
qty_allowance = flt(frappe.db.get_single_value("Buying Settings", "over_transfer_allowance"))
|
||||
|
||||
if not (self.purpose == "Send to Subcontractor" and self.purchase_order):
|
||||
if not (self.purpose == "Send to Subcontractor" and self.get(self.subcontract_data.order_field)):
|
||||
return
|
||||
|
||||
if backflush_raw_materials_based_on == "BOM":
|
||||
purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
|
||||
subcontract_order = frappe.get_doc(
|
||||
self.subcontract_data.order_doctype, self.get(self.subcontract_data.order_field)
|
||||
)
|
||||
for se_item in self.items:
|
||||
item_code = se_item.original_item or se_item.item_code
|
||||
precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||
required_qty = sum(
|
||||
[flt(d.required_qty) for d in purchase_order.supplied_items if d.rm_item_code == item_code]
|
||||
[flt(d.required_qty) for d in subcontract_order.supplied_items if d.rm_item_code == item_code]
|
||||
)
|
||||
|
||||
total_allowed = required_qty + (required_qty * (qty_allowance / 100))
|
||||
|
||||
if not required_qty:
|
||||
bom_no = frappe.db.get_value(
|
||||
"Purchase Order Item",
|
||||
{"parent": self.purchase_order, "item_code": se_item.subcontracted_item},
|
||||
f"{self.subcontract_data.order_doctype} Item",
|
||||
{
|
||||
"parent": self.get(self.subcontract_data.order_field),
|
||||
"item_code": se_item.subcontracted_item,
|
||||
},
|
||||
"bom",
|
||||
)
|
||||
|
||||
@ -830,7 +858,7 @@ class StockEntry(StockController):
|
||||
required_qty = sum(
|
||||
[
|
||||
flt(d.required_qty)
|
||||
for d in purchase_order.supplied_items
|
||||
for d in subcontract_order.supplied_items
|
||||
if d.rm_item_code == original_item_code
|
||||
]
|
||||
)
|
||||
@ -839,26 +867,57 @@ class StockEntry(StockController):
|
||||
|
||||
if not required_qty:
|
||||
frappe.throw(
|
||||
_("Item {0} not found in 'Raw Materials Supplied' table in Purchase Order {1}").format(
|
||||
se_item.item_code, self.purchase_order
|
||||
_("Item {0} not found in 'Raw Materials Supplied' table in {1} {2}").format(
|
||||
se_item.item_code,
|
||||
self.subcontract_data.order_doctype,
|
||||
self.get(self.subcontract_data.order_field),
|
||||
)
|
||||
)
|
||||
total_supplied = frappe.db.sql(
|
||||
"""select sum(transfer_qty)
|
||||
from `tabStock Entry Detail`, `tabStock Entry`
|
||||
where `tabStock Entry`.purchase_order = %s
|
||||
and `tabStock Entry`.docstatus = 1
|
||||
and `tabStock Entry Detail`.item_code = %s
|
||||
and `tabStock Entry Detail`.parent = `tabStock Entry`.name""",
|
||||
(self.purchase_order, se_item.item_code),
|
||||
)[0][0]
|
||||
|
||||
parent = frappe.qb.DocType("Stock Entry")
|
||||
child = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
conditions = (
|
||||
(parent.docstatus == 1)
|
||||
& (child.item_code == se_item.item_code)
|
||||
& (
|
||||
(parent.purchase_order == self.purchase_order)
|
||||
if self.subcontract_data.order_doctype == "Purchase Order"
|
||||
else (parent.subcontracting_order == self.subcontracting_order)
|
||||
)
|
||||
)
|
||||
|
||||
total_supplied = (
|
||||
frappe.qb.from_(parent)
|
||||
.inner_join(child)
|
||||
.on(parent.name == child.parent)
|
||||
.select(Sum(child.transfer_qty))
|
||||
.where(conditions)
|
||||
).run()[0][0]
|
||||
|
||||
if flt(total_supplied, precision) > flt(total_allowed, precision):
|
||||
frappe.throw(
|
||||
_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}").format(
|
||||
se_item.idx, se_item.item_code, total_allowed, self.purchase_order
|
||||
_("Row {0}# Item {1} cannot be transferred more than {2} against {3} {4}").format(
|
||||
se_item.idx,
|
||||
se_item.item_code,
|
||||
total_allowed,
|
||||
self.subcontract_data.order_doctype,
|
||||
self.get(self.subcontract_data.order_field),
|
||||
)
|
||||
)
|
||||
elif not se_item.get(self.subcontract_data.rm_detail_field):
|
||||
filters = {
|
||||
"parent": self.get(self.subcontract_data.order_field),
|
||||
"docstatus": 1,
|
||||
"rm_item_code": se_item.item_code,
|
||||
"main_item_code": se_item.subcontracted_item,
|
||||
}
|
||||
|
||||
order_rm_detail = frappe.db.get_value(
|
||||
self.subcontract_data.order_supplied_items_field, filters, "name"
|
||||
)
|
||||
if order_rm_detail:
|
||||
se_item.db_set(self.subcontract_data.rm_detail_field, order_rm_detail)
|
||||
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
|
||||
for row in self.items:
|
||||
if not row.subcontracted_item:
|
||||
@ -867,17 +926,19 @@ class StockEntry(StockController):
|
||||
row.idx, frappe.bold(row.item_code)
|
||||
)
|
||||
)
|
||||
elif not row.po_detail:
|
||||
elif not row.get(self.subcontract_data.rm_detail_field):
|
||||
filters = {
|
||||
"parent": self.purchase_order,
|
||||
"parent": self.get(self.subcontract_data.order_field),
|
||||
"docstatus": 1,
|
||||
"rm_item_code": row.item_code,
|
||||
"main_item_code": row.subcontracted_item,
|
||||
}
|
||||
|
||||
po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name")
|
||||
if po_detail:
|
||||
row.db_set("po_detail", po_detail)
|
||||
order_rm_detail = frappe.db.get_value(
|
||||
self.subcontract_data.order_supplied_items_field, filters, "name"
|
||||
)
|
||||
if order_rm_detail:
|
||||
row.db_set(self.subcontract_data.rm_detail_field, order_rm_detail)
|
||||
|
||||
def validate_bom(self):
|
||||
for d in self.get("items"):
|
||||
@ -1224,11 +1285,13 @@ class StockEntry(StockController):
|
||||
args.batch_no = get_batch_no(args["item_code"], args["s_warehouse"], args["qty"])
|
||||
|
||||
if (
|
||||
self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get("item_code")
|
||||
self.purpose == "Send to Subcontractor"
|
||||
and self.get(self.subcontract_data.order_field)
|
||||
and args.get("item_code")
|
||||
):
|
||||
subcontract_items = frappe.get_all(
|
||||
"Purchase Order Item Supplied",
|
||||
{"parent": self.purchase_order, "rm_item_code": args.get("item_code")},
|
||||
self.subcontract_data.order_supplied_items_field,
|
||||
{"parent": self.get(self.subcontract_data.order_field), "rm_item_code": args.get("item_code")},
|
||||
"main_item_code",
|
||||
)
|
||||
|
||||
@ -1322,27 +1385,27 @@ class StockEntry(StockController):
|
||||
|
||||
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
|
||||
|
||||
# Get PO Supplied Items Details
|
||||
if self.purchase_order and self.purpose == "Send to Subcontractor":
|
||||
# Get PO Supplied Items Details
|
||||
item_wh = frappe._dict(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
rm_item_code, reserve_warehouse
|
||||
FROM
|
||||
`tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
|
||||
WHERE
|
||||
po.name = poitemsup.parent and po.name = %s """,
|
||||
self.purchase_order,
|
||||
)
|
||||
)
|
||||
# Get Subcontract Order Supplied Items Details
|
||||
if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
|
||||
# Get Subcontract Order Supplied Items Details
|
||||
parent = frappe.qb.DocType(self.subcontract_data.order_doctype)
|
||||
child = frappe.qb.DocType(self.subcontract_data.order_supplied_items_field)
|
||||
|
||||
item_wh = (
|
||||
frappe.qb.from_(parent)
|
||||
.inner_join(child)
|
||||
.on(parent.name == child.parent)
|
||||
.select(child.rm_item_code, child.reserve_warehouse)
|
||||
.where(parent.name == self.get(self.subcontract_data.order_field))
|
||||
).run(as_list=True)
|
||||
|
||||
item_wh = frappe._dict(item_wh)
|
||||
|
||||
for item in item_dict.values():
|
||||
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
|
||||
item["from_warehouse"] = self.pro_doc.wip_warehouse
|
||||
# Get Reserve Warehouse from PO
|
||||
if self.purchase_order and self.purpose == "Send to Subcontractor":
|
||||
# Get Reserve Warehouse from Subcontract Order
|
||||
if self.get(self.subcontract_data.order_field) and self.purpose == "Send to Subcontractor":
|
||||
item["from_warehouse"] = item_wh.get(item.item_code)
|
||||
item["to_warehouse"] = self.to_warehouse if self.purpose == "Send to Subcontractor" else ""
|
||||
|
||||
@ -1478,7 +1541,9 @@ class StockEntry(StockController):
|
||||
fetch_qty_in_stock_uom=False,
|
||||
)
|
||||
|
||||
used_alternative_items = get_used_alternative_items(work_order=self.work_order)
|
||||
used_alternative_items = get_used_alternative_items(
|
||||
subcontract_order_field=self.subcontract_data.order_field, work_order=self.work_order
|
||||
)
|
||||
for item in item_dict.values():
|
||||
# if source warehouse presents in BOM set from_warehouse as bom source_warehouse
|
||||
if item["allow_alternative_item"]:
|
||||
@ -1844,7 +1909,7 @@ class StockEntry(StockController):
|
||||
se_child.is_process_loss = item_row.get("is_process_loss", 0)
|
||||
|
||||
for field in [
|
||||
"po_detail",
|
||||
self.subcontract_data.rm_detail_field,
|
||||
"original_item",
|
||||
"expense_account",
|
||||
"description",
|
||||
@ -1918,33 +1983,37 @@ class StockEntry(StockController):
|
||||
else:
|
||||
frappe.throw(_("Batch {0} of Item {1} is disabled.").format(item.batch_no, item.item_code))
|
||||
|
||||
def update_purchase_order_supplied_items(self):
|
||||
if self.purchase_order and (
|
||||
def update_subcontract_order_supplied_items(self):
|
||||
if self.get(self.subcontract_data.order_field) and (
|
||||
self.purpose in ["Send to Subcontractor", "Material Transfer"] or self.is_return
|
||||
):
|
||||
|
||||
# Get PO Supplied Items Details
|
||||
po_supplied_items = frappe.db.get_all(
|
||||
"Purchase Order Item Supplied",
|
||||
filters={"parent": self.purchase_order},
|
||||
# Get Subcontract Order Supplied Items Details
|
||||
order_supplied_items = frappe.db.get_all(
|
||||
self.subcontract_data.order_supplied_items_field,
|
||||
filters={"parent": self.get(self.subcontract_data.order_field)},
|
||||
fields=["name", "rm_item_code", "reserve_warehouse"],
|
||||
)
|
||||
|
||||
# Get Items Supplied in Stock Entries against PO
|
||||
supplied_items = get_supplied_items(self.purchase_order)
|
||||
# Get Items Supplied in Stock Entries against Subcontract Order
|
||||
supplied_items = get_supplied_items(
|
||||
self.get(self.subcontract_data.order_field),
|
||||
self.subcontract_data.rm_detail_field,
|
||||
self.subcontract_data.order_field,
|
||||
)
|
||||
|
||||
for row in po_supplied_items:
|
||||
for row in order_supplied_items:
|
||||
key, item = row.name, {}
|
||||
if not supplied_items.get(key):
|
||||
# no stock transferred against PO Supplied Items row
|
||||
# no stock transferred against Subcontract Order Supplied Items row
|
||||
item = {"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0}
|
||||
else:
|
||||
item = supplied_items.get(key)
|
||||
|
||||
frappe.db.set_value("Purchase Order Item Supplied", row.name, item)
|
||||
frappe.db.set_value(self.subcontract_data.order_supplied_items_field, row.name, item)
|
||||
|
||||
# RM Item-Reserve Warehouse Dict
|
||||
item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in po_supplied_items}
|
||||
item_wh = {x.get("rm_item_code"): x.get("reserve_warehouse") for x in order_supplied_items}
|
||||
|
||||
for d in self.get("items"):
|
||||
# Update reserved sub contracted quantity in bin based on Supplied Item Details and
|
||||
@ -2145,6 +2214,14 @@ class StockEntry(StockController):
|
||||
|
||||
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
|
||||
|
||||
def update_subcontracting_order_status(self):
|
||||
if self.subcontracting_order and self.purpose == "Send to Subcontractor":
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
update_subcontracting_order_status,
|
||||
)
|
||||
|
||||
update_subcontracting_order_status(self.subcontracting_order)
|
||||
|
||||
def set_missing_values(self):
|
||||
"Updates rate and availability of all the items of mapped doc."
|
||||
self.set_transfer_qty()
|
||||
@ -2293,13 +2370,13 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None):
|
||||
return operating_cost_per_unit
|
||||
|
||||
|
||||
def get_used_alternative_items(purchase_order=None, work_order=None):
|
||||
def get_used_alternative_items(
|
||||
subcontract_order=None, subcontract_order_field="subcontracting_order", work_order=None
|
||||
):
|
||||
cond = ""
|
||||
|
||||
if purchase_order:
|
||||
cond = "and ste.purpose = 'Send to Subcontractor' and ste.purchase_order = '{0}'".format(
|
||||
purchase_order
|
||||
)
|
||||
if subcontract_order:
|
||||
cond = f"and ste.purpose = 'Send to Subcontractor' and ste.{subcontract_order_field} = '{subcontract_order}'"
|
||||
elif work_order:
|
||||
cond = "and ste.purpose = 'Material Transfer for Manufacture' and ste.work_order = '{0}'".format(
|
||||
work_order
|
||||
@ -2352,7 +2429,6 @@ def get_valuation_rate_for_finished_good_entry(work_order):
|
||||
@frappe.whitelist()
|
||||
def get_uom_details(item_code, uom, qty):
|
||||
"""Returns dict `{"conversion_factor": [value], "transfer_qty": qty * [value]}`
|
||||
|
||||
:param args: dict with `item_code`, `uom` and `qty`"""
|
||||
conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor")
|
||||
|
||||
@ -2436,25 +2512,27 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no=None):
|
||||
return sample_quantity
|
||||
|
||||
|
||||
def get_supplied_items(purchase_order):
|
||||
def get_supplied_items(
|
||||
subcontract_order, rm_detail_field="sco_rm_detail", subcontract_order_field="subcontracting_order"
|
||||
):
|
||||
fields = [
|
||||
"`tabStock Entry Detail`.`transfer_qty`",
|
||||
"`tabStock Entry`.`is_return`",
|
||||
"`tabStock Entry Detail`.`po_detail`",
|
||||
f"`tabStock Entry Detail`.`{rm_detail_field}`",
|
||||
"`tabStock Entry Detail`.`item_code`",
|
||||
]
|
||||
|
||||
filters = [
|
||||
["Stock Entry", "docstatus", "=", 1],
|
||||
["Stock Entry", "purchase_order", "=", purchase_order],
|
||||
["Stock Entry", subcontract_order_field, "=", subcontract_order],
|
||||
]
|
||||
|
||||
supplied_item_details = {}
|
||||
for row in frappe.get_all("Stock Entry", fields=fields, filters=filters):
|
||||
if not row.po_detail:
|
||||
if not row.get(rm_detail_field):
|
||||
continue
|
||||
|
||||
key = row.po_detail
|
||||
key = row.get(rm_detail_field)
|
||||
if key not in supplied_item_details:
|
||||
supplied_item_details.setdefault(
|
||||
key, frappe._dict({"supplied_qty": 0, "returned_qty": 0, "total_supplied_qty": 0})
|
||||
@ -2474,6 +2552,39 @@ def get_supplied_items(purchase_order):
|
||||
return supplied_item_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_from_subcontracting_order(source_name, target_doc=None):
|
||||
sco = frappe.get_doc("Subcontracting Order", source_name)
|
||||
|
||||
if sco.docstatus == 1:
|
||||
if target_doc and isinstance(target_doc, str):
|
||||
target_doc = frappe.get_doc(json.loads(target_doc))
|
||||
|
||||
if target_doc.items:
|
||||
target_doc.items = []
|
||||
|
||||
warehouses = {}
|
||||
for item in sco.items:
|
||||
warehouses[item.name] = item.warehouse
|
||||
|
||||
for item in sco.supplied_items:
|
||||
target_doc.append(
|
||||
"items",
|
||||
{
|
||||
"s_warehouse": warehouses.get(item.reference_name),
|
||||
"t_warehouse": sco.supplier_warehouse,
|
||||
"item_code": item.rm_item_code,
|
||||
"qty": item.required_qty,
|
||||
"transfer_qty": item.required_qty,
|
||||
"uom": item.stock_uom,
|
||||
"stock_uom": item.stock_uom,
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
return target_doc
|
||||
|
||||
|
||||
def get_available_materials(work_order) -> dict:
|
||||
data = get_stock_entry_data(work_order)
|
||||
|
||||
|
@ -68,6 +68,7 @@
|
||||
"against_stock_entry",
|
||||
"ste_detail",
|
||||
"po_detail",
|
||||
"sco_rm_detail",
|
||||
"putaway_rule",
|
||||
"column_break_51",
|
||||
"reference_purchase_receipt",
|
||||
@ -496,6 +497,15 @@
|
||||
"print_hide": 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",
|
||||
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",
|
||||
|
@ -409,61 +409,6 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
lcv.cancel()
|
||||
pr.cancel()
|
||||
|
||||
def test_sub_contracted_item_costing(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
company = "_Test Company"
|
||||
rm_item_code = "_Test Item for Reposting"
|
||||
subcontracted_item = "_Test Subcontracted Item for Reposting"
|
||||
|
||||
frappe.db.set_value(
|
||||
"Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM"
|
||||
)
|
||||
make_bom(item=subcontracted_item, raw_materials=[rm_item_code], currency="INR")
|
||||
|
||||
# Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
|
||||
pr = make_purchase_receipt(
|
||||
company=company,
|
||||
posting_date="2020-04-10",
|
||||
warehouse="Stores - _TC",
|
||||
item_code=rm_item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
# Purchase Receipt for subcontracted item
|
||||
pr1 = make_purchase_receipt(
|
||||
company=company,
|
||||
posting_date="2020-04-20",
|
||||
warehouse="Finished Goods - _TC",
|
||||
supplier_warehouse="Stores - _TC",
|
||||
item_code=subcontracted_item,
|
||||
qty=10,
|
||||
rate=20,
|
||||
is_subcontracted=1,
|
||||
)
|
||||
|
||||
self.assertEqual(pr1.items[0].valuation_rate, 120)
|
||||
|
||||
# Update raw material's valuation via LCV, Additional cost = 50
|
||||
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
|
||||
|
||||
pr1.reload()
|
||||
self.assertEqual(pr1.items[0].valuation_rate, 125)
|
||||
|
||||
# check outgoing_rate for DN after reposting
|
||||
incoming_rate = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "item_code": subcontracted_item},
|
||||
"incoming_rate",
|
||||
)
|
||||
self.assertEqual(incoming_rate, 125)
|
||||
|
||||
# cleanup data
|
||||
pr1.cancel()
|
||||
lcv.cancel()
|
||||
pr.cancel()
|
||||
|
||||
def test_back_dated_entry_not_allowed(self):
|
||||
# Back dated stock transactions are only allowed to stock managers
|
||||
frappe.db.set_value(
|
||||
|
@ -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))
|
||||
|
||||
elif args.transaction_type == "buying" and args.doctype != "Material Request":
|
||||
if args.get("is_subcontracted") and item.is_sub_contracted_item != 1:
|
||||
throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
|
||||
if args.get("is_subcontracted"):
|
||||
if args.get("is_old_subcontracting_flow"):
|
||||
if item.is_sub_contracted_item != 1:
|
||||
throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
|
||||
else:
|
||||
if item.is_stock_item:
|
||||
throw(_("Item {0} must be a Non-Stock Item").format(item.name))
|
||||
|
||||
|
||||
def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
|
@ -630,6 +630,7 @@ class update_entries_after(object):
|
||||
"Purchase Invoice",
|
||||
"Delivery Note",
|
||||
"Sales Invoice",
|
||||
"Subcontracting Receipt",
|
||||
):
|
||||
if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
|
||||
from erpnext.controllers.sales_and_purchase_return import (
|
||||
@ -646,6 +647,8 @@ class update_entries_after(object):
|
||||
else:
|
||||
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||
rate_field = "valuation_rate"
|
||||
elif sle.voucher_type == "Subcontracting Receipt":
|
||||
rate_field = "rate"
|
||||
else:
|
||||
rate_field = "incoming_rate"
|
||||
|
||||
@ -659,6 +662,8 @@ class update_entries_after(object):
|
||||
else:
|
||||
if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
|
||||
ref_doctype = "Packed Item"
|
||||
elif sle == "Subcontracting Receipt":
|
||||
ref_doctype = "Subcontracting Receipt Supplied Item"
|
||||
else:
|
||||
ref_doctype = "Purchase Receipt Item Supplied"
|
||||
|
||||
@ -684,6 +689,8 @@ class update_entries_after(object):
|
||||
self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
|
||||
elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||
self.update_rate_on_purchase_receipt(sle, outgoing_rate)
|
||||
elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
|
||||
self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
|
||||
|
||||
def update_rate_on_stock_entry(self, sle, outgoing_rate):
|
||||
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
|
||||
@ -732,6 +739,14 @@ class update_entries_after(object):
|
||||
for d in doc.items + doc.supplied_items:
|
||||
d.db_update()
|
||||
|
||||
def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
|
||||
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
|
||||
frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "rate", outgoing_rate)
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Subcontracting Receipt Supplied Item", sle.voucher_detail_no, "rate", outgoing_rate
|
||||
)
|
||||
|
||||
def get_serialized_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
actual_qty = flt(sle.actual_qty)
|
||||
|
0
erpnext/subcontracting/__init__.py
Normal file
0
erpnext/subcontracting/__init__.py
Normal file
0
erpnext/subcontracting/doctype/__init__.py
Normal file
0
erpnext/subcontracting/doctype/__init__.py
Normal 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 }));
|
@ -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
|
||||
}
|
@ -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()
|
@ -0,0 +1,8 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "subcontracting_order",
|
||||
"transactions": [{"label": _("Reference"), "items": ["Subcontracting Receipt", "Stock Entry"]}],
|
||||
}
|
@ -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];
|
||||
},
|
||||
};
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
@ -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": []
|
||||
}
|
@ -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
|
@ -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": []
|
||||
}
|
@ -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
|
@ -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();
|
||||
},
|
||||
});
|
||||
};
|
@ -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
|
||||
}
|
@ -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)
|
@ -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"]},
|
||||
],
|
||||
}
|
@ -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];
|
||||
},
|
||||
};
|
@ -0,0 +1,374 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_rm_items,
|
||||
get_subcontracting_order,
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_stock_in_entry,
|
||||
make_stock_transfer_entry,
|
||||
make_subcontracted_item,
|
||||
make_subcontracted_items,
|
||||
set_backflush_based_on,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
|
||||
|
||||
class TestSubcontractingReceipt(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
def test_subcontracting(self):
|
||||
set_backflush_based_on("BOM")
|
||||
make_stock_entry(
|
||||
item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
qty=100,
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
basic_rate=100,
|
||||
)
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "_Test FG Item",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.submit()
|
||||
rm_supp_cost = sum(item.amount for item in scr.get("supplied_items"))
|
||||
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
|
||||
|
||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
||||
|
||||
set_backflush_based_on("BOM")
|
||||
make_stock_entry(
|
||||
item_code="_Test Item",
|
||||
target="Work In Progress - TCP1",
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
target="Work In Progress - TCP1",
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 10,
|
||||
"rate": 0,
|
||||
"fg_item": "_Test FG Item",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||
self.assertFalse(gl_entries)
|
||||
|
||||
def test_subcontracting_over_receipt(self):
|
||||
"""
|
||||
Behaviour: Raise multiple SCRs against one SCO that in total
|
||||
receive more than the required qty in the SCO.
|
||||
Expected Result: Error Raised for Over Receipt against SCO.
|
||||
"""
|
||||
from erpnext.controllers.subcontracting_controller import (
|
||||
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.test_subcontracting_order import (
|
||||
make_subcontracted_item,
|
||||
)
|
||||
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"fg_item": "_Test Subcontracted FG Item 1",
|
||||
"fg_item_qty": 1,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
service_items=service_items,
|
||||
include_exploded_items=0,
|
||||
)
|
||||
|
||||
# stock raw materials in a warehouse before transfer
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=10, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="_Test FG Item", qty=1, basic_rate=100
|
||||
)
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=1, basic_rate=100
|
||||
)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": sco.supplied_items[0].rm_item_code,
|
||||
"item_name": "_Test FG Item",
|
||||
"qty": sco.supplied_items[0].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": sco.supplied_items[1].rm_item_code,
|
||||
"item_name": "Test Extra Item 1",
|
||||
"qty": sco.supplied_items[1].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
},
|
||||
]
|
||||
ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items))
|
||||
ste.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
ste.save()
|
||||
ste.submit()
|
||||
|
||||
scr1 = make_subcontracting_receipt(sco.name)
|
||||
scr2 = make_subcontracting_receipt(sco.name)
|
||||
|
||||
scr1.submit()
|
||||
self.assertRaises(frappe.ValidationError, scr2.submit)
|
||||
|
||||
def test_subcontracted_scr_for_multi_transfer_batches(self):
|
||||
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
|
||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||
make_subcontracting_receipt,
|
||||
)
|
||||
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
item_code = "_Test Subcontracted FG Item 3"
|
||||
|
||||
make_item(
|
||||
"Sub Contracted Raw Material 3",
|
||||
{"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1, "create_new_batch": 1},
|
||||
)
|
||||
|
||||
make_subcontracted_item(
|
||||
item_code=item_code, has_batch_no=1, raw_materials=["Sub Contracted Raw Material 3"]
|
||||
)
|
||||
|
||||
order_qty = 500
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 3",
|
||||
"qty": order_qty,
|
||||
"rate": 100,
|
||||
"fg_item": "_Test Subcontracted FG Item 3",
|
||||
"fg_item_qty": order_qty,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
|
||||
ste1 = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="Sub Contracted Raw Material 3",
|
||||
qty=300,
|
||||
basic_rate=100,
|
||||
)
|
||||
ste2 = make_stock_entry(
|
||||
target="_Test Warehouse - _TC",
|
||||
item_code="Sub Contracted Raw Material 3",
|
||||
qty=200,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
transferred_batch = {ste1.items[0].batch_no: 300, ste2.items[0].batch_no: 200}
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 3",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 300,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
"name": sco.supplied_items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": "Sub Contracted Raw Material 3",
|
||||
"item_name": "_Test Item",
|
||||
"qty": 200,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos",
|
||||
"name": sco.supplied_items[0].name,
|
||||
},
|
||||
]
|
||||
|
||||
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
|
||||
self.assertEqual(len(se.items), 2)
|
||||
se.items[0].batch_no = ste1.items[0].batch_no
|
||||
se.items[1].batch_no = ste2.items[0].batch_no
|
||||
se.submit()
|
||||
|
||||
supplied_qty = frappe.db.get_value(
|
||||
"Subcontracting Order Supplied Item",
|
||||
{"parent": sco.name, "rm_item_code": "Sub Contracted Raw Material 3"},
|
||||
"supplied_qty",
|
||||
)
|
||||
|
||||
self.assertEqual(supplied_qty, 500.00)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
self.assertEqual(len(scr.supplied_items), 2)
|
||||
|
||||
for row in scr.supplied_items:
|
||||
self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty)
|
||||
|
||||
def test_subcontracting_order_partial_return(self):
|
||||
sco = get_subcontracting_order()
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
scr1 = make_subcontracting_receipt(sco.name)
|
||||
scr1.save()
|
||||
scr1.submit()
|
||||
|
||||
scr1_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-3)
|
||||
scr1.load_from_db()
|
||||
self.assertEqual(scr1_return.status, "Return")
|
||||
self.assertEqual(scr1.items[0].returned_qty, 3)
|
||||
|
||||
scr2_return = make_return_subcontracting_receipt(scr_name=scr1.name, qty=-7)
|
||||
scr1.load_from_db()
|
||||
self.assertEqual(scr2_return.status, "Return")
|
||||
self.assertEqual(scr1.status, "Return Issued")
|
||||
self.assertEqual(scr1.items[0].returned_qty, 10)
|
||||
|
||||
def test_subcontracting_order_over_return(self):
|
||||
sco = get_subcontracting_order()
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
scr1 = make_subcontracting_receipt(sco.name)
|
||||
scr1.save()
|
||||
scr1.submit()
|
||||
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
|
||||
args = frappe._dict(scr_name=scr1.name, qty=-15)
|
||||
self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args)
|
||||
|
||||
|
||||
def make_return_subcontracting_receipt(**args):
|
||||
args = frappe._dict(args)
|
||||
return_doc = make_return_doc("Subcontracting Receipt", args.scr_name)
|
||||
return_doc.supplier_warehouse = (
|
||||
args.supplier_warehouse or args.warehouse or "_Test Warehouse 1 - _TC"
|
||||
)
|
||||
|
||||
if args.qty:
|
||||
for item in return_doc.items:
|
||||
item.qty = args.qty
|
||||
|
||||
if not args.do_not_save:
|
||||
return_doc.save()
|
||||
if not args.do_not_submit:
|
||||
return_doc.submit()
|
||||
|
||||
return_doc.load_from_db()
|
||||
return return_doc
|
||||
|
||||
|
||||
def get_items(**args):
|
||||
args = frappe._dict(args)
|
||||
return [
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"description": "_Test Item",
|
||||
"doctype": "Subcontracting Receipt Item",
|
||||
"item_code": "_Test Item",
|
||||
"item_name": "_Test Item",
|
||||
"parentfield": "items",
|
||||
"qty": 5.0,
|
||||
"rate": 50.0,
|
||||
"received_qty": 5.0,
|
||||
"rejected_qty": 0.0,
|
||||
"stock_uom": "_Test UOM",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"cost_center": args.cost_center or "Main - _TC",
|
||||
},
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"description": "_Test Item Home Desktop 100",
|
||||
"doctype": "Subcontracting Receipt Item",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"item_name": "_Test Item Home Desktop 100",
|
||||
"parentfield": "items",
|
||||
"qty": 5.0,
|
||||
"rate": 50.0,
|
||||
"received_qty": 5.0,
|
||||
"rejected_qty": 0.0,
|
||||
"stock_uom": "_Test UOM",
|
||||
"warehouse": args.warehouse or "_Test Warehouse 1 - _TC",
|
||||
"cost_center": args.cost_center or "Main - _TC",
|
||||
},
|
||||
]
|
@ -0,0 +1,475 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2022-04-13 16:05:55.395695",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"column_break_2",
|
||||
"item_name",
|
||||
"section_break_4",
|
||||
"description",
|
||||
"brand",
|
||||
"image_column",
|
||||
"image",
|
||||
"image_view",
|
||||
"received_and_accepted",
|
||||
"received_qty",
|
||||
"qty",
|
||||
"rejected_qty",
|
||||
"returned_qty",
|
||||
"col_break2",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"tracking_section",
|
||||
"col_break_tracking_section",
|
||||
"rate_and_amount",
|
||||
"rate",
|
||||
"amount",
|
||||
"column_break_19",
|
||||
"rm_cost_per_qty",
|
||||
"service_cost_per_qty",
|
||||
"additional_cost_per_qty",
|
||||
"rm_supp_cost",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
"rejected_warehouse",
|
||||
"subcontracting_order",
|
||||
"column_break_40",
|
||||
"schedule_date",
|
||||
"quality_inspection",
|
||||
"subcontracting_order_item",
|
||||
"subcontracting_receipt_item",
|
||||
"section_break_45",
|
||||
"bom",
|
||||
"serial_no",
|
||||
"col_break5",
|
||||
"batch_no",
|
||||
"rejected_serial_no",
|
||||
"expense_account",
|
||||
"manufacture_details",
|
||||
"manufacturer",
|
||||
"column_break_16",
|
||||
"manufacturer_part_no",
|
||||
"accounting_dimensions_section",
|
||||
"project",
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"section_break_80",
|
||||
"page_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 3,
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"print_width": "100px",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"label": "Item Name",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Description",
|
||||
"print_width": "300px",
|
||||
"reqd": 1,
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach",
|
||||
"hidden": 1,
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "image_view",
|
||||
"fieldtype": "Image",
|
||||
"label": "Image View",
|
||||
"options": "image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "received_and_accepted",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Received and Accepted"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"default": "0",
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Quantity",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Accepted Quantity",
|
||||
"no_copy": 1,
|
||||
"print_width": "100px",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "rejected_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Rejected Quantity",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break2",
|
||||
"fieldtype": "Column Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Conversion Factor",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rate_and_amount",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Rate and Amount"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "currency",
|
||||
"print_width": "100px",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_19",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "rm_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Raw Material Cost Per Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "service_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Service Cost Per Qty",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "additional_cost_per_qty",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Additional Cost Per Qty",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse_and_reference",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Warehouse and Reference"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Accepted Warehouse",
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "rejected_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Rejected Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
"print_width": "100px",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "quality_inspection",
|
||||
"fieldtype": "Link",
|
||||
"label": "Quality Inspection",
|
||||
"no_copy": 1,
|
||||
"options": "Quality Inspection",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_40",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontracting_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subcontracting Order",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Order",
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Required By",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_45",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Serial No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Batch No",
|
||||
"no_copy": 1,
|
||||
"options": "Batch",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"fieldname": "rejected_serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Rejected Serial No",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontracting_order_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Subcontracting Order Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "bom",
|
||||
"fieldtype": "Link",
|
||||
"label": "BOM",
|
||||
"no_copy": 1,
|
||||
"options": "BOM",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.brand",
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Brand",
|
||||
"options": "Brand",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rm_supp_cost",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Raw Materials Supplied Cost",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"print_width": "150px",
|
||||
"read_only": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Expense Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "manufacture_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Manufacture"
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Manufacturer",
|
||||
"options": "Manufacturer"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturer_part_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Manufacturer Part Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontracting_receipt_item",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Subcontracting Receipt Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "image_column",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "tracking_section",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break_tracking_section",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": ":Company",
|
||||
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_80",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"fieldname": "page_break",
|
||||
"fieldtype": "Check",
|
||||
"label": "Page Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "returned_qty",
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-21 12:07:55.899701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SubcontractingReceiptItem(Document):
|
||||
pass
|
@ -0,0 +1,198 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2022-04-18 10:45:16.538479",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"rm_item_code",
|
||||
"item_name",
|
||||
"bom_detail_no",
|
||||
"col_break1",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"reference_name",
|
||||
"secbreak_1",
|
||||
"rate",
|
||||
"col_break2",
|
||||
"amount",
|
||||
"secbreak_2",
|
||||
"required_qty",
|
||||
"col_break3",
|
||||
"consumed_qty",
|
||||
"current_stock",
|
||||
"secbreak_3",
|
||||
"batch_no",
|
||||
"col_break4",
|
||||
"serial_no",
|
||||
"subcontracting_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "main_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rm_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Raw Material Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_global_search": 1,
|
||||
"label": "Description",
|
||||
"print_width": "300px",
|
||||
"read_only": 1,
|
||||
"width": "300px"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"no_copy": 1,
|
||||
"options": "Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Text",
|
||||
"label": "Serial No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Available Qty For Consumption",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty to be Consumed",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock Uom",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Conversion Factor",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "current_stock",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Current Stock",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bom_detail_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "BOM Detail No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secbreak_1",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "secbreak_2",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "secbreak_3",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subcontracting_order",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Subcontracting Order",
|
||||
"no_copy": 1,
|
||||
"options": "Subcontracting Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-18 10:45:16.538479",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Supplied Item",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class SubcontractingReceiptSuppliedItem(Document):
|
||||
pass
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user