Merge branch 'develop' into rfq-email-addressing

This commit is contained in:
Marica 2022-07-08 15:39:20 +05:30 committed by GitHub
commit dc3d492c83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
96 changed files with 7638 additions and 3369 deletions

View File

@ -539,7 +539,7 @@ frappe.ui.form.on("Purchase Invoice", {
},
add_custom_buttons: function(frm) {
if (frm.doc.per_received < 100) {
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frm.events.make_purchase_receipt(frm);
}, __('Create'));
@ -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);
},

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Purchase Order", {
setup: function(frm) {
if (frm.doc.is_old_subcontracting_flow) {
frm.set_query("reserve_warehouse", "supplied_items", function() {
return {
filters: {
@ -17,6 +18,7 @@ frappe.ui.form.on("Purchase Order", {
}
}
});
}
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,6 +176,8 @@ 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) {
// 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,
@ -151,6 +187,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
})
});
}
}
if (this.frm.has_perm("submit")) {
if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) {
if (doc.status != "On Hold") {
@ -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,6 +665,7 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
}
}
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 {
@ -624,6 +677,7 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt
]
}
}
}
function set_schedule_date(frm) {
if(frm.doc.schedule_date){
@ -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);
}
});

View File

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

View File

@ -69,8 +69,12 @@ class PurchaseOrder(BuyingController):
self.validate_with_previous_doc()
self.validate_for_subcontracting()
self.validate_minimum_order_qty()
if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items()
self.create_raw_materials_supplied("supplied_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(
_("BOM is not specified for subcontracting item {0} at row {1}").format(
item.item_code, item.idx
_("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.fg_item:
frappe.throw(
_("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.notify_update()
clear_doctype_notifications(self)
@ -310,8 +338,6 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty()
self.update_ordered_qty()
self.validate_budget()
if self.is_subcontracted:
self.update_reserved_qty_for_subcontract()
frappe.get_doc("Authorization Control").validate_approving_authority(
@ -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.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):
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()
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

View File

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

View File

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

View File

@ -1,38 +1,4 @@
[
{
"advance_paid": 0.0,
"buying_price_list": "_Test Price List",
"company": "_Test Company",
"conversion_rate": 1.0,
"currency": "INR",
"doctype": "Purchase Order",
"base_grand_total": 5000.0,
"grand_total": 5000.0,
"is_subcontracted": 1,
"naming_series": "_T-Purchase Order-",
"base_net_total": 5000.0,
"items": [
{
"base_amount": 5000.0,
"conversion_factor": 1.0,
"description": "_Test FG Item",
"doctype": "Purchase Order Item",
"item_code": "_Test FG Item",
"item_name": "_Test FG Item",
"parentfield": "items",
"qty": 10.0,
"rate": 500.0,
"schedule_date": "2013-03-01",
"stock_uom": "_Test UOM",
"uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC"
}
],
"supplier": "_Test Supplier",
"supplier_name": "_Test Supplier",
"transaction_date": "2013-02-12",
"schedule_date": "2013-02-13"
},
{
"advance_paid": 0.0,
"buying_price_list": "_Test Price List",

View File

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

View File

@ -27,19 +27,16 @@ frappe.query_reports["Subcontract Order Summary"] = {
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"
}
]
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,8 +11,7 @@ from erpnext.accounts.doctype.budget.budget import validate_expense_against_budg
from erpnext.accounts.party import get_party_details
from erpnext.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,6 +256,7 @@ class BuyingController(StockController, Subcontracting):
)
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
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
@ -263,6 +264,10 @@ class BuyingController(StockController, Subcontracting):
+ 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
@ -300,7 +305,7 @@ class BuyingController(StockController, Subcontracting):
raise_error_if_no_rate=False,
)
rate = flt(outgoing_rate * d.conversion_factor, d.precision("rate"))
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate")
@ -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,6 +494,7 @@ class BuyingController(StockController, Subcontracting):
)
)
if self.get("is_old_subcontracting_flow"):
self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
@ -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")

View File

@ -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),
]
if doctype != "Subcontracting Receipt":
fields += [
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype),
]
if doctype in ("Purchase Receipt", "Purchase Invoice"):
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,7 +385,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
for d in doc.get("packed_items"):
d.qty = d.qty * -1
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):
@ -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,11 +412,20 @@ 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))
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)
)
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
@ -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]

View File

@ -1,469 +0,0 @@
import copy
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, flt, get_link_to_form
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class Subcontracting:
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == "Purchase Invoice" and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_purchase_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
)
def __get_purchase_orders(self):
self.purchase_orders = []
if self.doctype == "Purchase Order":
return
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
def __identify_change_in_item_table(self):
self.__changed_name = []
self.__reference_name = []
if self.doctype == "Purchase Order" or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
for n_row in self.items:
self.__reference_name.append(n_row.name)
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
self.__changed_name.append(n_row.name)
if item_dict.get(n_row.name):
del item_dict[n_row.name]
self.__changed_name.extend(item_dict.keys())
def __get_data_before_save(self):
item_dict = {}
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self._doc_before_save:
for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
def get_available_materials(self):
"""Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, purchase_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
"""
if not self.purchase_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if key not in self.available_materials:
self.available_materials.setdefault(
key,
frappe._dict(
{
"qty": 0,
"serial_no": [],
"batch_no": defaultdict(float),
"item_details": row,
"po_details": [],
}
),
)
details = self.available_materials[key]
details.qty += row.qty
details.po_details.append(row.po_detail)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
for doctype in ["Purchase Receipt", "Purchase Invoice"]:
self.__update_consumed_materials(doctype)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
"""Deduct the consumed materials from the available materials."""
pr_items = self.__get_received_items(doctype)
if not pr_items:
return ([], {}) if return_consumed_items else None
pr_items = {d.name: d.get(self.get("po_field") or "purchase_order") for d in pr_items}
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
if return_consumed_items:
return (consumed_materials, pr_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]["qty"] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def __get_transferred_items(self):
fields = ["`tabStock Entry`.`purchase_order`"]
alias_dict = {
"item_code": "rm_item_code",
"subcontracted_item": "main_item_code",
"basic_rate": "rate",
}
child_table_fields = [
"item_code",
"item_name",
"description",
"qty",
"basic_rate",
"amount",
"serial_no",
"uom",
"subcontracted_item",
"stock_uom",
"batch_no",
"conversion_factor",
"s_warehouse",
"t_warehouse",
"item_group",
"po_detail",
]
if self.backflush_based_on == "BOM":
child_table_fields.append("original_item")
for field in child_table_fields:
fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
filters = [
["Stock Entry", "docstatus", "=", 1],
["Stock Entry", "purpose", "=", "Send to Subcontractor"],
["Stock Entry", "purchase_order", "in", self.purchase_orders],
]
return frappe.get_all("Stock Entry", fields=fields, filters=filters)
def __get_received_items(self, doctype):
fields = []
self.po_field = "purchase_order"
for field in ["name", self.po_field, "parent"]:
fields.append(f"`tab{doctype} Item`.`{field}`")
filters = [
[doctype, "docstatus", "=", 1],
[f"{doctype} Item", self.po_field, "in", self.purchase_orders],
]
if doctype == "Purchase Invoice":
filters.append(["Purchase Invoice", "update_stock", "=", 1])
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
def __get_consumed_items(self, doctype, pr_items):
return frappe.get_all(
"Purchase Receipt Item Supplied",
fields=[
"serial_no",
"rm_item_code",
"reference_name",
"batch_no",
"consumed_qty",
"main_item_code",
],
filters={"docstatus": 1, "reference_name": ("in", list(pr_items)), "parenttype": doctype},
)
def __set_alternative_item_details(self, row):
if row.get("original_item"):
self.alternative_item_details[row.get("original_item")] = row
def __get_pending_qty_to_receive(self):
"""Get qty to be received against the purchase order."""
self.qty_to_be_received = defaultdict(float)
if (
self.doctype != "Purchase Order" and self.backflush_based_on != "BOM" and self.purchase_orders
):
for row in frappe.get_all(
"Purchase Order Item",
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
filters={"docstatus": 1, "parent": ("in", self.purchase_orders)},
):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
alias_dict = {
"item_code": "rm_item_code",
"name": "bom_detail_no",
"source_warehouse": "reserve_warehouse",
}
for field in [
"item_code",
"name",
"rate",
"stock_uom",
"source_warehouse",
"description",
"item_name",
"stock_uom",
]:
fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
filters = [
[doctype, "parent", "=", bom_no],
[doctype, "docstatus", "=", 1],
["BOM", "item", "=", item_code],
[doctype, "sourced_by_supplier", "=", 0],
]
return (
frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
)
def __remove_changed_rows(self):
if not self.__changed_name:
return
i = 1
self.set(self.raw_material_table, [])
for d in self._doc_before_save.supplied_items:
if d.reference_name in self.__changed_name:
continue
if d.reference_name not in self.__reference_name:
continue
d.idx = i
self.append("supplied_items", d)
i += 1
def __set_supplied_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if self.doctype != "Purchase Order" and (
(self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name)
):
continue
if self.doctype == "Purchase Order" or self.backflush_based_on == "BOM":
for bom_item in self.__get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != "BOM":
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
if self.qty_to_be_received:
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
def __update_reserve_warehouse(self, row, item):
if self.doctype == "Purchase Order":
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.purchase_order)
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
"UOM", transfer_item.item_details.stock_uom, "must_be_whole_number"
):
return frappe.utils.ceil(qty)
return qty
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == "Purchase Order":
rm_obj.required_qty = qty
else:
rm_obj.consumed_qty = 0
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]["batch_no"][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update(
{
"consumed_qty": qty,
"batch_no": batch_no,
"required_qty": qty,
"purchase_order": item_row.purchase_order,
}
)
self.__set_serial_nos(item_row, rm_obj)
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
rm_obj.serial_no = "\n".join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]["serial_no"].remove(sn)
def set_consumed_qty_in_po(self):
# Update consumed qty back in the purchase order
if not self.is_subcontracted:
return
self.__get_purchase_orders()
itemwise_consumed_qty = defaultdict(float)
for doctype in ["Purchase Receipt", "Purchase Invoice"]:
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_po(itemwise_consumed_qty)
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
filters = {"docstatus": 1, "parent": ("in", self.purchase_orders)}
for row in frappe.get_all(
"Purchase Order Item Supplied", fields=fields, filters=filters, order_by="idx"
):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value("Purchase Order Item Supplied", row.name, "consumed_qty", consumed_qty)
def __validate_supplied_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
"batch_no"
):
link = get_link_to_form("Purchase Order", row.purchase_order)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_no"):
serial_nos = get_serial_nos(row.get("serial_no"))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form("Purchase Order", row.purchase_order)
msg = f"The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))

View File

@ -0,0 +1,902 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import json
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, cstr, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_incoming_rate
class SubcontractingController(StockController):
def __init__(self, *args, **kwargs):
super(SubcontractingController, self).__init__(*args, **kwargs)
if self.get("is_old_subcontracting_flow"):
self.subcontract_data = frappe._dict(
{
"order_doctype": "Purchase Order",
"order_field": "purchase_order",
"rm_detail_field": "po_detail",
"receipt_supplied_items_field": "Purchase Receipt Item Supplied",
"order_supplied_items_field": "Purchase Order Item Supplied",
}
)
else:
self.subcontract_data = frappe._dict(
{
"order_doctype": "Subcontracting Order",
"order_field": "subcontracting_order",
"rm_detail_field": "sco_rm_detail",
"receipt_supplied_items_field": "Subcontracting Receipt Supplied Item",
"order_supplied_items_field": "Subcontracting Order Supplied Item",
}
)
def before_validate(self):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.remove_empty_rows()
self.set_items_conversion_factor()
def validate(self):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.validate_items()
self.create_raw_materials_supplied()
else:
super(SubcontractingController, self).validate()
def remove_empty_rows(self):
for key in ["service_items", "items", "supplied_items"]:
if self.get(key):
idx = 1
for item in self.get(key)[:]:
if not (item.get("item_code") or item.get("main_item_code")):
self.get(key).remove(item)
else:
item.idx = idx
idx += 1
def set_items_conversion_factor(self):
for item in self.get("items"):
if not item.conversion_factor:
item.conversion_factor = 1
def validate_items(self):
for item in self.items:
if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"):
msg = f"Item {item.item_name} must be a subcontracted item."
frappe.throw(_(msg))
if item.bom:
bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active:
msg = f"Please select an active BOM for Item {item.item_name}."
frappe.throw(_(msg))
if bom.item != item.item_code:
msg = f"Please select an valid BOM for Item {item.item_name}."
frappe.throw(_(msg))
def __get_data_before_save(self):
item_dict = {}
if (
self.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]
and self._doc_before_save
):
for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
def __identify_change_in_item_table(self):
self.__changed_name = []
self.__reference_name = []
if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
for row in self.items:
self.__reference_name.append(row.name)
if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]:
self.__changed_name.append(row.name)
if item_dict.get(row.name):
del item_dict[row.name]
self.__changed_name.extend(item_dict.keys())
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
)
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_subcontract_orders(self):
self.subcontract_orders = []
if self.doctype in ["Purchase Order", "Subcontracting Order"]:
return
self.subcontract_orders = [
item.get(self.subcontract_data.order_field)
for item in self.items
if item.get(self.subcontract_data.order_field)
]
def __get_pending_qty_to_receive(self):
"""Get qty to be received against the subcontract order."""
self.qty_to_be_received = defaultdict(float)
if (
self.doctype != self.subcontract_data.order_doctype
and self.backflush_based_on != "BOM"
and self.subcontract_orders
):
for row in frappe.get_all(
f"{self.subcontract_data.order_doctype} Item",
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_transferred_items(self):
fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"]
alias_dict = {
"item_code": "rm_item_code",
"subcontracted_item": "main_item_code",
"basic_rate": "rate",
}
child_table_fields = [
"item_code",
"item_name",
"description",
"qty",
"basic_rate",
"amount",
"serial_no",
"uom",
"subcontracted_item",
"stock_uom",
"batch_no",
"conversion_factor",
"s_warehouse",
"t_warehouse",
"item_group",
self.subcontract_data.rm_detail_field,
]
if self.backflush_based_on == "BOM":
child_table_fields.append("original_item")
for field in child_table_fields:
fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
filters = [
["Stock Entry", "docstatus", "=", 1],
["Stock Entry", "purpose", "=", "Send to Subcontractor"],
["Stock Entry", self.subcontract_data.order_field, "in", self.subcontract_orders],
]
return frappe.get_all("Stock Entry", fields=fields, filters=filters)
def __set_alternative_item_details(self, row):
if row.get("original_item"):
self.alternative_item_details[row.get("original_item")] = row
def __get_received_items(self, doctype):
fields = []
for field in ["name", self.subcontract_data.order_field, "parent"]:
fields.append(f"`tab{doctype} Item`.`{field}`")
filters = [
[doctype, "docstatus", "=", 1],
[f"{doctype} Item", self.subcontract_data.order_field, "in", self.subcontract_orders],
]
if doctype == "Purchase Invoice":
filters.append(["Purchase Invoice", "update_stock", "=", 1])
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
def __get_consumed_items(self, doctype, receipt_items):
return frappe.get_all(
self.subcontract_data.receipt_supplied_items_field,
fields=[
"serial_no",
"rm_item_code",
"reference_name",
"batch_no",
"consumed_qty",
"main_item_code",
],
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
"""Deduct the consumed materials from the available materials."""
receipt_items = self.__get_received_items(doctype)
if not receipt_items:
return ([], {}) if return_consumed_items else None
receipt_items = {
item.name: item.get(self.subcontract_data.order_field) for item in receipt_items
}
consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys())
if return_consumed_items:
return (consumed_materials, receipt_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]["qty"] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def get_available_materials(self):
"""Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, subcontract_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
"""
if not self.subcontract_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if key not in self.available_materials:
self.available_materials.setdefault(
key,
frappe._dict(
{
"qty": 0,
"serial_no": [],
"batch_no": defaultdict(float),
"item_details": row,
f"{self.subcontract_data.rm_detail_field}s": [],
}
),
)
details = self.available_materials[key]
details.qty += row.qty
details[f"{self.subcontract_data.rm_detail_field}s"].append(
row.get(self.subcontract_data.rm_detail_field)
)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
if self.get("is_old_subcontracting_flow"):
for doctype in ["Purchase Receipt", "Purchase Invoice"]:
self.__update_consumed_materials(doctype)
else:
self.__update_consumed_materials("Subcontracting Receipt")
def __remove_changed_rows(self):
if not self.__changed_name:
return
i = 1
self.set(self.raw_material_table, [])
for item in self._doc_before_save.supplied_items:
if item.reference_name in self.__changed_name:
continue
if item.reference_name not in self.__reference_name:
continue
item.idx = i
self.append("supplied_items", item)
i += 1
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
alias_dict = {
"item_code": "rm_item_code",
"name": "bom_detail_no",
"source_warehouse": "reserve_warehouse",
}
for field in [
"item_code",
"name",
"rate",
"stock_uom",
"source_warehouse",
"description",
"item_name",
"stock_uom",
]:
fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
filters = [
[doctype, "parent", "=", bom_no],
[doctype, "docstatus", "=", 1],
["BOM", "item", "=", item_code],
[doctype, "sourced_by_supplier", "=", 0],
]
return (
frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
)
def __update_reserve_warehouse(self, row, item):
if self.doctype == self.subcontract_data.order_doctype:
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
rm_obj.serial_no = "\n".join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]["serial_no"].remove(sn)
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update(
{
"consumed_qty": qty,
"batch_no": batch_no,
"required_qty": qty,
self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field),
}
)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]["batch_no"][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == "Subcontracting Receipt":
args = frappe._dict(
{
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"serial_no": rm_obj.serial_no,
"batch_no": rm_obj.batch_no,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
"allow_zero_valuation": 1,
}
)
rm_obj.rate = get_incoming_rate(args)
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = qty
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
else:
rm_obj.consumed_qty = 0
setattr(
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
)
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
"UOM", transfer_item.item_details.stock_uom, "must_be_whole_number"
):
return frappe.utils.ceil(qty)
return qty
def __set_supplied_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if self.doctype != self.subcontract_data.order_doctype and (
(self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name)
):
continue
if self.doctype == self.subcontract_data.order_doctype or self.backflush_based_on == "BOM":
for bom_item in self.__get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != "BOM":
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (
row.item_code,
row.get(self.subcontract_data.order_field),
) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
if self.qty_to_be_received:
self.qty_to_be_received[
(row.item_code, row.get(self.subcontract_data.order_field))
] -= row.qty
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_subcontract_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
"batch_no"
):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_no"):
serial_nos = get_serial_nos(row.get("serial_no"))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
def __validate_supplied_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == "Purchase Invoice" and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def create_raw_materials_supplied(self, raw_material_table="supplied_items"):
self.set_materials_for_subcontracted_items(raw_material_table)
if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
item.rm_supp_cost = 0.0
def __update_consumed_qty_in_subcontract_order(self, itemwise_consumed_qty):
fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
filters = {"docstatus": 1, "parent": ("in", self.subcontract_orders)}
for row in frappe.get_all(
self.subcontract_data.order_supplied_items_field, fields=fields, filters=filters, order_by="idx"
):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value(
self.subcontract_data.order_supplied_items_field, row.name, "consumed_qty", consumed_qty
)
def set_consumed_qty_in_subcontract_order(self):
# Update consumed qty back in the subcontract order
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"] or self.get(
"is_old_subcontracting_flow"
):
self.__get_subcontract_orders()
itemwise_consumed_qty = defaultdict(float)
if self.get("is_old_subcontracting_flow"):
doctypes = ["Purchase Receipt", "Purchase Invoice"]
else:
doctypes = ["Subcontracting Receipt"]
for doctype in doctypes:
consumed_items, receipt_items = self.__update_consumed_materials(
doctype, return_consumed_items=True
)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_subcontract_order(itemwise_consumed_qty)
def update_ordered_and_reserved_qty(self):
sco_map = {}
for item in self.get("items"):
if self.doctype == "Subcontracting Receipt" and item.subcontracting_order:
sco_map.setdefault(item.subcontracting_order, []).append(item.subcontracting_order_item)
for sco, sco_item_rows in sco_map.items():
if sco and sco_item_rows:
sco_doc = frappe.get_doc("Subcontracting Order", sco)
if sco_doc.status in ["Closed", "Cancelled"]:
frappe.throw(
_("{0} {1} is cancelled or closed").format(_("Subcontracting Order"), sco),
frappe.InvalidStatusError,
)
sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows)
sco_doc.update_reserved_qty_for_subcontracting()
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
if hasattr(self, "supplied_items"):
for item in self.get("supplied_items"):
# negative quantity is passed, as raw material qty has to be decreased
# when SCR is submitted and it has to be increased when SCR is cancelled
sl_entries.append(
self.get_sl_entries(
item,
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1 * flt(item.consumed_qty),
"dependant_sle_voucher_detail_no": item.reference_name,
},
)
)
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_and_reserved_qty()
sl_entries = []
stock_items = self.get_stock_items()
for item in self.get("items"):
if item.item_code in stock_items and item.warehouse:
scr_qty = flt(item.qty) * flt(item.conversion_factor)
if scr_qty:
sle = self.get_sl_entries(
item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
)
rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
incoming_rate = flt(item.rate, rate_db_precision)
sle.update(
{
"incoming_rate": incoming_rate,
"recalculate_rate": 1,
}
)
sl_entries.append(sle)
if flt(item.rejected_qty) != 0:
sl_entries.append(
self.get_sl_entries(
item,
{
"warehouse": item.rejected_warehouse,
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
"serial_no": cstr(item.rejected_serial_no).strip(),
"incoming_rate": 0.0,
"recalculate_rate": 1,
},
)
)
self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0
for item in self.get("supplied_items"):
if item.reference_name == item_row_id:
if (
self.get("is_old_subcontracting_flow")
and reset_outgoing_rate
and frappe.get_cached_value("Item", item.rm_item_code, "is_stock_item")
):
rate = get_incoming_rate(
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
"serial_no": item.serial_no,
"batch_no": item.batch_no,
}
)
if rate > 0:
item.rate = rate
item.amount = flt(flt(item.consumed_qty) * flt(item.rate), item.precision("amount"))
supplied_items_cost += item.amount
return supplied_items_cost
def set_subcontracting_order_status(self):
if self.doctype == "Subcontracting Order":
self.update_status()
elif self.doctype == "Subcontracting Receipt":
self.__get_subcontract_orders
if self.subcontract_orders:
for sco in set(self.subcontract_orders):
sco_doc = frappe.get_doc("Subcontracting Order", sco)
sco_doc.update_status()
@frappe.whitelist()
def get_current_stock(self):
if self.doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
for item in self.get("supplied_items"):
if self.supplier_warehouse:
actual_qty = frappe.db.get_value(
"Bin",
{"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse},
"actual_qty",
)
item.current_stock = flt(actual_qty) or 0
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
self._sub_contracted_items = []
item_codes = list(set(item.item_code for item in self.get("items")))
if item_codes:
items = frappe.get_all(
"Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1}
)
self._sub_contracted_items = [item.name for item in items]
return self._sub_contracted_items
def get_item_details(items):
item = frappe.qb.DocType("Item")
item_list = (
frappe.qb.from_(item)
.select(item.item_code, item.description, item.allow_alternative_item)
.where(item.name.isin(items))
.run(as_dict=True)
)
item_details = {}
for item in item_list:
item_details[item.item_code] = item
return item_details
@frappe.whitelist()
def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"):
rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
fg_items = list(set(item["item_code"] for item in rm_items_list))
else:
frappe.throw(_("No Items selected for transfer"))
if subcontract_order:
subcontract_order = frappe.get_doc(order_doctype, subcontract_order)
if fg_items:
items = tuple(set(item["rm_item_code"] for item in rm_items_list))
item_wh = get_item_details(items)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.supplier = subcontract_order.supplier
stock_entry.supplier_name = subcontract_order.supplier_name
stock_entry.supplier_address = subcontract_order.supplier_address
stock_entry.address_display = subcontract_order.address_display
stock_entry.company = subcontract_order.company
stock_entry.to_warehouse = subcontract_order.supplier_warehouse
stock_entry.set_stock_entry_type()
if order_doctype == "Purchase Order":
rm_detail_field = "po_detail"
else:
rm_detail_field = "sco_rm_detail"
for item_code in fg_items:
for rm_item_data in rm_items_list:
if rm_item_data["item_code"] == item_code:
rm_item_code = rm_item_data["rm_item_code"]
items_dict = {
rm_item_code: {
rm_detail_field: rm_item_data.get("name"),
"item_name": rm_item_data["item_name"],
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item_data["qty"],
"from_warehouse": rm_item_data["warehouse"],
"stock_uom": rm_item_data["stock_uom"],
"serial_no": rm_item_data.get("serial_no"),
"batch_no": rm_item_data.get("batch_no"),
"main_item_code": rm_item_data["item_code"],
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
return subcontract_order.name
def add_items_in_ste(
ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_detail", batch_no=None
):
item = ste_doc.append("items", row.item_details)
rm_detail = list(set(row.get(f"{rm_detail_field}s")).intersection(rm_details))
item.update(
{
"qty": qty,
"batch_no": batch_no,
"basic_rate": row.item_details["rate"],
rm_detail_field: rm_detail[0] if rm_detail else "",
"s_warehouse": row.item_details["t_warehouse"],
"t_warehouse": row.item_details["s_warehouse"],
"item_code": row.item_details["rm_item_code"],
"subcontracted_item": row.item_details["main_item_code"],
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
}
)
def make_return_stock_entry_for_subcontract(
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
):
ste_doc = frappe.new_doc("Stock Entry")
ste_doc.purpose = "Material Transfer"
if order_doctype == "Purchase Order":
ste_doc.purchase_order = order_doc.name
rm_detail_field = "po_detail"
else:
ste_doc.subcontracting_order = order_doc.name
rm_detail_field = "sco_rm_detail"
ste_doc.company = order_doc.company
ste_doc.is_return = 1
for key, value in available_materials.items():
if not value.qty:
continue
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
return ste_doc
@frappe.whitelist()
def get_materials_from_supplier(
subcontract_order, rm_details, order_doctype="Subcontracting Order"
):
if isinstance(rm_details, str):
rm_details = json.loads(rm_details)
doc = frappe.get_cached_doc(order_doctype, subcontract_order)
doc.initialized_fields()
doc.subcontract_orders = [doc.name]
doc.get_available_materials()
if not doc.available_materials:
frappe.throw(
_("Materials are already received against the {0} {1}").format(order_doctype, subcontract_order)
)
return make_return_stock_entry_for_subcontract(
doc.available_materials, doc, rm_details, order_doctype
)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -36,7 +36,7 @@ def get_data(filters):
"total_time_in_mins",
]
for field in ["work_order", "workstation", "operation", "company"]:
for field in ["work_order", "workstation", "operation", "status", "company"]:
if filters.get(field):
query_filters[field] = ("in", filters.get(field))

View File

@ -21,3 +21,4 @@ Payroll
Telephony
Bulk Transaction
E-commerce
Subcontracting

View File

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

View File

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

View File

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

View File

@ -0,0 +1,12 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
def execute():
for doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
tab = frappe.qb.DocType(doctype).as_("tab")
frappe.qb.update(tab).set(tab.is_old_subcontracting_flow, 1).where(
tab.is_subcontracted == 1
).run()

View File

@ -83,9 +83,17 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
this.frm.set_query("item_code", "items", function() {
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 {

View File

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

View File

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

View File

@ -1,3 +0,0 @@
{% include "erpnext/regional/india/taxes.js" %}
erpnext.setup_auto_gst_taxation('Quotation');

View File

@ -10,8 +10,9 @@ from frappe import _
from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import cint, formatdate, get_timestamp, today
from frappe.utils.nestedset import NestedSet
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
@ -150,9 +151,7 @@ class Company(NestedSet):
self.create_default_tax_template()
if not frappe.db.get_value("Department", {"company": self.name}):
from erpnext.setup.setup_wizard.operations.install_fixtures import install_post_company_fixtures
install_post_company_fixtures(frappe._dict({"company_name": self.name}))
self.create_default_departments()
if not frappe.local.flags.ignore_chart_of_accounts:
self.set_default_accounts()
@ -224,6 +223,104 @@ class Company(NestedSet):
),
)
def create_default_departments(self):
records = [
# Department
{
"doctype": "Department",
"department_name": _("All Departments"),
"is_group": 1,
"parent_department": "",
"__condition": lambda: not frappe.db.exists("Department", _("All Departments")),
},
{
"doctype": "Department",
"department_name": _("Accounts"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Marketing"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Sales"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Purchase"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Operations"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Production"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Dispatch"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Customer Service"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Human Resources"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Management"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Quality Management"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Research & Development"),
"parent_department": _("All Departments"),
"company": self.name,
},
{
"doctype": "Department",
"department_name": _("Legal"),
"parent_department": _("All Departments"),
"company": self.name,
},
]
# Make root department with NSM updation
make_records(records[:1])
frappe.local.flags.ignore_update_nsm = True
make_records(records)
frappe.local.flags.ignore_update_nsm = False
rebuild_tree("Department", "parent_department")
def validate_coa_input(self):
if self.create_chart_of_accounts_based_on == "Existing Company":
self.chart_of_accounts = None

View File

@ -12,7 +12,6 @@ from frappe.desk.doctype.global_search_settings.global_search_settings import (
)
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import cstr, getdate
from frappe.utils.nestedset import rebuild_tree
from erpnext.accounts.doctype.account.account import RootNotEditable
from erpnext.regional.address_template.setup import set_up_address_templates
@ -656,104 +655,6 @@ def install_company(args):
make_records(records)
def install_post_company_fixtures(args=None):
records = [
# Department
{
"doctype": "Department",
"department_name": _("All Departments"),
"is_group": 1,
"parent_department": "",
},
{
"doctype": "Department",
"department_name": _("Accounts"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Marketing"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Sales"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Purchase"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Operations"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Production"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Dispatch"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Customer Service"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Human Resources"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Management"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Quality Management"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Research & Development"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
{
"doctype": "Department",
"department_name": _("Legal"),
"parent_department": _("All Departments"),
"company": args.company_name,
},
]
# Make root department with NSM updation
make_records(records[:1])
frappe.local.flags.ignore_update_nsm = True
make_records(records[1:])
frappe.local.flags.ignore_update_nsm = False
rebuild_tree("Department", "parent_department")
def install_defaults(args=None):
records = [
# Price Lists

View File

@ -40,25 +40,37 @@ class Bin(Document):
self.db_set("reserved_qty_for_production", flt(self.reserved_qty_for_production))
self.db_set("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:

View File

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

View File

@ -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', {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
"add_to_transit",
"work_order",
"purchase_order",
"subcontracting_order",
"delivery_note_no",
"sales_invoice_no",
"pick_list",
@ -153,6 +154,13 @@
"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",

View File

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

View File

@ -5,7 +5,7 @@
import frappe
from frappe.permissions import add_user_permission, remove_user_permission
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate, nowtime
from frappe.utils import add_days, flt, nowdate, nowtime
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import (
@ -1457,6 +1457,138 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.items[0].item_name, item.item_name)
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
def test_reposting_for_depedent_warehouse(self):
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
# Inward at WH1 warehouse (Component)
# 1st Repack (Component (WH1) - Subcomponent (WH2))
# 2nd Repack (Subcomponent (WH2) - FG Item (WH3))
# Material Transfer of FG Item -> WH 3 -> WH2 -> Wh1 (Two transfer entries)
# Backdated transction which should update valuation rate in repack as well trasfer entries
for item_code in ["FG Item 1", "Sub Component 1", "Component 1"]:
create_item(item_code)
for warehouse in ["WH 1", "WH 2", "WH 3"]:
create_warehouse(warehouse)
make_stock_entry(
item_code="Component 1",
rate=100,
purpose="Material Receipt",
qty=10,
to_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -10),
)
repack1 = make_stock_entry(
item_code="Component 1",
purpose="Repack",
do_not_save=True,
qty=10,
from_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -9),
)
repack1.append(
"items",
{
"item_code": "Sub Component 1",
"qty": 10,
"t_warehouse": "WH 2 - _TC",
"transfer_qty": 10,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1.0,
},
)
repack1.save()
repack1.submit()
self.assertEqual(repack1.items[1].basic_rate, 100)
self.assertEqual(repack1.items[1].amount, 1000)
repack2 = make_stock_entry(
item_code="Sub Component 1",
purpose="Repack",
do_not_save=True,
qty=10,
from_warehouse="WH 2 - _TC",
posting_date=add_days(nowdate(), -8),
)
repack2.append(
"items",
{
"item_code": "FG Item 1",
"qty": 10,
"t_warehouse": "WH 3 - _TC",
"transfer_qty": 10,
"uom": "Nos",
"stock_uom": "Nos",
"conversion_factor": 1.0,
},
)
repack2.save()
repack2.submit()
self.assertEqual(repack2.items[1].basic_rate, 100)
self.assertEqual(repack2.items[1].amount, 1000)
transfer1 = make_stock_entry(
item_code="FG Item 1",
purpose="Material Transfer",
qty=10,
from_warehouse="WH 3 - _TC",
to_warehouse="WH 2 - _TC",
posting_date=add_days(nowdate(), -7),
)
self.assertEqual(transfer1.items[0].basic_rate, 100)
self.assertEqual(transfer1.items[0].amount, 1000)
transfer2 = make_stock_entry(
item_code="FG Item 1",
purpose="Material Transfer",
qty=10,
from_warehouse="WH 2 - _TC",
to_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -6),
)
self.assertEqual(transfer2.items[0].basic_rate, 100)
self.assertEqual(transfer2.items[0].amount, 1000)
# Backdated transaction
receipt2 = make_stock_entry(
item_code="Component 1",
rate=200,
purpose="Material Receipt",
qty=10,
to_warehouse="WH 1 - _TC",
posting_date=add_days(nowdate(), -15),
)
self.assertEqual(receipt2.items[0].basic_rate, 200)
self.assertEqual(receipt2.items[0].amount, 2000)
repost_name = frappe.db.get_value(
"Repost Item Valuation", {"voucher_no": receipt2.name, "docstatus": 1}, "name"
)
doc = frappe.get_doc("Repost Item Valuation", repost_name)
repost_sl_entries(doc)
for obj in [repack1, repack2, transfer1, transfer2]:
obj.load_from_db()
index = 1 if obj.purpose == "Repack" else 0
self.assertEqual(obj.items[index].basic_rate, 200)
self.assertEqual(obj.items[index].basic_amount, 2000)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

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

View File

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

View File

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

View File

@ -250,12 +250,7 @@ def repost_future_sle(
data.sle_changed = False
i += 1
if doc and i % 2 == 0:
update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
if doc and args:
if doc:
update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
@ -501,7 +496,8 @@ class update_entries_after(object):
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
else:
return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
return entries_to_fix
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
@ -520,14 +516,11 @@ class update_entries_after(object):
def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle or frappe._dict(
{"item_code": self.item_code, "warehouse": dependant_sle.warehouse}
self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict(
{"sle": dependant_sle}
)
future_sle_for_dependant = list(self.get_sle_after_datetime(args))
entries_to_fix.extend(future_sle_for_dependant)
return sorted(entries_to_fix, key=lambda k: k["timestamp"])
self.new_items_found = True
def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@ -637,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 (
@ -653,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"
@ -666,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"
@ -691,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)
@ -739,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)

View File

View File

@ -0,0 +1,328 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide('erpnext.buying');
frappe.ui.form.on('Subcontracting Order', {
setup: (frm) => {
frm.get_field("items").grid.cannot_add_rows = true;
frm.get_field("items").grid.only_sortable();
frm.set_indicator_formatter('item_code',
(doc) => (doc.qty <= doc.received_qty) ? 'green' : 'orange');
frm.set_query('supplier_warehouse', () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query('purchase_order', () => {
return {
filters: {
docstatus: 1,
is_subcontracted: 1,
is_old_subcontracting_flow: 0
}
};
});
frm.set_query('set_warehouse', () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query('warehouse', 'items', () => ({
filters: {
company: frm.doc.company,
is_group: 0
}
}));
frm.set_query('expense_account', 'items', () => ({
query: 'erpnext.controllers.queries.get_expense_account',
filters: {
company: frm.doc.company
}
}));
frm.set_query('bom', 'items', (doc, cdt, cdn) => {
let d = locals[cdt][cdn];
return {
filters: {
item: d.item_code,
is_active: 1,
docstatus: 1,
company: frm.doc.company
}
};
});
frm.set_query('set_reserve_warehouse', () => {
return {
filters: {
company: frm.doc.company,
name: ['!=', frm.doc.supplier_warehouse],
is_group: 0
}
};
});
},
onload: (frm) => {
if (!frm.doc.transaction_date) {
frm.set_value('transaction_date', frappe.datetime.get_today());
}
},
purchase_order: (frm) => {
frm.set_value('service_items', null);
frm.set_value('items', null);
frm.set_value('supplied_items', null);
if (frm.doc.purchase_order) {
erpnext.utils.map_current_doc({
method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order',
source_name: frm.doc.purchase_order,
target_doc: frm,
freeze: true,
freeze_message: __('Mapping Subcontracting Order ...'),
});
}
},
refresh: function (frm) {
frm.trigger('get_materials_from_supplier');
},
get_materials_from_supplier: function (frm) {
let sco_rm_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100)) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
sco_rm_details.push(d.name);
}
});
}
if (sco_rm_details && sco_rm_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.controllers.subcontracting_controller.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: {
subcontract_order: frm.doc.name,
rm_details: sco_rm_details,
order_doctype: cur_frm.doc.doctype
},
callback: function (r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
}
});
erpnext.buying.SubcontractingOrderController = class SubcontractingOrderController {
setup() {
this.frm.custom_make_buttons = {
'Subcontracting Receipt': 'Subcontracting Receipt',
'Stock Entry': 'Material to Supplier',
};
}
refresh(doc) {
var me = this;
if (doc.docstatus == 1) {
if (doc.status != 'Completed') {
if (flt(doc.per_received) < 100) {
cur_frm.add_custom_button(__('Subcontracting Receipt'), this.make_subcontracting_receipt, __('Create'));
if (me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'),
() => {
me.make_stock_entry();
}, __('Transfer'));
}
}
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}
}
}
items_add(doc, cdt, cdn) {
if (doc.set_warehouse) {
var row = frappe.get_doc(cdt, cdn);
row.warehouse = doc.set_warehouse;
}
}
set_warehouse(doc) {
this.set_warehouse_in_children(doc.items, "warehouse", doc.set_warehouse);
}
set_reserve_warehouse(doc) {
this.set_warehouse_in_children(doc.supplied_items, "reserve_warehouse", doc.set_reserve_warehouse);
}
set_warehouse_in_children(child_table, warehouse_field, warehouse) {
let transaction_controller = new erpnext.TransactionController();
transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
}
make_stock_entry() {
var items = $.map(cur_frm.doc.items, (d) => d.bom ? d.item_code : false);
var me = this;
if (items.length >= 1) {
me.raw_material_data = [];
me.show_dialog = 1;
let title = __('Transfer Material to Supplier');
let fields = [
{ fieldtype: 'Section Break', label: __('Raw Materials') },
{
fieldname: 'sub_con_rm_items', fieldtype: 'Table', label: __('Items'),
fields: [
{
fieldtype: 'Data',
fieldname: 'item_code',
label: __('Item'),
read_only: 1,
in_list_view: 1
},
{
fieldtype: 'Data',
fieldname: 'rm_item_code',
label: __('Raw Material'),
read_only: 1,
in_list_view: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'qty',
label: __('Quantity'),
in_list_view: 1
},
{
fieldtype: 'Data',
read_only: 1,
fieldname: 'warehouse',
label: __('Reserve Warehouse'),
in_list_view: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'rate',
label: __('Rate'),
hidden: 1
},
{
fieldtype: 'Float',
read_only: 1,
fieldname: 'amount',
label: __('Amount'),
hidden: 1
},
{
fieldtype: 'Link',
read_only: 1,
fieldname: 'uom',
label: __('UOM'),
hidden: 1
}
],
data: me.raw_material_data,
get_data: () => me.raw_material_data
}
];
me.dialog = new frappe.ui.Dialog({
title: title, fields: fields
});
if (me.frm.doc['supplied_items']) {
me.frm.doc['supplied_items'].forEach((item) => {
if (item.rm_item_code && item.main_item_code && item.required_qty - item.supplied_qty != 0) {
me.raw_material_data.push({
'name': item.name,
'item_code': item.main_item_code,
'rm_item_code': item.rm_item_code,
'item_name': item.rm_item_code,
'qty': item.required_qty - item.supplied_qty,
'warehouse': item.reserve_warehouse,
'rate': item.rate,
'amount': item.amount,
'stock_uom': item.stock_uom
});
me.dialog.fields_dict.sub_con_rm_items.grid.refresh();
}
});
}
me.dialog.get_field('sub_con_rm_items').check_all_rows();
me.dialog.show();
this.dialog.set_primary_action(__('Transfer'), () => {
me.values = me.dialog.get_values();
if (me.values) {
me.values.sub_con_rm_items.map((row, i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
let row_id = i + 1;
frappe.throw(__('Item Code, warehouse and quantity are required on row {0}', [row_id]));
}
});
me.make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children());
me.dialog.hide();
}
});
}
me.dialog.get_close_btn().on('click', () => {
me.dialog.hide();
});
}
has_unsupplied_items() {
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
}
make_subcontracting_receipt() {
frappe.model.open_mapped_doc({
method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
frm: cur_frm,
freeze_message: __('Creating Subcontracting Receipt ...')
});
}
make_rm_stock_entry(rm_items) {
frappe.call({
method: 'erpnext.controllers.subcontracting_controller.make_rm_stock_entry',
args: {
subcontract_order: cur_frm.doc.name,
rm_items: rm_items,
order_doctype: cur_frm.doc.doctype
},
callback: (r) => {
var doclist = frappe.model.sync(r.message);
frappe.set_route('Form', doclist[0].doctype, doclist[0].name);
}
});
}
};
extend_cscript(cur_frm.cscript, new erpnext.buying.SubcontractingOrderController({ frm: cur_frm }));

View File

@ -0,0 +1,485 @@
{
"actions": [],
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2022-04-01 22:39:17.662819",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"title",
"naming_series",
"purchase_order",
"supplier",
"supplier_name",
"supplier_warehouse",
"column_break_7",
"company",
"transaction_date",
"schedule_date",
"amended_from",
"address_and_contact_section",
"supplier_address",
"address_display",
"contact_person",
"contact_display",
"contact_mobile",
"contact_email",
"column_break_19",
"shipping_address",
"shipping_address_display",
"billing_address",
"billing_address_display",
"section_break_24",
"column_break_25",
"set_warehouse",
"items",
"section_break_32",
"total_qty",
"column_break_29",
"total",
"service_items_section",
"service_items",
"raw_materials_supplied_section",
"set_reserve_warehouse",
"supplied_items",
"additional_costs_section",
"distribute_additional_costs_based_on",
"additional_costs",
"total_additional_costs",
"order_status_section",
"status",
"column_break_39",
"per_received",
"printing_settings_section",
"select_print_heading",
"column_break_43",
"letter_head"
],
"fields": [
{
"allow_on_submit": 1,
"default": "{supplier_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "SC-ORD-.YYYY.-",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"label": "Subcontracting Purchase Order",
"options": "Purchase Order",
"reqd": 1
},
{
"bold": 1,
"fieldname": "supplier",
"fieldtype": "Link",
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Supplier",
"options": "Supplier",
"print_hide": 1,
"reqd": 1,
"search_index": 1
},
{
"bold": 1,
"fetch_from": "supplier.supplier_name",
"fieldname": "supplier_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Supplier Name",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "supplier",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
"options": "Warehouse",
"reqd": 1
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
"reqd": 1
},
{
"default": "Today",
"fetch_from": "purchase_order.transaction_date",
"fetch_if_empty": 1,
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1,
"search_index": 1
},
{
"allow_on_submit": 1,
"fetch_from": "purchase_order.schedule_date",
"fetch_if_empty": 1,
"fieldname": "schedule_date",
"fieldtype": "Date",
"label": "Required By",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"options": "Subcontracting Order",
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "address_and_contact_section",
"fieldtype": "Section Break",
"label": "Address and Contact"
},
{
"fetch_from": "supplier.supplier_primary_address",
"fetch_if_empty": 1,
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Supplier Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "address_display",
"fieldtype": "Small Text",
"label": "Supplier Address Details",
"read_only": 1
},
{
"fetch_from": "supplier.supplier_primary_contact",
"fetch_if_empty": 1,
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Supplier Contact",
"options": "Contact",
"print_hide": 1
},
{
"fieldname": "contact_display",
"fieldtype": "Small Text",
"in_global_search": 1,
"label": "Contact Name",
"read_only": 1
},
{
"fieldname": "contact_mobile",
"fieldtype": "Small Text",
"label": "Contact Mobile No",
"read_only": 1
},
{
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
"options": "Email",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Company Shipping Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address Details",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Company Billing Address",
"options": "Address"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address Details",
"read_only": 1
},
{
"fieldname": "section_break_24",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"depends_on": "purchase_order",
"description": "Sets 'Warehouse' in each row of the Items table.",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse",
"print_hide": 1
},
{
"allow_bulk_edit": 1,
"depends_on": "purchase_order",
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Subcontracting Order Item",
"reqd": 1
},
{
"fieldname": "section_break_32",
"fieldtype": "Section Break"
},
{
"depends_on": "purchase_order",
"fieldname": "total_qty",
"fieldtype": "Float",
"label": "Total Quantity",
"read_only": 1
},
{
"fieldname": "column_break_29",
"fieldtype": "Column Break"
},
{
"depends_on": "purchase_order",
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
"read_only": 1
},
{
"collapsible": 1,
"depends_on": "purchase_order",
"fieldname": "service_items_section",
"fieldtype": "Section Break",
"label": "Service Items"
},
{
"fieldname": "service_items",
"fieldtype": "Table",
"label": "Service Items",
"options": "Subcontracting Order Service Item",
"read_only": 1,
"reqd": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "supplied_items",
"depends_on": "supplied_items",
"fieldname": "raw_materials_supplied_section",
"fieldtype": "Section Break",
"label": "Raw Materials Supplied"
},
{
"depends_on": "supplied_items",
"description": "Sets 'Reserve Warehouse' in each row of the Supplied Items table.",
"fieldname": "set_reserve_warehouse",
"fieldtype": "Link",
"label": "Set Reserve Warehouse",
"options": "Warehouse"
},
{
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
"no_copy": 1,
"options": "Subcontracting Order Supplied Item",
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "total_additional_costs",
"depends_on": "eval:(doc.docstatus == 0 || doc.total_additional_costs)",
"fieldname": "additional_costs_section",
"fieldtype": "Section Break",
"label": "Additional Costs"
},
{
"fieldname": "additional_costs",
"fieldtype": "Table",
"label": "Additional Costs",
"options": "Landed Cost Taxes and Charges"
},
{
"fieldname": "total_additional_costs",
"fieldtype": "Currency",
"label": "Total Additional Costs",
"print_hide_if_no_value": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "order_status_section",
"fieldtype": "Section Break",
"label": "Order Status"
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
"fieldname": "column_break_39",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "per_received",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Received",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "printing_settings_section",
"fieldtype": "Section Break",
"label": "Printing Settings",
"print_hide": 1,
"print_width": "50%",
"width": "50%"
},
{
"allow_on_submit": 1,
"fieldname": "select_print_heading",
"fieldtype": "Link",
"label": "Print Heading",
"no_copy": 1,
"options": "Print Heading",
"print_hide": 1,
"report_hide": 1
},
{
"fieldname": "column_break_43",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head",
"print_hide": 1
},
{
"default": "Qty",
"fieldname": "distribute_additional_costs_based_on",
"fieldtype": "Select",
"label": "Distribute Additional Costs Based On ",
"options": "Qty\nAmount"
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2022-04-11 21:02:44.097841",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"read": 1,
"report": 1,
"role": "Stock User"
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"submit": 1,
"write": 1
},
{
"permlevel": 1,
"read": 1,
"role": "Purchase Manager",
"write": 1
}
],
"search_fields": "status, transaction_date, supplier",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "supplier_name",
"track_changes": 1
}

View File

@ -0,0 +1,246 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
from erpnext.stock.utils import get_bin
class SubcontractingOrder(SubcontractingController):
def before_validate(self):
super(SubcontractingOrder, self).before_validate()
def validate(self):
super(SubcontractingOrder, self).validate()
self.validate_purchase_order_for_subcontracting()
self.validate_items()
self.validate_service_items()
self.validate_supplied_items()
self.set_missing_values()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def on_submit(self):
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status()
def on_cancel(self):
self.update_ordered_qty_for_subcontracting()
self.update_reserved_qty_for_subcontracting()
self.update_status()
def validate_purchase_order_for_subcontracting(self):
if self.purchase_order:
if is_subcontracting_order_created(self.purchase_order):
frappe.throw(
_(
"Only one Subcontracting Order can be created against a Purchase Order, cancel the existing Subcontracting Order to create a new one."
)
)
po = frappe.get_doc("Purchase Order", self.purchase_order)
if not po.is_subcontracted:
frappe.throw(_("Please select a valid Purchase Order that is configured for Subcontracting."))
if po.is_old_subcontracting_flow:
frappe.throw(_("Please select a valid Purchase Order that has Service Items."))
if po.docstatus != 1:
msg = f"Please submit Purchase Order {po.name} before proceeding."
frappe.throw(_(msg))
if po.per_received == 100:
msg = f"Cannot create more Subcontracting Orders against the Purchase Order {po.name}."
frappe.throw(_(msg))
else:
self.service_items = self.items = self.supplied_items = None
frappe.throw(_("Please select a Subcontracting Purchase Order."))
def validate_service_items(self):
for item in self.service_items:
if frappe.get_value("Item", item.item_code, "is_stock_item"):
msg = f"Service Item {item.item_name} must be a non-stock item."
frappe.throw(_(msg))
def validate_supplied_items(self):
if self.supplier_warehouse:
for item in self.supplied_items:
if self.supplier_warehouse == item.reserve_warehouse:
msg = f"Reserve Warehouse must be different from Supplier Warehouse for Supplied Item {item.main_item_code}."
frappe.throw(_(msg))
def set_missing_values(self):
self.set_missing_values_in_additional_costs()
self.set_missing_values_in_service_items()
self.set_missing_values_in_supplied_items()
self.set_missing_values_in_items()
def set_missing_values_in_additional_costs(self):
if self.get("additional_costs"):
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
if self.total_additional_costs:
if self.distribute_additional_costs_based_on == "Amount":
total_amt = sum(flt(item.amount) for item in self.get("items"))
for item in self.items:
item.additional_cost_per_qty = (
(item.amount * self.total_additional_costs) / total_amt
) / item.qty
else:
total_qty = sum(flt(item.qty) for item in self.get("items"))
additional_cost_per_qty = self.total_additional_costs / total_qty
for item in self.items:
item.additional_cost_per_qty = additional_cost_per_qty
else:
self.total_additional_costs = 0
def set_missing_values_in_service_items(self):
for idx, item in enumerate(self.get("service_items")):
self.items[idx].service_cost_per_qty = item.amount / self.items[idx].qty
def set_missing_values_in_supplied_items(self):
for item in self.get("items"):
bom = frappe.get_doc("BOM", item.bom)
rm_cost = sum(flt(rm_item.amount) for rm_item in bom.items)
item.rm_cost_per_qty = rm_cost / flt(bom.quantity)
def set_missing_values_in_items(self):
total_qty = total = 0
for item in self.items:
item.rate = (
item.rm_cost_per_qty + item.service_cost_per_qty + (item.additional_cost_per_qty or 0)
)
item.amount = item.qty * item.rate
total_qty += flt(item.qty)
total += flt(item.amount)
else:
self.total_qty = total_qty
self.total = total
def update_ordered_qty_for_subcontracting(self, sco_item_rows=None):
item_wh_list = []
for item in self.get("items"):
if (
(not sco_item_rows or item.name in sco_item_rows)
and [item.item_code, item.warehouse] not in item_wh_list
and frappe.get_cached_value("Item", item.item_code, "is_stock_item")
and item.warehouse
):
item_wh_list.append([item.item_code, item.warehouse])
for item_code, warehouse in item_wh_list:
update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
def update_reserved_qty_for_subcontracting(self):
for item in self.supplied_items:
if item.rm_item_code:
stock_bin = get_bin(item.rm_item_code, item.reserve_warehouse)
stock_bin.update_reserved_qty_for_sub_contracting()
def populate_items_table(self):
items = []
for si in self.service_items:
if si.fg_item:
item = frappe.get_doc("Item", si.fg_item)
bom = frappe.db.get_value("BOM", {"item": item.item_code, "is_active": 1, "is_default": 1})
items.append(
{
"item_code": item.item_code,
"item_name": item.item_name,
"schedule_date": self.schedule_date,
"description": item.description,
"qty": si.fg_item_qty,
"stock_uom": item.stock_uom,
"bom": bom,
},
)
else:
frappe.throw(
_("Please select Finished Good Item for Service Item {0}").format(
si.item_name or si.item_code
)
)
else:
for item in items:
self.append("items", item)
else:
self.set_missing_values()
def update_status(self, status=None, update_modified=False):
if self.docstatus >= 1 and not status:
if self.docstatus == 1:
if self.status == "Draft":
status = "Open"
elif self.per_received >= 100:
status = "Completed"
elif self.per_received > 0 and self.per_received < 100:
status = "Partially Received"
else:
total_required_qty = total_supplied_qty = 0
for item in self.supplied_items:
total_required_qty += item.required_qty
total_supplied_qty += item.supplied_qty or 0
if total_supplied_qty:
status = "Partial Material Transferred"
if total_supplied_qty >= total_required_qty:
status = "Material Transferred"
else:
status = "Open"
elif self.docstatus == 2:
status = "Cancelled"
frappe.db.set_value("Subcontracting Order", self.name, "status", status, update_modified)
@frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None):
return get_mapped_subcontracting_receipt(source_name, target_doc)
def get_mapped_subcontracting_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target_doc = get_mapped_doc(
"Subcontracting Order",
source_name,
{
"Subcontracting Order": {
"doctype": "Subcontracting Receipt",
"field_map": {"supplier_warehouse": "supplier_warehouse"},
"validation": {
"docstatus": ["=", 1],
},
},
"Subcontracting Order Item": {
"doctype": "Subcontracting Receipt Item",
"field_map": {
"name": "subcontracting_order_item",
"parent": "subcontracting_order",
"bom": "bom",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
},
},
target_doc,
)
return target_doc
@frappe.whitelist()
def update_subcontracting_order_status(sco):
if isinstance(sco, str):
sco = frappe.get_doc("Subcontracting Order", sco)
sco.update_status()

View File

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

View File

@ -0,0 +1,16 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.listview_settings['Subcontracting Order'] = {
get_indicator: function (doc) {
const status_colors = {
"Draft": "grey",
"Open": "orange",
"Partially Received": "yellow",
"Completed": "green",
"Partial Material Transferred": "purple",
"Material Transferred": "blue",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
};

View File

@ -0,0 +1,536 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import copy
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
from erpnext.controllers.tests.test_subcontracting_controller import (
get_rm_items,
get_subcontracting_order,
make_bom_for_subcontracted_items,
make_raw_materials,
make_service_items,
make_stock_in_entry,
make_stock_transfer_entry,
make_subcontracted_item,
make_subcontracted_items,
set_backflush_based_on,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt,
)
class TestSubcontractingOrder(FrappeTestCase):
def setUp(self):
make_subcontracted_items()
make_raw_materials()
make_service_items()
make_bom_for_subcontracted_items()
def test_populate_items_table(self):
sco = get_subcontracting_order()
sco.items = None
sco.populate_items_table()
self.assertEqual(len(sco.service_items), len(sco.items))
def test_set_missing_values(self):
sco = get_subcontracting_order()
before = {sco.total_qty, sco.total, sco.total_additional_costs}
sco.total_qty = sco.total = sco.total_additional_costs = 0
sco.set_missing_values()
after = {sco.total_qty, sco.total, sco.total_additional_costs}
self.assertSetEqual(before, after)
def test_update_status(self):
# Draft
sco = get_subcontracting_order(do_not_submit=1)
self.assertEqual(sco.status, "Draft")
# Open
sco.submit()
sco.load_from_db()
self.assertEqual(sco.status, "Open")
# Partial Material Transferred
rm_items = get_rm_items(sco.supplied_items)
rm_items[0]["qty"] -= 1
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
sco.load_from_db()
self.assertEqual(sco.status, "Partial Material Transferred")
# Material Transferred
rm_items[0]["qty"] = 1
itemwise_details = make_stock_in_entry(rm_items=rm_items)
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
sco.load_from_db()
self.assertEqual(sco.status, "Material Transferred")
# Partially Received
scr = make_subcontracting_receipt(sco.name)
scr.items[0].qty -= 1
scr.save()
scr.submit()
sco.load_from_db()
self.assertEqual(sco.status, "Partially Received")
# Completed
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
sco.load_from_db()
self.assertEqual(sco.status, "Completed")
# Partially Received (scr cancelled)
scr.load_from_db()
scr.cancel()
sco.load_from_db()
self.assertEqual(sco.status, "Partially Received")
def test_make_rm_stock_entry(self):
sco = get_subcontracting_order()
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
ste = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
self.assertEqual(len(ste.items), len(rm_items))
def test_make_rm_stock_entry_for_serial_items(self):
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 2",
"qty": 5,
"rate": 100,
"fg_item": "Subcontracted Item SA2",
"fg_item_qty": 5,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 5",
"qty": 6,
"rate": 100,
"fg_item": "Subcontracted Item SA5",
"fg_item_qty": 6,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
ste = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
self.assertEqual(len(ste.items), len(rm_items))
def test_make_rm_stock_entry_for_batch_items(self):
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 4",
"qty": 5,
"rate": 100,
"fg_item": "Subcontracted Item SA4",
"fg_item_qty": 5,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 6",
"qty": 6,
"rate": 100,
"fg_item": "Subcontracted Item SA6",
"fg_item_qty": 6,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
ste = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
self.assertEqual(len(ste.items), len(rm_items))
def test_update_reserved_qty_for_subcontracting(self):
# Make stock available for raw materials
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code="_Test Item Home Desktop 100",
qty=30,
basic_rate=100,
)
bin1 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
as_dict=1,
)
# Create SCO
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 100,
"fg_item": "_Test FG Item",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(service_items=service_items)
bin2 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
as_dict=1,
)
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
self.assertNotEqual(bin1.modified, bin2.modified)
# Create stock transfer
rm_items = [
{
"item_code": "_Test FG Item",
"rm_item_code": "_Test Item",
"item_name": "_Test Item",
"qty": 6,
"warehouse": "_Test Warehouse - _TC",
"rate": 100,
"amount": 600,
"stock_uom": "Nos",
}
]
ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
ste.to_warehouse = "_Test Warehouse 1 - _TC"
ste.save()
ste.submit()
bin3 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
make_stock_entry(
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code="_Test Item Home Desktop 100",
qty=40,
basic_rate=100,
)
# Make SCR against the SCO
scr = make_subcontracting_receipt(sco.name)
scr.save()
scr.submit()
bin4 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
# Cancel SCR
scr.reload()
scr.cancel()
bin5 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
# Cancel Stock Entry
ste.cancel()
bin6 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
# Cancel PO
sco.reload()
sco.cancel()
bin7 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin7.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items(self):
item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 1,
"rate": 100,
"fg_item": item_code,
"fg_item_qty": 1,
},
]
sco1 = get_subcontracting_order(service_items=service_items, include_exploded_items=1)
item_name = frappe.db.get_value("BOM", {"item": item_code}, "name")
bom = frappe.get_doc("BOM", item_name)
exploded_items = sorted([item.item_code for item in bom.exploded_items])
supplied_items = sorted([item.rm_item_code for item in sco1.supplied_items])
self.assertEqual(exploded_items, supplied_items)
sco2 = get_subcontracting_order(service_items=service_items, include_exploded_items=0)
supplied_items1 = sorted([item.rm_item_code for item in sco2.supplied_items])
bom_items = sorted([item.item_code for item in bom.items])
self.assertEqual(supplied_items1, bom_items)
def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code=item_code)
make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1})
set_backflush_based_on("Material Transferred for Subcontract")
order_qty = 5
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": order_qty,
"rate": 100,
"fg_item": item_code,
"fg_item_qty": order_qty,
},
]
sco = get_subcontracting_order(service_items=service_items)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse - _TC",
item_code="Sub Contracted Raw Material 1",
qty=10,
basic_rate=100,
)
rm_items = [
{
"item_code": item_code,
"rm_item_code": "Sub Contracted Raw Material 1",
"item_name": "_Test Item",
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
{
"item_code": item_code,
"rm_item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
"qty": 20,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
{
"item_code": item_code,
"rm_item_code": "Test Extra Item 1",
"item_name": "Test Extra Item 1",
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
{
"item_code": item_code,
"rm_item_code": "Test Extra Item 2",
"stock_uom": "Nos",
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"item_name": "Test Extra Item 2",
},
]
ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
ste.submit()
scr = make_subcontracting_receipt(sco.name)
received_qty = 2
# partial receipt
scr.get("items")[0].qty = received_qty
scr.save()
scr.submit()
transferred_items = sorted(
[item.item_code for item in ste.get("items") if ste.subcontracting_order == sco.name]
)
issued_items = sorted([item.rm_item_code for item in scr.get("supplied_items")])
self.assertEqual(transferred_items, issued_items)
self.assertEqual(scr.get_supplied_items_cost(scr.get("items")[0].name), 2000)
transferred_rm_map = frappe._dict()
for item in rm_items:
transferred_rm_map[item.get("rm_item_code")] = item
set_backflush_based_on("BOM")
def test_supplied_qty(self):
item_code = "_Test Subcontracted FG Item 5"
make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1})
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
set_backflush_based_on("Material Transferred for Subcontract")
order_qty = 250
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": order_qty,
"rate": 100,
"fg_item": item_code,
"fg_item_qty": order_qty,
},
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": order_qty,
"rate": 100,
"fg_item": item_code,
"fg_item_qty": order_qty,
},
]
sco = get_subcontracting_order(service_items=service_items)
# Material receipt entry for the raw materials which will be send to supplier
make_stock_entry(
target="_Test Warehouse - _TC",
item_code="Sub Contracted Raw Material 4",
qty=500,
basic_rate=100,
)
rm_items = [
{
"item_code": item_code,
"rm_item_code": "Sub Contracted Raw Material 4",
"item_name": "_Test Item",
"qty": 250,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
"name": sco.supplied_items[0].name,
},
{
"item_code": item_code,
"rm_item_code": "Sub Contracted Raw Material 4",
"item_name": "_Test Item",
"qty": 250,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
]
# Raw Materials transfer entry from stores to supplier's warehouse
ste = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
ste.submit()
# Test sco_rm_detail field has value or not
for item_row in ste.items:
self.assertEqual(item_row.sco_rm_detail, sco.supplied_items[item_row.idx - 1].name)
sco.load_from_db()
for row in sco.supplied_items:
# Valid that whether transferred quantity is matching with supplied qty or not in the subcontracting order
self.assertEqual(row.supplied_qty, 250.0)
set_backflush_based_on("BOM")
def create_subcontracting_order(**args):
args = frappe._dict(args)
sco = get_mapped_subcontracting_order(source_name=args.po_name)
for item in sco.items:
item.include_exploded_items = args.get("include_exploded_items", 1)
if args.get("warehouse"):
for item in sco.items:
item.warehouse = args.warehouse
else:
warehouse = frappe.get_value("Purchase Order", args.po_name, "set_warehouse")
if warehouse:
for item in sco.items:
item.warehouse = warehouse
else:
po = frappe.get_doc("Purchase Order", args.po_name)
warehouses = []
for item in po.items:
warehouses.append(item.warehouse)
else:
for idx, val in enumerate(sco.items):
val.warehouse = warehouses[idx]
if not args.do_not_save:
sco.insert()
if not args.do_not_submit:
sco.submit()
return sco

View File

@ -0,0 +1,326 @@
{
"actions": [],
"autoname": "hash",
"creation": "2022-04-01 19:26:31.475015",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"bom",
"include_exploded_items",
"column_break_3",
"schedule_date",
"expected_delivery_date",
"description_section",
"description",
"column_break_8",
"image",
"image_view",
"quantity_and_rate_section",
"qty",
"received_qty",
"returned_qty",
"column_break_13",
"stock_uom",
"conversion_factor",
"section_break_16",
"rate",
"amount",
"column_break_19",
"rm_cost_per_qty",
"service_cost_per_qty",
"additional_cost_per_qty",
"warehouse_section",
"warehouse",
"accounting_details_section",
"expense_account",
"manufacture_section",
"manufacturer",
"manufacturer_part_no",
"section_break_34",
"page_break"
],
"fields": [
{
"bold": 1,
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"read_only": 1,
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Item Name",
"print_hide": 1,
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"bold": 1,
"columns": 2,
"fieldname": "schedule_date",
"fieldtype": "Date",
"label": "Required By",
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"bold": 1,
"fieldname": "expected_delivery_date",
"fieldtype": "Date",
"label": "Expected Delivery Date",
"search_index": 1
},
{
"collapsible": 1,
"fieldname": "description_section",
"fieldtype": "Section Break",
"label": "Description"
},
{
"fetch_from": "item_code.description",
"fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description",
"print_width": "300px",
"reqd": 1,
"width": "300px"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
"label": "Image"
},
{
"fieldname": "image_view",
"fieldtype": "Image",
"label": "Image View",
"options": "image",
"print_hide": 1
},
{
"fieldname": "quantity_and_rate_section",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{
"bold": 1,
"columns": 1,
"default": "1",
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"print_width": "60px",
"read_only": 1,
"reqd": 1,
"width": "60px"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break",
"print_hide": 1
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"print_width": "100px",
"read_only": 1,
"reqd": 1,
"width": "100px"
},
{
"default": "1",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"hidden": 1,
"label": "Conversion Factor",
"read_only": 1
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break"
},
{
"bold": 1,
"columns": 2,
"fetch_from": "item_code.standard_rate",
"fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "warehouse_section",
"fieldtype": "Section Break",
"label": "Warehouse Details"
},
{
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse",
"print_hide": 1,
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Account",
"options": "Account",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "manufacture_section",
"fieldtype": "Section Break",
"label": "Manufacture"
},
{
"fieldname": "manufacturer",
"fieldtype": "Link",
"label": "Manufacturer",
"options": "Manufacturer"
},
{
"fieldname": "manufacturer_part_no",
"fieldtype": "Data",
"label": "Manufacturer Part Number"
},
{
"depends_on": "item_code",
"fetch_from": "item_code.default_bom",
"fieldname": "bom",
"fieldtype": "Link",
"in_list_view": 1,
"label": "BOM",
"options": "BOM",
"print_hide": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
"label": "Include Exploded Items",
"print_hide": 1
},
{
"fieldname": "service_cost_per_qty",
"fieldtype": "Currency",
"label": "Service Cost Per Qty",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "additional_cost_per_qty",
"fieldtype": "Currency",
"label": "Additional Cost Per Qty",
"read_only": 1
},
{
"fieldname": "rm_cost_per_qty",
"fieldtype": "Currency",
"label": "Raw Material Cost Per Qty",
"no_copy": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "page_break",
"fieldtype": "Check",
"label": "Page Break",
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "section_break_34",
"fieldtype": "Section Break"
},
{
"depends_on": "received_qty",
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-04-11 21:28:06.585338",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SubcontractingOrderItem(Document):
pass

View File

@ -0,0 +1,131 @@
{
"actions": [],
"autoname": "hash",
"creation": "2022-04-01 19:23:05.728354",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"column_break_2",
"item_name",
"section_break_4",
"qty",
"column_break_6",
"rate",
"column_break_8",
"amount",
"section_break_10",
"fg_item",
"column_break_12",
"fg_item_qty"
],
"fields": [
{
"bold": 1,
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"label": "Item Name",
"print_hide": 1,
"reqd": 1
},
{
"bold": 1,
"columns": 1,
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Quantity",
"print_width": "60px",
"reqd": 1,
"width": "60px"
},
{
"bold": 1,
"columns": 2,
"fetch_from": "item_code.standard_rate",
"fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"reqd": 1
},
{
"columns": 2,
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good Item",
"options": "Item",
"reqd": 1
},
{
"default": "1",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Item Quantity",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
}
],
"istable": 1,
"links": [],
"modified": "2022-04-07 11:43:43.094867",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Service Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SubcontractingOrderServiceItem(Document):
pass

View File

@ -0,0 +1,178 @@
{
"actions": [],
"creation": "2022-04-01 19:29:30.923800",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"main_item_code",
"rm_item_code",
"column_break_3",
"stock_uom",
"conversion_factor",
"reserve_warehouse",
"column_break_6",
"bom_detail_no",
"reference_name",
"section_break_9",
"rate",
"column_break_11",
"amount",
"section_break_13",
"required_qty",
"supplied_qty",
"column_break_16",
"consumed_qty",
"returned_qty",
"total_supplied_qty"
],
"fields": [
{
"columns": 2,
"fieldname": "main_item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"options": "Item",
"read_only": 1
},
{
"columns": 2,
"fieldname": "rm_item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Raw Material Item Code",
"options": "Item",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock Uom",
"options": "UOM",
"read_only": 1
},
{
"default": "1",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"hidden": 1,
"label": "Conversion Factor",
"read_only": 1
},
{
"columns": 2,
"fieldname": "reserve_warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reserve Warehouse",
"options": "Warehouse"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "bom_detail_no",
"fieldtype": "Data",
"label": "BOM Detail No",
"read_only": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Data",
"label": "Reference Name",
"read_only": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "section_break_13",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Required Qty",
"read_only": 1
},
{
"fieldname": "supplied_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"hidden": 1
},
{
"fieldname": "total_supplied_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Total Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"hide_toolbar": 1,
"istable": 1,
"links": [],
"modified": "2022-04-07 12:58:28.208847",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Order Supplied Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SubcontractingOrderSuppliedItem(Document):
pass

View File

@ -0,0 +1,157 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide('erpnext.buying');
frappe.ui.form.on('Subcontracting Receipt', {
setup: (frm) => {
frm.get_field('supplied_items').grid.cannot_add_rows = true;
frm.get_field('supplied_items').grid.only_sortable();
frm.set_query('set_warehouse', () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query('rejected_warehouse', () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query('supplier_warehouse', () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query('warehouse', 'items', () => ({
filters: {
company: frm.doc.company,
is_group: 0
}
}));
frm.set_query('rejected_warehouse', 'items', () => ({
filters: {
company: frm.doc.company,
is_group: 0
}
}));
},
refresh: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(__("Stock Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
company: frm.doc.company,
show_cancelled_entries: frm.doc.docstatus === 2
};
frappe.set_route("query-report", "Stock Ledger");
}, __("View"));
frm.add_custom_button(__('Accounting Ledger'), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
company: frm.doc.company,
group_by: "Group by Voucher (Consolidated)",
show_cancelled_entries: frm.doc.docstatus === 2
};
frappe.set_route("query-report", "General Ledger");
}, __("View"));
}
if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) {
frm.add_custom_button(__('Subcontract Return'), function () {
frappe.model.open_mapped_doc({
method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
frm: frm
});
}, __('Create'));
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if (frm.doc.docstatus == 0) {
frm.add_custom_button(__('Subcontracting Order'), function () {
if (!frm.doc.supplier) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Supplier")
});
}
erpnext.utils.map_current_doc({
method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
source_doctype: "Subcontracting Order",
target: frm,
setters: {
supplier: frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
per_received: ["<", 100],
company: frm.doc.company
}
});
}, __("Get Items From"));
}
},
set_warehouse: (frm) => {
set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
},
rejected_warehouse: (frm) => {
set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
},
});
frappe.ui.form.on('Subcontracting Receipt Item', {
item_code(frm) {
set_missing_values(frm);
},
qty(frm) {
set_missing_values(frm);
},
rate(frm) {
set_missing_values(frm);
},
});
frappe.ui.form.on('Subcontracting Receipt Supplied Item', {
consumed_qty(frm) {
set_missing_values(frm);
},
});
let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => {
let transaction_controller = new erpnext.TransactionController();
transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
};
let set_missing_values = (frm) => {
frappe.call({
doc: frm.doc,
method: 'set_missing_values',
callback: (r) => {
if (!r.exc) frm.refresh();
},
});
};

View File

@ -0,0 +1,645 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2022-04-18 11:20:44.226738",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"naming_series",
"supplier",
"supplier_name",
"column_break1",
"company",
"posting_date",
"posting_time",
"is_return",
"return_against",
"section_addresses",
"supplier_address",
"contact_person",
"address_display",
"contact_display",
"contact_mobile",
"contact_email",
"col_break_address",
"shipping_address",
"shipping_address_display",
"billing_address",
"billing_address_display",
"sec_warehouse",
"set_warehouse",
"rejected_warehouse",
"col_break_warehouse",
"supplier_warehouse",
"items_section",
"items",
"section_break0",
"total_qty",
"column_break_27",
"total",
"raw_material_details",
"get_current_stock",
"supplied_items",
"section_break_46",
"in_words",
"bill_no",
"bill_date",
"accounting_details_section",
"provisional_expense_account",
"more_info",
"status",
"column_break_39",
"per_returned",
"section_break_47",
"amended_from",
"range",
"column_break4",
"represents_company",
"subscription_detail",
"auto_repeat",
"printing_settings",
"letter_head",
"language",
"instructions",
"column_break_97",
"select_print_heading",
"other_details",
"remarks",
"transporter_info",
"transporter_name",
"column_break5",
"lr_no",
"lr_date"
],
"fields": [
{
"allow_on_submit": 1,
"default": "{supplier_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "MAT-SCR-.YYYY.-\nMAT-SCR-RET-.YYYY.-",
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"bold": 1,
"fieldname": "supplier",
"fieldtype": "Link",
"in_global_search": 1,
"label": "Supplier",
"options": "Supplier",
"print_hide": 1,
"print_width": "150px",
"reqd": 1,
"search_index": 1,
"width": "150px"
},
{
"bold": 1,
"depends_on": "supplier",
"fetch_from": "supplier.supplier_name",
"fieldname": "supplier_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Supplier Name",
"read_only": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"no_copy": 1,
"print_width": "100px",
"reqd": 1,
"search_index": 1,
"width": "100px"
},
{
"description": "Time at which materials were received",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
"no_copy": 1,
"print_hide": 1,
"print_width": "100px",
"reqd": 1,
"width": "100px"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"print_width": "150px",
"remember_last_selected_value": 1,
"reqd": 1,
"width": "150px"
},
{
"collapsible": 1,
"fieldname": "section_addresses",
"fieldtype": "Section Break",
"label": "Address and Contact"
},
{
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Select Supplier Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"fieldname": "address_display",
"fieldtype": "Small Text",
"label": "Address",
"read_only": 1
},
{
"fieldname": "contact_display",
"fieldtype": "Small Text",
"in_global_search": 1,
"label": "Contact",
"read_only": 1
},
{
"fieldname": "contact_mobile",
"fieldtype": "Small Text",
"label": "Mobile No",
"read_only": 1
},
{
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
"options": "Email",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "col_break_address",
"fieldtype": "Column Break"
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Select Shipping Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "sec_warehouse",
"fieldtype": "Section Break"
},
{
"description": "Sets 'Accepted Warehouse' in each row of the Items table.",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"label": "Accepted Warehouse",
"options": "Warehouse",
"print_hide": 1
},
{
"description": "Sets 'Rejected Warehouse' in each row of the Items table.",
"fieldname": "rejected_warehouse",
"fieldtype": "Link",
"label": "Rejected Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1
},
{
"fieldname": "col_break_warehouse",
"fieldtype": "Column Break"
},
{
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
{
"allow_bulk_edit": 1,
"fieldname": "items",
"fieldtype": "Table",
"label": "Items",
"options": "Subcontracting Receipt Item",
"reqd": 1
},
{
"depends_on": "supplied_items",
"fieldname": "get_current_stock",
"fieldtype": "Button",
"label": "Get Current Stock",
"options": "get_current_stock",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "supplied_items",
"depends_on": "supplied_items",
"fieldname": "raw_material_details",
"fieldtype": "Section Break",
"label": "Raw Materials Consumed",
"options": "fa fa-table",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Consumed Items",
"no_copy": 1,
"options": "Subcontracting Receipt Supplied Item",
"print_hide": 1
},
{
"fieldname": "section_break0",
"fieldtype": "Section Break"
},
{
"fieldname": "total_qty",
"fieldtype": "Float",
"label": "Total Quantity",
"read_only": 1
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
"read_only": 1
},
{
"fieldname": "section_break_46",
"fieldtype": "Section Break"
},
{
"fieldname": "in_words",
"fieldtype": "Data",
"label": "In Words",
"length": 240,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "bill_no",
"fieldtype": "Data",
"hidden": 1,
"label": "Bill No",
"print_hide": 1
},
{
"fieldname": "bill_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Bill Date",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
"options": "fa fa-file-text"
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nCompleted\nReturn\nReturn Issued\nCancelled",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"reqd": 1,
"search_index": 1,
"width": "150px"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"options": "Subcontracting Receipt",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
"width": "150px"
},
{
"fieldname": "range",
"fieldtype": "Data",
"hidden": 1,
"label": "Range",
"print_hide": 1
},
{
"fieldname": "column_break4",
"fieldtype": "Column Break",
"print_hide": 1,
"print_width": "50%",
"width": "50%"
},
{
"fieldname": "subscription_detail",
"fieldtype": "Section Break",
"label": "Auto Repeat Detail"
},
{
"fieldname": "auto_repeat",
"fieldtype": "Link",
"label": "Auto Repeat",
"no_copy": 1,
"options": "Auto Repeat",
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "printing_settings",
"fieldtype": "Section Break",
"label": "Printing Settings"
},
{
"allow_on_submit": 1,
"fieldname": "letter_head",
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head",
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "select_print_heading",
"fieldtype": "Link",
"label": "Print Heading",
"no_copy": 1,
"options": "Print Heading",
"print_hide": 1,
"report_hide": 1
},
{
"fieldname": "language",
"fieldtype": "Data",
"label": "Print Language",
"read_only": 1
},
{
"fieldname": "column_break_97",
"fieldtype": "Column Break"
},
{
"fieldname": "other_details",
"fieldtype": "HTML",
"hidden": 1,
"label": "Other Details",
"options": "<div class=\"columnHeading\">Other Details</div>",
"print_hide": 1,
"print_width": "30%",
"width": "30%"
},
{
"fieldname": "instructions",
"fieldtype": "Small Text",
"label": "Instructions"
},
{
"fieldname": "remarks",
"fieldtype": "Small Text",
"label": "Remarks",
"print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "transporter_name",
"fieldname": "transporter_info",
"fieldtype": "Section Break",
"label": "Transporter Details",
"options": "fa fa-truck"
},
{
"fieldname": "transporter_name",
"fieldtype": "Data",
"label": "Transporter Name"
},
{
"fieldname": "column_break5",
"fieldtype": "Column Break",
"print_width": "50%",
"width": "50%"
},
{
"fieldname": "lr_no",
"fieldtype": "Data",
"label": "Vehicle Number",
"no_copy": 1,
"print_width": "100px",
"width": "100px"
},
{
"fieldname": "lr_date",
"fieldtype": "Date",
"label": "Vehicle Date",
"no_copy": 1,
"print_width": "100px",
"width": "100px"
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Select Billing Address",
"options": "Address"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address",
"read_only": 1
},
{
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "provisional_expense_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Provisional Expense Account",
"options": "Account"
},
{
"default": "0",
"fieldname": "is_return",
"fieldtype": "Check",
"label": "Is Return",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "is_return",
"fieldname": "return_against",
"fieldtype": "Link",
"label": "Return Against Subcontracting Receipt",
"no_copy": 1,
"options": "Subcontracting Receipt",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_39",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(!doc.__islocal && doc.is_return==0)",
"fieldname": "per_returned",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "% Returned",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_47",
"fieldtype": "Section Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-04-18 13:15:12.011682",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"submit": 1,
"write": 1
},
{
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"permlevel": 1,
"read": 1,
"role": "Stock Manager",
"write": 1
}
],
"search_fields": "status, posting_date, supplier",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}

View File

@ -0,0 +1,188 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import cint, getdate, nowdate
from erpnext.controllers.subcontracting_controller import SubcontractingController
class SubcontractingReceipt(SubcontractingController):
def __init__(self, *args, **kwargs):
super(SubcontractingReceipt, self).__init__(*args, **kwargs)
self.status_updater = [
{
"target_dt": "Subcontracting Order Item",
"join_field": "subcontracting_order_item",
"target_field": "received_qty",
"target_parent_dt": "Subcontracting Order",
"target_parent_field": "per_received",
"target_ref_field": "qty",
"source_dt": "Subcontracting Receipt Item",
"source_field": "received_qty",
"percent_join_field": "subcontracting_order",
"overflow_type": "receipt",
},
]
def update_status_updater_args(self):
if cint(self.is_return):
self.status_updater.extend(
[
{
"source_dt": "Subcontracting Receipt Item",
"target_dt": "Subcontracting Order Item",
"join_field": "subcontracting_order_item",
"target_field": "returned_qty",
"source_field": "-1 * qty",
"extra_cond": """ and exists (select name from `tabSubcontracting Receipt`
where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""",
},
{
"source_dt": "Subcontracting Receipt Item",
"target_dt": "Subcontracting Receipt Item",
"join_field": "subcontracting_receipt_item",
"target_field": "returned_qty",
"target_parent_dt": "Subcontracting Receipt",
"target_parent_field": "per_returned",
"target_ref_field": "received_qty",
"source_field": "-1 * received_qty",
"percent_join_field_parent": "return_against",
},
]
)
def before_validate(self):
super(SubcontractingReceipt, self).before_validate()
self.set_items_cost_center()
self.set_items_expense_account()
def validate(self):
super(SubcontractingReceipt, self).validate()
self.set_missing_values()
self.validate_posting_time()
self.validate_rejected_warehouse()
if self._action == "submit":
self.make_batches("warehouse")
if getdate(self.posting_date) > getdate(nowdate()):
frappe.throw(_("Posting Date cannot be future date"))
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.get_current_stock()
def on_submit(self):
self.update_status_updater_args()
self.update_prevdoc_status()
self.set_subcontracting_order_status()
self.set_consumed_qty_in_subcontract_order()
self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.update_status()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.update_status_updater_args()
self.update_prevdoc_status()
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
self.set_subcontracting_order_status()
self.update_status()
@frappe.whitelist()
def set_missing_values(self):
self.set_missing_values_in_supplied_items()
self.set_missing_values_in_items()
def set_missing_values_in_supplied_items(self):
for item in self.get("supplied_items") or []:
item.amount = item.rate * item.consumed_qty
def set_missing_values_in_items(self):
rm_supp_cost = {}
for item in self.get("supplied_items") or []:
if item.reference_name in rm_supp_cost:
rm_supp_cost[item.reference_name] += item.amount
else:
rm_supp_cost[item.reference_name] = item.amount
total_qty = total_amount = 0
for item in self.items:
if item.name in rm_supp_cost:
item.rm_supp_cost = rm_supp_cost[item.name]
item.rm_cost_per_qty = item.rm_supp_cost / item.qty
rm_supp_cost.pop(item.name)
if self.is_new() and item.rm_supp_cost > 0:
item.rate = (
item.rm_cost_per_qty + (item.service_cost_per_qty or 0) + item.additional_cost_per_qty
)
item.received_qty = item.qty + (item.rejected_qty or 0)
item.amount = item.qty * item.rate
total_qty += item.qty
total_amount += item.amount
else:
self.total_qty = total_qty
self.total = total_amount
def validate_rejected_warehouse(self):
if not self.rejected_warehouse:
for item in self.items:
if item.rejected_qty:
frappe.throw(
_("Rejected Warehouse is mandatory against rejected Item {0}").format(item.item_code)
)
def set_items_cost_center(self):
if self.company:
cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
for item in self.items:
if not item.cost_center:
item.cost_center = cost_center
def set_items_expense_account(self):
if self.company:
expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
for item in self.items:
if not item.expense_account:
item.expense_account = expense_account
def update_status(self, status=None, update_modified=False):
if self.docstatus >= 1 and not status:
if self.docstatus == 1:
if self.is_return:
status = "Return"
return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
return_against.run_method("update_status")
else:
if self.per_returned == 100:
status = "Return Issued"
elif self.status == "Draft":
status = "Completed"
elif self.docstatus == 2:
status = "Cancelled"
if status:
frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
@frappe.whitelist()
def make_subcontract_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("Subcontracting Receipt", source_name, target_doc)

View File

@ -0,0 +1,15 @@
from frappe import _
def get_data():
return {
"fieldname": "subcontracting_receipt_no",
"internal_links": {
"Subcontracting Order": ["items", "subcontracting_order"],
"Project": ["items", "project"],
"Quality Inspection": ["items", "quality_inspection"],
},
"transactions": [
{"label": _("Reference"), "items": ["Subcontracting Order", "Quality Inspection", "Project"]},
],
}

View File

@ -0,0 +1,14 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.listview_settings['Subcontracting Receipt'] = {
get_indicator: function (doc) {
const status_colors = {
"Draft": "grey",
"Return": "gray",
"Return Issued": "grey",
"Completed": "green",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
};

View File

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

View File

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

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SubcontractingReceiptItem(Document):
pass

View File

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

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SubcontractingReceiptSuppliedItem(Document):
pass

View File

@ -5,7 +5,7 @@
{% endblock %}
{% block head_include %}
<link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
<link rel="stylesheet" href="/assets/frappe/css/fonts/fontawesome/font-awesome.min.css">
{% endblock %}
{% block header %}

View File

@ -1,9 +1,9 @@
import frappe
import requests
from frappe import _
from frappe.core.utils import html2text
from frappe.utils import sanitize_html
from frappe.utils.global_search import search
from html2text import html2text
from jinja2 import utils

File diff suppressed because it is too large Load Diff

View File

@ -408,7 +408,7 @@ Boms,Boms,
Bonus Payment Date cannot be a past date,Дата выплаты бонуса не может быть прошлой датой,
Both Trial Period Start Date and Trial Period End Date must be set,"Должны быть установлены как дата начала пробного периода, так и дата окончания пробного периода",
Both Warehouse must belong to same Company,Оба Склад должены принадлежать одной Компании,
Branch,Ветвь,
Branch,Филиал,
Broadcasting,Вещание,
Brokerage,Посредничество,
Browse BOM,Просмотр спецификации,
@ -563,7 +563,7 @@ Commercial,Коммерческий сектор,
Commission,Комиссионный сбор,
Commission Rate %,Ставка комиссии %,
Commission on Sales,Комиссия по продажам,
Commission rate cannot be greater than 100,"Скорость Комиссия не может быть больше, чем 100",
Commission rate cannot be greater than 100,"Стоимость комиссии не может быть больше, чем 100",
Community Forum,Форум,
Company (not Customer or Supplier) master.,Компания (не клиента или поставщика) хозяин.,
Company Abbreviation,Аббревиатура компании,
@ -1066,7 +1066,7 @@ For Employee,Для сотрудника,
For Quantity (Manufactured Qty) is mandatory,Для Количество (Изготовитель Количество) является обязательным,
For Supplier,Для поставщиков,
For Warehouse,Для склада,
For Warehouse is required before Submit,Для Склада является обязательным полем для проведения,
For Warehouse is required before Submit,Для склада - обязательное полем для проводки,
"For an item {0}, quantity must be negative number",Для элемента {0} количество должно быть отрицательным числом,
"For an item {0}, quantity must be positive number",Для элемента {0} количество должно быть положительным числом,
"For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry",Для карты задания {0} вы можете только сделать запись запаса типа &#39;Передача материала для производства&#39;,
@ -1498,7 +1498,7 @@ Maintenance Schedule,График технического обслуживан
Maintenance Schedule is not generated for all the items. Please click on 'Generate Schedule',"График обслуживания не генерируется для всех элементов. Пожалуйста, нажмите на кнопку ""Generate Расписание""",
Maintenance Schedule {0} exists against {1},График обслуживания {0} существует против {1},
Maintenance Schedule {0} must be cancelled before cancelling this Sales Order,График Обслуживания {0} должен быть отменен до отмены этой Сделки,
Maintenance Status has to be Cancelled or Completed to Submit,Статус обслуживания должен быть отменен или завершен для отправки,
Maintenance Status has to be Cancelled or Completed to Submit,Статус обслуживания должен быть отменен или завершен для проводки,
Maintenance User,Сотрудник обслуживания,
Maintenance Visit,Заявки на техническое обслуживание,
Maintenance Visit {0} must be cancelled before cancelling this Sales Order,Посещение по Обслуживанию {0} должно быть отменено до отмены этой Сделки,
@ -1683,7 +1683,7 @@ No Data,Нет данных,
No Delivery Note selected for Customer {},Нет примечания о доставке для клиента {},
No Employee Found,Сотрудник не найден,
No Item with Barcode {0},Нет продукта со штрих-кодом {0},
No Item with Serial No {0},Нет продукта с серийным {0},
No Item with Serial No {0},Нет продукта с серийным номером {0},
No Items available for transfer,Нет доступных продуктов для перемещения,
No Items selected for transfer,Не выбраны продукты для перемещения,
No Items to pack,Нет продуктов для упаковки,
@ -2807,8 +2807,8 @@ Stock Transactions,Транзакции запасов,
Stock UOM,Единица измерения запасов,
Stock Value,Стоимость акций,
Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3},Для продукта {2} на складе {3} остатки запасов для партии {0} станут отрицательными {1},
Stock cannot be updated against Delivery Note {0},Фото не могут быть обновлены против накладной {0},
Stock cannot be updated against Purchase Receipt {0},Фото не может быть обновлен с квитанцией о покупке {0},
Stock cannot be updated against Delivery Note {0},Запасы не могут быть обновлены против накладной {0},
Stock cannot be updated against Purchase Receipt {0},Запасы не может быть обновлен с квитанцией о покупке {0},
Stock cannot exist for Item {0} since has variants,Запасов продукта {0} не существует с момента появления вариантов,
Stock transactions before {0} are frozen,Перемещения по складу до {0} заморожены,
Stop,Стоп,
@ -2845,12 +2845,12 @@ Sub Type,Подтип,
Sub-contracting,Суб-контракты,
Subcontract,Субподряд,
Subject,Тема,
Submit,Провести,
Submit,Утвердить,
Submit Proof,Отправить подтверждение,
Submit Salary Slip,Провести Зарплатную ведомость,
Submit this Work Order for further processing.,Отправьте этот рабочий заказ для дальнейшей обработки.,
Submit this to create the Employee record,"Отправьте это, чтобы создать запись сотрудника",
Submitting Salary Slips...,Отправка зарплатных листов ...,
Submit Salary Slip,Утведрдить зарплатную ведомость,
Submit this Work Order for further processing.,Утвердите этот рабочий заказ для дальнейшей обработки.,
Submit this to create the Employee record,"Утвердите это, чтобы создать запись сотрудника",
Submitting Salary Slips...,Проводка зарплатных ведомостей...,
Subscription,Подписка,
Subscription Management,Управление подпиской,
Subscriptions,Подписки,
@ -3187,7 +3187,7 @@ Update Print Format,Обновить формат печати,
Update Response,Обновить ответ,
Update bank payment dates with journals.,Обновление банк платежные даты с журналов.,
Update in progress. It might take a while.,Идет обновление. Это может занять некоторое время.,
Update rate as per last purchase,Скорость обновления согласно последней покупке,
Update rate as per last purchase,Обновлять стоимость согласно последней покупке,
Update stock must be enable for the purchase invoice {0},Обновление запасов должно быть включено для счета на покупку {0},
Updating Variants...,Обновление вариантов...,
Upload your letter head and logo. (you can edit them later).,Загрузить шапку фирменного бланка и логотип. (Вы можете отредактировать их позднее).,
@ -3310,7 +3310,7 @@ Work Order {0} must be cancelled before cancelling this Sales Order,Заказ
Work Order {0} must be submitted,Порядок работы {0} должен быть отправлен,
Work Orders Created: {0},Созданы рабочие задания: {0},
Work Summary for {0},Резюме работы для {0},
Work-in-Progress Warehouse is required before Submit,Работа-в-Прогресс Склад требуется перед Отправить,
Work-in-Progress Warehouse is required before Submit,Перед утверждением требуется склад незавершенного производства,
Workflow,Рабочий процесс,
Working,В работе,
Working Hours,Часы работы,
@ -3843,7 +3843,7 @@ Mobile No,Мобильный номер,
Mobile Number,Мобильный номер,
Month,Mесяц,
Name,Имя,
Near you,Возле тебя,
Near you,Возле вас,
Net Profit/Loss,Чистая прибыль / убыток,
New Expense,Новый расход,
New Invoice,Новый счет,
@ -3851,8 +3851,8 @@ New Payment,Новый платеж,
New release date should be in the future,Дата нового релиза должна быть в будущем,
Newsletter,Рассылка новостей,
No Account matched these filters: {},"Нет аккаунта, соответствующего этим фильтрам: {}",
No Employee found for the given employee field value. '{}': {},Сотрудник не найден для данного значения поля сотрудника. &#39;{}&#39;: {},
No Leaves Allocated to Employee: {0} for Leave Type: {1},Сотрудникам не выделено ни одного листа: {0} для типа отпуска: {1},
No Employee found for the given employee field value. '{}': {},Сотрудник не найден для данного значения поля сотрудника. '{}': {},
No Leaves Allocated to Employee: {0} for Leave Type: {1},Сотруднику не назначен отпуск: {0} для типа отпуска: {1},
No communication found.,Связь не найдена.,
No correct answer is set for {0},Не указан правильный ответ для {0},
No description,Без описания,
@ -3883,8 +3883,8 @@ Only expired allocation can be cancelled,Отменить можно тольк
Only users with the {0} role can create backdated leave applications,Только пользователи с ролью {0} могут создавать оставленные приложения с задним сроком действия,
Open,Открыт,
Open Contact,Открытый контакт,
Open Lead,Открытое обращение,
Opening and Closing,Открытие и Закрытие,
Open Lead,Открытый лид,
Opening and Closing,Открытие и закрытие,
Operating Cost as per Work Order / BOM,Эксплуатационные расходы согласно заказу на работу / спецификации,
Order Amount,Сумма заказа,
Page {0} of {1},Страница {0} из {1},
@ -4072,7 +4072,7 @@ Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and
Stores - {0},Магазины - {0},
Student with email {0} does not exist,Студент с электронной почтой {0} не существует,
Submit Review,Добавить отзыв,
Submitted,Проведенный,
Submitted,Утвержден,
Supplier Addresses And Contacts,Адреса и контакты поставщика,
Synchronize this account,Синхронизировать этот аккаунт,
Tag,Тег,
@ -4113,7 +4113,7 @@ Total Early Exits,Всего ранних выходов,
Total Late Entries,Всего поздних заявок,
Total Payment Request amount cannot be greater than {0} amount,Общая сумма запроса платежа не может превышать сумму {0},
Total payments amount can't be greater than {},Общая сумма платежей не может быть больше {},
Totals,Всего:,
Totals,Всего,
Training Event:,Учебное мероприятие:,
Transactions already retreived from the statement,Транзакции уже получены из заявления,
Transfer Material to Supplier,Перевести Материал Поставщику,
@ -4235,7 +4235,7 @@ Add to cart,Добавить в корзину,
Budget,Бюджет,
Chart of Accounts,План счетов,
Customer database.,База данных клиентов.,
Days Since Last order,Дни с последнего Заказать,
Days Since Last order,Дней с момента последнего заказа,
Download as JSON,Скачать как JSON,
End date can not be less than start date,"Дата окончания не может быть меньше, чем Дата начала",
For Default Supplier (Optional),Поставщик по умолчанию (необязательно),
@ -4304,9 +4304,9 @@ Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ст
Assets not created for {0}. You will have to create asset manually.,Активы не созданы для {0}. Вам придется создать актив вручную.,
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} имеет бухгалтерские записи в валюте {2} для компании {3}. Выберите счет дебиторской или кредиторской задолженности с валютой {2}.,
Invalid Account,Неверный аккаунт,
Purchase Order Required,"Покупка порядке, предусмотренном",
Purchase Receipt Required,Покупка Получение необходимое,
Account Missing,Аккаунт отсутствует,
Purchase Order Required,Требуется заказ на покупку,
Purchase Receipt Required,Требуется чек о покупке,
Account Missing,Счет отсутствует,
Requested,Запрошено,
Partially Paid,Частично оплачено,
Invalid Account Currency,Неверная валюта счета,
@ -4349,15 +4349,15 @@ Valid Upto date cannot be before Valid From date,Дата начала дейс
Valid From date not in Fiscal Year {0},Дата начала действия не в финансовом году {0},
Valid Upto date not in Fiscal Year {0},Действительно до даты не в финансовом году {0},
Group Roll No,Групповой опрос №,
Maintain Same Rate Throughout Sales Cycle,Поддержание же скоростью протяжении цикла продаж,
Maintain Same Rate Throughout Sales Cycle,Поддержание одинаковой ставки на протяжении всего цикла продаж,
"Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.","Строка {1}: количество ({0}) не может быть дробью. Чтобы разрешить это, отключите &#39;{2}&#39; в единицах измерения {3}.",
Must be Whole Number,Должно быть целое число,
Please setup Razorpay Plan ID,Настройте идентификатор плана Razorpay,
Contact Creation Failed,Не удалось создать контакт,
{0} already exists for employee {1} and period {2},{0} уже существует для сотрудника {1} и периода {2},
Leaves Allocated,Распределенные листья,
Leaves Expired,Листья просрочены,
Leave Without Pay does not match with approved {} records,&quot;Leave Without Pay&quot; не совпадает с утвержденными записями: {},
Leaves Allocated,Распределенные отпуска,
Leaves Expired,Отпуска просрочены,
Leave Without Pay does not match with approved {} records,Leave Without Pay не совпадает с утвержденными записями: {},
Income Tax Slab not set in Salary Structure Assignment: {0},Плита подоходного налога не указана в назначении структуры заработной платы: {0},
Income Tax Slab: {0} is disabled,Плита подоходного налога: {0} отключена,
Income Tax Slab must be effective on or before Payroll Period Start Date: {0},Таблица подоходного налога должна вступить в силу не позднее даты начала периода расчета зарплаты: {0},
@ -4529,7 +4529,7 @@ Automatically Fetch Payment Terms,Автоматически получать у
Show Payment Schedule in Print,Показать график платежей в печати,
Currency Exchange Settings,Настройки обмена валюты,
Allow Stale Exchange Rates,Разрешить статичные обменные курсы,
Stale Days,Прошлые дни,
Stale Days,Прошедшие дни,
Report Settings,Настройки отчета,
Use Custom Cash Flow Format,Использовать формат пользовательского денежного потока,
Allowed To Transact With,Разрешено спрятать,
@ -4628,16 +4628,16 @@ Action if Accumulated Monthly Budget Exceeded on Actual,"Действие, ес
Budget Accounts,Счета бюджета,
Budget Account,Бюджет аккаунта,
Budget Amount,Сумма бюджета,
C-Form,C-образный,
C-Form,C-Форма,
ACC-CF-.YYYY.-,ACC-CF-.YYYY.-,
C-Form No,C-образный Нет,
C-Form No,C-Форма №,
Received Date,Дата получения,
Quarter,Квартал,
I,I,
II,II,
III,III,
IV,IV,
C-Form Invoice Detail,C-образный Счет Подробно,
C-Form Invoice Detail,C-Форма детали счета,
Invoice No,Номер cчета,
Cash Flow Mapper,Диспетчер денежных потоков,
Section Name,Название раздела,
@ -4739,7 +4739,7 @@ GST Account,НДС счет,
CGST Account,CGST счет,
SGST Account,SGST счет,
IGST Account,IGST счет,
CESS Account,CESS-аккаунт,
CESS Account,CESS-счет,
Loan Start Date,Дата начала займа,
Loan Period (Days),Срок кредитования (дни),
Loan End Date,Дата окончания займа,
@ -4806,15 +4806,15 @@ Collection Tier,Уровень сбора,
Collection Rules,Правила сбора,
Redemption,Выплата,
Conversion Factor,Коэффициент конверсии,
1 Loyalty Points = How much base currency?,1 Бонусные баллы = Сколько базовой валюты?,
1 Loyalty Points = How much base currency?,1 балл лояльности = Сколько базовой валюты?,
Expiry Duration (in days),Продолжительность действия (в днях),
Help Section,Раздел справки,
Loyalty Program Help,Помощь в программе лояльности,
Loyalty Program Collection,Коллекция программы лояльности,
Tier Name,Название уровня,
Minimum Total Spent,Минимальные общие затраты,
Collection Factor (=1 LP),Коэффициент сбора (=1 Балл),
For how much spent = 1 Loyalty Point,За сколько потраченных = 1 Балл лояльности,
Collection Factor (=1 LP),Коэффициент сбора (=1 БЛ),
For how much spent = 1 Loyalty Point,За сколько потрачено = 1 Балл лояльности,
Mode of Payment Account,Форма оплаты счета,
Default Account,По умолчанию учетная запись,
Default account will be automatically updated in POS Invoice when this mode is selected.,"Учетная запись по умолчанию будет автоматически обновляться в POS-счете, если выбран этот режим.",
@ -5159,7 +5159,7 @@ Brand Name,Имя бренда,
Qty as per Stock UOM,Кол-во в соответствии с ед.измерения запасов,
Discount and Margin,Скидка и маржа,
Rate With Margin,Оценить с маржой,
Discount (%) on Price List Rate with Margin,Скидка (%) на цену Прейскурант с маржой,
Discount (%) on Price List Rate with Margin,Скидка (%) на цену из прайс-листа с маржой,
Rate With Margin (Company Currency),Ставка с маржей (в валюте компании),
Delivered By Supplier,Доставлено поставщиком,
Deferred Revenue,Отложенный доход,
@ -5505,9 +5505,9 @@ Returned Qty,Вернулся Кол-во,
Purchase Order Item Supplied,Заказ товара Поставляется,
BOM Detail No,Подробности спецификации №,
Stock Uom,Единица измерения запасов,
Raw Material Item Code,Код сырьевой позиции,
Raw Material Item Code,Код исходного материала,
Supplied Qty,Поставляемое кол-во,
Purchase Receipt Item Supplied,Покупка Получение товара Поставляется,
Purchase Receipt Item Supplied,Квитанция о покупке предоставлена,
Current Stock,Наличие на складе,
PUR-RFQ-.YYYY.-,PUR-RFQ-.YYYY.-,
For individual supplier,Для индивидуального поставщика,
@ -5541,7 +5541,7 @@ Default Payable Accounts,По умолчанию задолженность Кр
Mention if non-standard payable account,"Упомяните, если нестандартный подлежащий оплате счет",
Default Tax Withholding Config,Конфигурация удержания налога по умолчанию,
Supplier Details,Подробная информация о поставщике,
Statutory info and other general information about your Supplier,Уставный информации и другие общие сведения о вашем Поставщик,
Statutory info and other general information about your Supplier,Правовая информация и другие общие сведения о вашем поставщике,
PUR-SQTN-.YYYY.-,PUR-SQTN-.YYYY.-,
Supplier Address,Адрес поставщика,
Link to material requests,Ссылка на заявки на материалы,
@ -5569,7 +5569,7 @@ Notify Supplier,Сообщите поставщику,
Notify Employee,Уведомить сотрудника,
Supplier Scorecard Criteria,Критерии оценки поставщиков,
Criteria Name,Название критерия,
Max Score,Макс. Оценка,
Max Score,Макс. оценка,
Criteria Formula,Формула критериев,
Criteria Weight,Вес критериев,
Supplier Scorecard Period,Период оценки поставщика,
@ -5752,7 +5752,7 @@ Examiner,экзаменатор,
Examiner Name,Имя экзаменатора,
Supervisor,Руководитель,
Supervisor Name,Имя супервизора,
Evaluate,оценивать,
Evaluate,Оценивать,
Maximum Assessment Score,Максимальный балл оценки,
Assessment Plan Criteria,Критерии оценки плана,
Maximum Score,Максимальный балл,
@ -6179,7 +6179,7 @@ Basic Details,Основные детали,
HLC-PRAC-.YYYY.-,HLC-PRAC-.YYYY.-,
Mobile,Мобильный,
Phone (R),Телефон (R),
Phone (Office),Телефон(офисный),
Phone (Office),Телефон (офис),
Employee and User Details,Сведения о сотруднике и пользователе,
Hospital,Больница,
Appointments,Назначения,
@ -6198,7 +6198,7 @@ Vacant,Вакантно,
Occupied,Занято,
Item Details,Детальная информация о продукте,
UOM Conversion in Hours,Преобразование UOM в часы,
Rate / UOM,Скорость / UOM,
Rate / UOM,Стоимость / UOM,
Change in Item,Изменение продукта,
Out Patient Settings,Настройки пациента,
Patient Name By,Имя пациента,
@ -6266,7 +6266,7 @@ Discharge Date,Дата выписки,
Lab Prescription,Лабораторный рецепт,
Lab Test Name,Название лабораторного теста,
Test Created,Тест создан,
Submitted Date,Дата отправки,
Submitted Date,Дата утверждения,
Approved Date,Утвержденная дата,
Sample ID,Образец,
Lab Technician,Лаборант,
@ -6475,7 +6475,7 @@ On Duty,На службе,
Explanation,Объяснение,
Compensatory Leave Request,Компенсационный отпуск,
Leave Allocation,Распределение отпусков,
Worked On Holiday,Работал на отдыхе,
Worked On Holiday,Работал на праздниках,
Work From Date,Работа с даты,
Work End Date,Дата окончания работы,
Email Sent To,Email отправлен,
@ -6486,7 +6486,7 @@ Daily Work Summary Group User,Ежедневная рабочая группа,
email,Эл. адрес,
Parent Department,Родительский отдел,
Leave Block List,Оставьте список есть,
Days for which Holidays are blocked for this department.,"Дни, для которых Праздники заблокированные для этого отдела.",
Days for which Holidays are blocked for this department.,"Дни, для которые праздники заблокированные для этого отдела.",
Leave Approver,Подтверждение отпусков,
Expense Approver,Подтверждающий расходы,
Department Approver,Подтверждение департамента,
@ -6805,7 +6805,7 @@ Encashable days,Места для инкаширования,
Encashment Amount,Сумма инкассации,
Leave Ledger Entry,Выйти из книги,
Transaction Name,Название транзакции,
Is Carry Forward,Является ли переносить,
Is Carry Forward,Переносится вперед,
Is Expired,Истек,
Is Leave Without Pay,Отпуск без содержания,
Holiday List for Optional Leave,Список праздников для дополнительного отпуска,
@ -6926,9 +6926,9 @@ Last Sync of Checkin,Последняя синхронизация регист
Last Known Successful Sync of Employee Checkin. Reset this only if you are sure that all Logs are synced from all the locations. Please don't modify this if you are unsure.,"Последняя известная успешная синхронизация регистрации сотрудника. Сбрасывайте это, только если вы уверены, что все журналы синхронизированы из всех мест. Пожалуйста, не изменяйте это, если вы не уверены.",
Grace Period Settings For Auto Attendance,Настройки льготного периода для автоматической посещаемости,
Enable Entry Grace Period,Включить льготный период,
Late Entry Grace Period,Льготный период позднего въезда,
Late Entry Grace Period,Льготный период позднего входа,
The time after the shift start time when check-in is considered as late (in minutes).,"Время после начала смены, когда регистрация считается поздней (в минутах).",
Enable Exit Grace Period,Включить Exit Grace Period,
Enable Exit Grace Period,Разрешить выход из льготного периода,
Early Exit Grace Period,Льготный период раннего выхода,
The time before the shift end time when check-out is considered as early (in minutes).,Время до окончания смены при выезде считается ранним (в минутах).,
Skill Name,Название навыка,
@ -7228,7 +7228,7 @@ BOM Operation,Операция спецификации,
Operation Time ,Время операции,
In minutes,В считанные минуты,
Batch Size,Размер партии,
Base Hour Rate(Company Currency),Базовый час Rate (в валюте компании),
Base Hour Rate(Company Currency),Базовая часовая ставка (в валюте компании),
Operating Cost(Company Currency),Эксплуатационные расходы (в валюте компании),
BOM Scrap Item,Спецификация отходов продукта,
Basic Amount (Company Currency),Базовая сумма (в валюте компании),
@ -8304,7 +8304,7 @@ Default Source Warehouse,По умолчанию склад сырья,
Source Warehouse Address,Адрес источника склада,
Default Target Warehouse,Цель по умолчанию Склад,
Target Warehouse Address,Адрес целевого склада,
Update Rate and Availability,Скорость обновления и доступность,
Update Rate and Availability,Обновить стоимость и доступность,
Total Incoming Value,Всего входное значение,
Total Outgoing Value,Всего исходящее значение,
Total Value Difference (Out - In),Общая стоимость Разница (Out - In),
@ -8328,7 +8328,7 @@ Stock Ledger Entry,Записи в остатках,
Outgoing Rate,Исходящие Оценить,
Actual Qty After Transaction,Остаток после проведения,
Stock Value Difference,Расхождение стоимости запасов,
Stock Queue (FIFO),Фото со Очередь (FIFO),
Stock Queue (FIFO),Очередь запасов (FIFO),
Is Cancelled,Является отмененным,
Stock Reconciliation,Инвентаризация запасов,
This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Этот инструмент поможет вам обновить или исправить количество и оценку запасов в системе. Это, как правило, используется для синхронизации системных значений и то, что на самом деле существует в ваших складах.",
@ -8888,7 +8888,7 @@ Practitioner Name,Имя практикующего,
Enter a name for the Clinical Procedure Template,Введите имя для шаблона клинической процедуры,
Set the Item Code which will be used for billing the Clinical Procedure.,"Установите код товара, который будет использоваться для выставления счета за клиническую процедуру.",
Select an Item Group for the Clinical Procedure Item.,Выберите группу элементов для элемента клинической процедуры.,
Clinical Procedure Rate,Скорость клинической процедуры,
Clinical Procedure Rate,Стоимость клинической процедуры,
Check this if the Clinical Procedure is billable and also set the rate.,"Отметьте это, если клиническая процедура оплачивается, а также установите ставку.",
Check this if the Clinical Procedure utilises consumables. Click ,"Проверьте это, если в клинической процедуре используются расходные материалы. Нажмите",
to know more,узнать больше,
@ -9067,7 +9067,7 @@ Rented To Date,Сдано на дату,
Monthly Eligible Amount,Ежемесячная приемлемая сумма,
Total Eligible HRA Exemption,Полное соответствие требованиям HRA,
Validating Employee Attendance...,Проверка явки сотрудников...,
Submitting Salary Slips and creating Journal Entry...,Отправка ведомостей о заработной плате и создание записи в журнале ...,
Submitting Salary Slips and creating Journal Entry...,Утверждение ведомостей о заработной плате и создание записи в журнале ...,
Calculate Payroll Working Days Based On,Расчет рабочих дней для расчета заработной платы на основе,
Consider Unmarked Attendance As,Считайте неотмеченную посещаемость как,
Fraction of Daily Salary for Half Day,Доля дневной заработной платы за полдня,
@ -9109,7 +9109,7 @@ MAT-PR-RET-.YYYY.-,MAT-PR-RET-.YYYY.-,
Track this Purchase Receipt against any Project,Отслеживайте эту квитанцию о покупке для любого проекта,
Please Select a Supplier,"Пожалуйста, выберите поставщика",
Add to Transit,Добавить в общественный транспорт,
Set Basic Rate Manually,Установить базовую скорость вручную,
Set Basic Rate Manually,Установить базовую стоимость вручную,
"By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ","По умолчанию имя элемента устанавливается в соответствии с введенным кодом элемента. Если вы хотите, чтобы элементы назывались",
Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.,Установите склад по умолчанию для складских операций. Он будет загружен в Хранилище по умолчанию в мастере предметов.,
"This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.","Это позволит отображать товары на складе с отрицательными значениями. Использование этой опции зависит от вашего варианта использования. Если этот параметр не отмечен, система предупреждает, прежде чем препятствовать транзакции, вызывающей отрицательный запас.",
@ -9845,3 +9845,8 @@ Overdue,Просрочено,
Completed,Завершенно,
Total Tasks,Всего задач,
Build,Конструктор,
Amend,Исправить,
Role Allowed to Over Deliver/Receive,"Роль, разрешенная для сверхдоставки/получения",
Unit of Measure (UOM),Единицы измерения (ЕИ),
Bank Reconciliation Tool,Инструмент сверки банковских счетов,
Delayed Tasks Summary,Сводка отложенных задач,

Can't render this file because it is too large.

View File

@ -1,6 +1,6 @@
# TODO: Remove this file when v15.0.0 is released
from setuptools import setup
name = "frappe"
name = "erpnext"
setup()