Merge branch 'develop' into patch-4

This commit is contained in:
Dany Robert 2021-06-22 11:18:17 +05:30 committed by GitHub
commit 884dd9764b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2098 additions and 1093 deletions

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.5.0' __version__ = '13.5.1'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -257,9 +257,10 @@
}, },
{ {
"default": "1", "default": "1",
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
"fieldname": "post_change_gl_entries", "fieldname": "post_change_gl_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Post Ledger Entries for Given Change" "label": "Create Ledger Entries for Change Amount"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -267,7 +268,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-05-25 12:34:05.858669", "modified": "2021-06-17 20:26:03.721202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
def check_duplicate_terms(self): def check_duplicate_terms(self):
terms = [] terms = []
for term in self.terms: for term in self.terms:
term_info = (term.credit_days, term.credit_months, term.due_date_based_on) term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
if term_info in terms: if term_info in terms:
frappe.msgprint( frappe.msgprint(
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx), _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),

View File

@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO # because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()

View File

@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction()) self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self): 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 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", 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", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100) qty=100, basic_rate=100)

View File

@ -989,7 +989,7 @@ class SalesInvoice(SellingController):
for payment_mode in self.payments: for payment_mode in self.payments:
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= self.change_amount payment_mode.base_amount -= flt(self.change_amount)
if payment_mode.amount: if payment_mode.amount:
# POS, make payment entries # POS, make payment entries

View File

@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
} else { } else {
frm.set_value("tax_withholding_category", frm.supplier_tds); frm.set_value("tax_withholding_category", frm.supplier_tds);
} }
},
refresh: function(frm) {
frm.trigger('get_materials_from_supplier');
},
get_materials_from_supplier: function(frm) {
let po_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
}
});
}
if (po_details && po_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: { purchase_order: frm.doc.name, po_details: po_details },
callback: function(r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
} }
}); });
@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
} }
has_unsupplied_items() { has_unsupplied_items() {
return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty) return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
} }
make_stock_entry() { make_stock_entry() {
@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); var data = d.get_values();
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
frappe.call({ frappe.call({
method: "frappe.desk.form.utils.add_comment", method: "frappe.desk.form.utils.add_comment",
args: { args: {
reference_doctype: me.frm.doctype, reference_doctype: me.frm.doctype,
reference_name: me.frm.docname, reference_name: me.frm.docname,
content: __('Reason for hold:') + " " +data.reason_for_hold, content: __(reason_for_hold),
comment_email: frappe.session.user, comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname comment_by: frappe.session.user_fullname
}, },

View File

@ -609,6 +609,7 @@
"fieldname": "supplied_items", "fieldname": "supplied_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Supplied Items", "label": "Supplied Items",
"no_copy": 1,
"oldfieldname": "po_raw_material_details", "oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Order Item Supplied", "options": "Purchase Order Item Supplied",
@ -1377,7 +1378,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 00:55:30.781375", "modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency from erpnext.accounts.party import get_party_account_currency
from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
unlink_inter_company_doc update_linked_doc, unlink_inter_company_doc)
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist() @frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items): def make_rm_stock_entry(purchase_order, rm_items):
if isinstance(rm_items, string_types): rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items) rm_items_list = json.loads(rm_items)
else: elif not rm_items:
frappe.throw(_("No Items available for transfer")) frappe.throw(_("No Items available for transfer"))
if rm_items_list: if rm_items_list:
@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"], 'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"], 'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"], '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"], 'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
} }
@ -582,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None): def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc) return make_inter_company_transaction("Purchase Order", source_name, target_doc)
@frappe.whitelist()
def get_materials_from_supplier(purchase_order, po_details):
if isinstance(po_details, 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_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
ste_doc = frappe.new_doc('Stock Entry')
ste_doc.purpose = 'Material Transfer'
ste_doc.purchase_order = po_doc.name
ste_doc.company = po_doc.company
ste_doc.is_return = 1
for key, value in available_materials.items():
if not value.qty:
continue
if value.batch_no:
for batch_no, qty in value.batch_no.items():
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.calculate_rate_and_amount()
return ste_doc
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
item = ste_doc.append('items', row.item_details)
po_detail = list(set(row.po_details).intersection(po_details))
item.update({
'qty': qty,
'batch_no': batch_no,
'basic_rate': row.item_details['rate'],
'po_detail': po_detail[0] if po_detail else '',
's_warehouse': row.item_details['t_warehouse'],
't_warehouse': row.item_details['s_warehouse'],
'item_code': row.item_details['rm_item_code'],
'subcontracted_item': row.item_details['main_item_code'],
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
})

View File

@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase): class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self): def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code) make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1, po = create_purchase_order(item_code=item_code, qty=1,
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items: for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item transferred_rm_map[item.get('rm_item_code')] = item
for item in pr.get('supplied_items'):
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
update_backflush_based_on("BOM")
def test_backflushed_based_on_for_multiple_batches(self):
item_code = "_Test Subcontracted FG Item 2"
make_item('Sub Contracted Raw Material 2', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
raw_materials=["Sub Contracted Raw Material 2"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 500
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
make_new_batch(batch_id=batch, item_code=item_code)
pr = make_purchase_receipt(po.name)
# partial receipt
pr.get('items')[0].qty = 30
pr.get('items')[0].batch_no = "ABCD1"
purchase_order = po.name
purchase_order_item = po.items[0].name
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
pr.append("items", {
"item_code": pr.get('items')[0].item_code,
"item_name": pr.get('items')[0].item_name,
"uom": pr.get('items')[0].uom,
"stock_uom": pr.get('items')[0].stock_uom,
"warehouse": pr.get('items')[0].warehouse,
"conversion_factor": pr.get('items')[0].conversion_factor,
"cost_center": pr.get('items')[0].cost_center,
"rate": pr.get('items')[0].rate,
"qty": qty,
"batch_no": batch_no,
"purchase_order": purchase_order,
"purchase_order_item": purchase_order_item
})
pr.submit()
pr1 = make_purchase_receipt(po.name)
pr1.get('items')[0].qty = 300
pr1.get('items')[0].batch_no = "ABCD1"
pr1.save()
pr_key = ("Sub Contracted Raw Material 2", po.name)
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
update_backflush_based_on("BOM") update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self): def test_supplied_qty_against_subcontracted_po(self):
@ -1117,6 +1043,10 @@ def create_purchase_order(**args):
po.conversion_factor = args.conversion_factor or 1 po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None po.supplier_warehouse = args.supplier_warehouse or None
if args.rm_items:
for row in args.rm_items:
po.append("items", row)
else:
po.append("items", { po.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
@ -1126,12 +1056,15 @@ def create_purchase_order(**args):
"include_exploded_items": args.get('include_exploded_items', 1), "include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order "against_blanket_order": args.against_blanket_order
}) })
po.set_missing_values()
if not args.do_not_save: if not args.do_not_save:
po.insert() po.insert()
if not args.do_not_submit: if not args.do_not_submit:
if po.is_subcontracted == "Yes": if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items") supp_items = po.get("supplied_items")
for d in supp_items: for d in supp_items:
if not d.reserve_warehouse:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC" d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit() po.submit()

View File

@ -6,21 +6,25 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"bom_detail_no", "rm_item_code",
"column_break_3",
"stock_uom", "stock_uom",
"reserve_warehouse",
"conversion_factor", "conversion_factor",
"column_break_6", "column_break_6",
"rm_item_code", "bom_detail_no",
"reference_name", "reference_name",
"reserve_warehouse",
"section_break2", "section_break2",
"rate", "rate",
"col_break2", "col_break2",
"amount", "amount",
"section_break1", "section_break1",
"required_qty", "required_qty",
"supplied_qty",
"col_break1", "col_break1",
"supplied_qty" "consumed_qty",
"returned_qty",
"total_supplied_qty"
], ],
"fields": [ "fields": [
{ {
@ -125,6 +129,8 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Supplied Qty", "label": "Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@ -142,13 +148,42 @@
{ {
"fieldname": "col_break2", "fieldname": "col_break2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "total_supplied_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Total Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-09 15:17:58.128242",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item Supplied", "name": "Purchase Order Item Supplied",

View File

@ -6,10 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"description", "rm_item_code",
"item_name",
"bom_detail_no", "bom_detail_no",
"col_break1", "col_break1",
"rm_item_code", "description",
"stock_uom", "stock_uom",
"conversion_factor", "conversion_factor",
"reference_name", "reference_name",
@ -25,7 +26,8 @@
"secbreak_3", "secbreak_3",
"batch_no", "batch_no",
"col_break4", "col_break4",
"serial_no" "serial_no",
"purchase_order"
], ],
"fields": [ "fields": [
{ {
@ -52,7 +54,6 @@
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1,
"label": "Description", "label": "Description",
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@ -81,18 +82,20 @@
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Required Qty", "label": "Available Qty For Consumption",
"oldfieldname": "required_qty", "oldfieldname": "required_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Consumed Qty", "in_list_view": 1,
"label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty", "oldfieldname": "consumed_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@ -183,12 +186,28 @@
{ {
"fieldname": "col_break4", "fieldname": "col_break4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"hidden": 1,
"label": "Purchase Order",
"no_copy": 1,
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Receipt Item Supplied", "name": "Purchase Receipt Item Supplied",

View File

@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
create_rfq_items(sq_doc, supplier, data) create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data): def create_rfq_items(sq_doc, supplier, data):
sq_doc.append('items', { args = {}
"item_code": data.item_code,
"item_name": data.item_name, for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
"description": data.description, 'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
"qty": data.qty, args[field] = data.get(field)
"rate": data.rate,
"conversion_factor": data.conversion_factor if data.conversion_factor else None, args.update({
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
"warehouse": data.warehouse or '',
"request_for_quotation_item": data.name, "request_for_quotation_item": data.name,
"request_for_quotation": data.parent "request_for_quotation": data.parent,
"supplier_part_no": frappe.db.get_value("Item Supplier",
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
}) })
sq_doc.append('items', args)
@frappe.whitelist() @frappe.whitelist()
def get_pdf(doctype, name, supplier): def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier) doc = get_rfq_doc(doctype, name, supplier)

View File

@ -0,0 +1,45 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontract Order Summary"] = {
"filters": [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __("From Date"),
fieldname:"from_date",
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1
},
{
label: __("Purchase Order"),
fieldname: "name",
fieldtype: "Link",
options: "Purchase Order",
get_query: function() {
return {
filters: {
docstatus: 1,
is_subcontracted: 'Yes',
company: frappe.query_report.get_filter_value('company')
}
}
}
}
]
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-05-31 14:43:32.417694",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-05-31 14:43:32.417694",
"modified_by": "Administrator",
"module": "Buying",
"name": "Subcontract Order Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Subcontract Order Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
}
]
}

View File

@ -0,0 +1,152 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns, data = [], []
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(report_filters):
data = []
orders = get_subcontracted_orders(report_filters)
if orders:
supplied_items = get_supplied_items(orders, report_filters)
po_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(po_details, data)
return data
def get_subcontracted_orders(report_filters):
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
filters = get_filters(report_filters)
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
def get_filters(report_filters):
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
for field in ['name', 'company']:
if report_filters.get(field):
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
return filters
def get_supplied_items(orders, report_filters):
if not orders:
return []
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
supplied_items = {}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
return supplied_items
def prepare_subcontracted_data(orders, supplied_items):
po_details = {}
for row in orders:
key = (row.po_id, row.name, row.item_code)
if key not in po_details:
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
details = po_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details['supplied_items'].append(supplied_item)
return po_details
def get_subcontracted_data(po_details, data):
for key, details in po_details.items():
res = details.po_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
res.update(row)
data.append(res)
def get_columns():
return [
{
"label": _("Purchase Order"),
"fieldname": "po_id",
"fieldtype": "Link",
"options": "Purchase Order",
"width": 100
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
"width": 80
},
{
"label": _("Subcontracted Item"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Order Qty"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 90
},
{
"label": _("Received Qty"),
"fieldname": "received_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Item"),
"fieldname": "rm_item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Qty"),
"fieldname": "supplied_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Consumed Qty"),
"fieldname": "consumed_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Returned Qty"),
"fieldname": "returned_qty",
"fieldtype": "Float",
"width": 110
}
]

View File

@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController): from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.subcontracting import Subcontracting
class BuyingController(StockController, Subcontracting):
def get_feed(self): def get_feed(self):
if self.get("supplier_name"): if self.get("supplier_name"):
@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate() self.update_valuation_rate()
def onload(self):
super(BuyingController, self).onload()
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
'backflush_raw_materials_of_subcontract_based_on'))
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@ -171,12 +177,13 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount TODO: rename item_tax_amount to valuation_tax_amount
""" """
stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1 last_item_idx = 1
for d in self.get("items"): for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items: if (d.item_code and d.item_code in stock_and_asset_items):
stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount) stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx last_item_idx = d.idx
@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0 supplied_items_cost = 0.0
for d in self.get("supplied_items"): for d in self.get("supplied_items"):
if d.reference_name == item_row_id: if d.reference_name == item_row_id:
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({ rate = get_incoming_rate({
"item_code": d.rm_item_code, "item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse, "warehouse": self.supplier_warehouse,
@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom: if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype == "Purchase Order": if self.doctype != "Purchase Order":
for supplied_item in self.get("supplied_items"): return
if not supplied_item.reserve_warehouse:
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
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: else:
for item in self.get("items"): for item in self.get("items"):
if item.bom: if item.bom:
@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table): def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes": if self.is_subcontracted=="Yes":
parent_items = [] self.set_materials_for_subcontracted_items(raw_material_table)
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
if (self.doctype == 'Purchase Receipt' and
backflush_raw_materials_based_on != 'BOM'):
self.update_raw_materials_supplied_based_on_stock_entries()
else:
for item in self.get("items"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
item.rm_supp_cost = 0.0
if item.bom and item.item_code in self.sub_contracted_items:
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
if [item.item_code, item.name] not in parent_items:
parent_items.append([item.item_code, item.name])
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"): for item in self.get("items"):
@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"): if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', []) self.set('supplied_items', [])
def update_raw_materials_supplied_based_on_stock_entries(self):
self.set('supplied_items', [])
purchase_orders = set(d.purchase_order for d in self.items)
# qty of raw materials backflushed (for each item per purchase order)
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
# qty of "finished good" item yet to be received
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'):
if not item.purchase_order:
continue
# reset raw_material cost
item.rm_supp_cost = 0
# qty of raw materials transferred to the supplier
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
item_key = '{}{}'.format(item.item_code, item.purchase_order)
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
if not fg_yet_to_be_received:
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
.format(item.idx, frappe.bold(item.item_code),
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty
rm_qty_to_be_consumed = transferred_qty - consumed_qty
# backflush all remaining transferred qty in the last Purchase Receipt
if fg_yet_to_be_received == item.qty:
qty = rm_qty_to_be_consumed
else:
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
qty = frappe.utils.ceil(qty)
if qty > rm_qty_to_be_consumed:
qty = rm_qty_to_be_consumed
if not qty: continue
if raw_material.serial_nos:
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
if qty > 0:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
rm.main_item_code = fg_item_row.item_code
rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
exploded_item = item.get('include_exploded_items')
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
items = list(set([d.item_code for d in bom_items]))
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
from `tabItem` i, `tabItem Default` id
where id.parent=i.name and id.company=%s and i.name in ({0})"""
.format(", ".join(["%s"] * len(items))), [self.company] + items))
for bom_item in bom_items:
if self.doctype == "Purchase Order":
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
reserve_warehouse = None
conversion_factor = item.conversion_factor
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
bom_item.item_name = alternative_item_data.item_name
bom_item.stock_uom = alternative_item_data.stock_uom
conversion_factor = alternative_item_data.conversion_factor
bom_item.description = alternative_item_data.description
# check if exists
exists = 0
for d in self.get(raw_material_table):
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
and d.reference_name == item.name:
rm, exists = d, 1
break
if not exists:
rm = self.append(raw_material_table, {})
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
flt(conversion_factor), rm.precision("required_qty"))
rm.reference_name = item.name
rm.bom_detail_no = bom_item.name
rm.main_item_code = item.item_code
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
rm.consumed_qty = required_qty
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
elif not rm.reserve_warehouse:
rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
delete_list = []
for d in self.get(raw_material_table):
if [d.main_item_code, d.reference_name] not in parent_items:
# mark for deletion from doclist
delete_list.append(d)
# delete from doclist
if delete_list:
rm_supplied_details = self.get(raw_material_table)
self.set(raw_material_table, [])
for d in rm_supplied_details:
if d not in delete_list:
self.append(raw_material_table, d)
@property @property
def sub_contracted_items(self): def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"): if not hasattr(self, "_sub_contracted_items"):
@ -683,6 +506,7 @@ class BuyingController(StockController):
self.process_fixed_asset() self.process_fixed_asset()
self.update_fixed_asset(field) self.update_fixed_asset(field)
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 1) update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self): def on_cancel(self):
@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'): if self.get('is_return'):
return return
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 0) update_last_purchase_rate(self, is_submit = 0)
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@ -863,104 +689,6 @@ class BuyingController(StockController):
else: else:
validate_item_type(self, "is_purchase_item", "purchase") validate_item_type(self, "is_purchase_item", "purchase")
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
from
`tabBOM` t1, `tab{0}` t2, tabItem t3
where
t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.sourced_by_supplier = 0
and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1)
if not bom_items:
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
return bom_items
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
common_query = """
SELECT
sed.item_code AS rm_item_code,
SUM(sed.qty) AS qty,
sed.description,
sed.stock_uom,
sed.subcontracted_item AS main_item_code,
{serial_no_concat_syntax} AS serial_nos,
{batch_no_concat_syntax} AS batch_nos
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
)
}, (purchase_order, fg_item), as_dict=1)
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
purchase_receipts = frappe.get_all("Purchase Receipt Item",
fields = ["purchase_order", "item_code", "name", "parent"],
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
distinct_purchase_receipts = {}
for pr in purchase_receipts:
key = (pr.purchase_order, pr.item_code, pr.parent)
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
for args, references in iteritems(distinct_purchase_receipts):
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
for data in purchase_receipt_supplied_items:
pr_key = (data.rm_item_code, data.main_item_code, args[0])
if pr_key not in backflushed_raw_materials_map:
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
"qty": 0.0,
"serial_no": [],
"batch_no": [],
"consumed_batch": {}
}))
row = backflushed_raw_materials_map.get(pr_key)
row.qty += data.consumed_qty
for field in ["serial_no", "batch_no"]:
if data.get(field):
row[field].append(data.get(field))
if data.get("batch_no"):
if data.get("batch_no") in row.consumed_batch:
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
else:
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
def get_supplied_items(item_code, purchase_receipt, references):
return frappe.get_all("Purchase Receipt Item Supplied",
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
def get_asset_item_details(asset_items): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message) frappe.throw(error_message)
def get_qty_to_be_received(purchase_orders):
return frappe._dict(frappe.db.sql("""
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
FROM `tabPurchase Order Item` poi
WHERE
poi.`parent` in %s
GROUP BY poi.`item_code`, poi.`parent`
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
""", (purchase_orders)))
def get_non_stock_items(purchase_order, fg_item_code):
return frappe.db.sql("""
SELECT
pois.main_item_code,
pois.rm_item_code,
item.description,
pois.required_qty AS qty,
pois.rate,
1 as non_stock_item,
pois.stock_uom
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
WHERE
pois.`rm_item_code` = item.`name`
AND item.is_stock_item = 0
AND pois.`parent` = %s
AND pois.`main_item_code` = %s
""", (purchase_order, fg_item_code), as_dict=1)
def set_serial_nos(raw_material, consumed_serial_nos, qty):
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
set(get_serial_nos(consumed_serial_nos))
if serial_nos and qty <= len(serial_nos):
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
def get_transferred_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
transferred_batch_qty_map = {}
transferred_batches = frappe.db.sql("""
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code,
sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
sed.item_code
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
transferred_batch_qty_map.setdefault(key, OrderedDict())
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
def get_backflushed_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
backflushed_batch_qty_map = {}
backflushed_batches = frappe.db.sql("""
SELECT
pris.batch_no,
SUM(pris.consumed_qty) AS qty,
pris.rm_item_code AS item_code
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
WHERE
pr.name = pri.parent
AND pri.parent = pris.parent
AND pri.purchase_order = %s
AND pri.item_code = pris.main_item_code
AND pr.docstatus = 1
AND pris.main_item_code = %s
AND pris.batch_no IS NOT NULL
GROUP BY
pris.rm_item_code, pris.batch_no
""", (purchase_order, fg_item), as_dict=1)
for batch_data in backflushed_batches:
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
if not transferred_batches:
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
backflushed_qty = backflushed_batches.get(batch, 0)
available_qty = transferred_qty - backflushed_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
for row in available_batches:
if backflushed_batches.get(row.get('batch'), 0) > 0:
backflushed_batches[row.get('batch')] += row.get('qty')
else:
backflushed_batches[row.get('batch')] = row.get('qty')
return available_batches

View File

@ -501,7 +501,6 @@ class StockController(AccountsController):
check_if_stock_and_account_balance_synced(self.posting_date, check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name) self.company, self.doctype, self.name)
@frappe.whitelist() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):
if isinstance(items, str): if isinstance(items, str):
@ -533,21 +532,75 @@ def make_quality_inspections(doctype, docname, items):
return inspections return inspections
def is_reposting_pending(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
def future_sle_exists(args, sl_entries=None):
key = (args.voucher_type, args.voucher_no)
def future_sle_exists(args): if validate_future_sle_not_exists(args, key, sl_entries):
sl_entries = frappe.get_all("Stock Ledger Entry", return False
elif get_cached_data(args, key):
return True
if not sl_entries:
sl_entries = get_sle_entries_against_voucher(args)
if not sl_entries:
return
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
GROUP BY
item_code, warehouse
""".format(" or ".join(or_conditions)), args, as_dict=1)
for d in data:
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
def validate_future_sle_not_exists(args, key, sl_entries=None):
item_key = ''
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
if not sl_entries and hasattr(frappe.local, 'future_sle'):
if (not frappe.local.future_sle.get(key) or
(item_key and item_key not in frappe.local.future_sle.get(key))):
return True
def get_cached_data(args, key):
if not hasattr(frappe.local, 'future_sle'):
frappe.local.future_sle = {}
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
count = frappe.local.future_sle[key].get(item_key)
return True if (count or count == 0) else False
else:
return frappe.local.future_sle[key]
def get_sle_entries_against_voucher(args):
return frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"], fields=["item_code", "warehouse"],
order_by="creation asc") order_by="creation asc")
if not sl_entries: def get_conditions_to_validate_future_sle(sl_entries):
return
warehouse_items_map = {} warehouse_items_map = {}
for entry in sl_entries: for entry in sl_entries:
if entry.warehouse not in warehouse_items_map: if entry.warehouse not in warehouse_items_map:
@ -561,17 +614,7 @@ def future_sle_exists(args):
f"""warehouse = {frappe.db.escape(warehouse)} f"""warehouse = {frappe.db.escape(warehouse)}
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
return frappe.db.sql(""" return or_conditions
select name
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
limit 1
""".format(" or ".join(or_conditions)), args)
def create_repost_item_valuation_entry(args): def create_repost_item_valuation_entry(args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -0,0 +1,393 @@
import frappe
import copy
from frappe import _
from frappe.utils import flt, cint, get_link_to_form
from collections import defaultdict
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 self.is_subcontracted != 'Yes':
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):
self.__validate_consumed_qty(row)
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_consumed_qty(self, row):
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
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

@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
item.margin_type = None item.margin_type = None
item.margin_rate_or_amount = 0.0 item.margin_rate_or_amount = 0.0
if item.margin_type and item.margin_rate_or_amount: if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
item.margin_type = "Amount"
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
item.precision("margin_rate_or_amount"))
item.rate_with_margin = item.rate
elif item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
rate_with_margin = flt(item.price_list_rate) + flt(margin_value) rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)

View File

@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update
from six import string_types from six import string_types
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records('BOM') test_records = frappe.get_test_records('BOM')
@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase):
def test_subcontractor_sourced_item(self): def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
set_backflush_based_on('Material Transferred for Subcontract')
if not frappe.db.exists('Item', item_code): if not frappe.db.exists('Item', item_code):
make_item(item_code, { make_item(item_code, {

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import getdate, nowdate from frappe.utils import getdate, nowdate, add_days
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet
from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice
@ -16,17 +16,22 @@ class TestProjectProfitability(unittest.TestCase):
make_salary_structure_for_timesheet(emp, company='_Test Company') make_salary_structure_for_timesheet(emp, company='_Test Company')
self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name) self.salary_slip = make_salary_slip(self.timesheet.name)
holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate())
if holidays:
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
self.salary_slip.submit() self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
self.sales_invoice.due_date = nowdate() self.sales_invoice.due_date = nowdate()
self.sales_invoice.submit() self.sales_invoice.submit()
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0)
def test_project_profitability(self): def test_project_profitability(self):
filters = { filters = {
'company': '_Test Company', 'company': '_Test Company',
'start_date': getdate(), 'start_date': add_days(getdate(), -3),
'end_date': getdate() 'end_date': getdate()
} }

View File

@ -122,9 +122,20 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
this.set_from_product_bundle(); this.set_from_product_bundle();
} }
this.toggle_subcontracting_fields();
super.refresh(); super.refresh();
} }
toggle_subcontracting_fields() {
if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) {
this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty',
'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM');
this.frm.set_df_property('supplied_items', 'cannot_add_rows', 1);
this.frm.set_df_property('supplied_items', 'cannot_delete_rows', 1);
}
}
supplier() { supplier() {
var me = this; var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function(){ erpnext.utils.get_party_details(this.frm, null, null, function(){

View File

@ -387,7 +387,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if(this.frm.doc.scan_barcode) { if(this.frm.doc.scan_barcode) {
frappe.call({ frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_serial_or_batch_or_barcode_number", method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
args: { search_value: this.frm.doc.scan_barcode } args: { search_value: this.frm.doc.scan_barcode }
}).then(r => { }).then(r => {
const data = r && r.message; const data = r && r.message;
@ -743,6 +743,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
var me = this; var me = this;
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if (item && item.doctype === 'Purchase Receipt Item Supplied') {
return;
}
if (item && item.serial_no) { if (item && item.serial_no) {
if (!item.item_code) { if (!item.item_code) {
this.frm.trigger("item_code", cdt, cdn); this.frm.trigger("item_code", cdt, cdn);

View File

@ -115,7 +115,7 @@ class Bin(Document):
#Get Transferred Entries #Get Transferred Entries
materials_transferred = frappe.db.sql(""" materials_transferred = frappe.db.sql("""
select select
ifnull(sum(transfer_qty),0) ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
from from
`tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
where where

View File

@ -6,8 +6,8 @@ frappe.listview_settings['Delivery Note'] = {
return [__("Return"), "gray", "is_return,=,Yes"]; return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") { } else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"]; return [__("Closed"), "green", "status,=,Closed"];
} else if (flt(doc.per_returned, 2) === 100) { } else if (doc.status === "Return Issued") {
return [__("Return Issued"), "grey", "per_returned,=,100"]; return [__("Return Issued"), "grey", "status,=,Return Issued"];
} else if (flt(doc.per_billed, 2) < 100) { } else if (flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"]; return [__("To Bill"), "orange", "per_billed,<,100"];
} else if (flt(doc.per_billed, 2) === 100) { } else if (flt(doc.per_billed, 2) === 100) {

View File

@ -18,6 +18,9 @@ class TestItemAlternative(unittest.TestCase):
make_items() make_items()
def test_alternative_item_for_subcontract_rm(self): def test_alternative_item_for_subcontract_rm(self):
frappe.db.set_value('Buying Settings', None,
'backflush_raw_materials_of_subcontract_based_on', 'BOM')
create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC',
qty=5, rate=2000) qty=5, rate=2000)
create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
@ -65,6 +68,8 @@ class TestItemAlternative(unittest.TestCase):
status = True status = True
self.assertEqual(status, True) self.assertEqual(status, True)
frappe.db.set_value('Buying Settings', None,
'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract')
def test_alternative_item_for_production_rm(self): def test_alternative_item_for_production_rm(self):
create_stock_reconciliation(item_code='Alternate Item For A RW 1', create_stock_reconciliation(item_code='Alternate Item For A RW 1',

View File

@ -514,8 +514,7 @@
"oldfieldname": "pr_raw_material_details", "oldfieldname": "pr_raw_material_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Receipt Item Supplied", "options": "Purchase Receipt Item Supplied",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"fieldname": "section_break0", "fieldname": "section_break0",
@ -1149,7 +1148,7 @@
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 01:01:00.754119", "modified": "2021-05-25 00:15:12.239017",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@ -202,6 +202,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.set_consumed_qty_in_po()
def check_next_docstatus(self): def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name submit_rv = frappe.db.sql("""select t1.name
@ -233,6 +234,7 @@ class PurchaseReceipt(BuyingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
@frappe.whitelist() @frappe.whitelist()
def get_current_stock(self): def get_current_stock(self):

View File

@ -335,6 +335,10 @@ class TestPurchaseReceipt(unittest.TestCase):
se2.cancel() se2.cancel()
se3.cancel() se3.cancel()
po.reload() po.reload()
pr2.load_from_db()
pr2.cancel()
po.load_from_db()
po.cancel() po.cancel()
def test_serial_no_supplier(self): def test_serial_no_supplier(self):

View File

@ -1079,6 +1079,10 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
} }
function attach_bom_items(bom_no) { function attach_bom_items(bom_no) {
if (!bom_no) {
return
}
if (check_should_not_attach_bom_items(bom_no)) return if (check_should_not_attach_bom_items(bom_no)) return
frappe.db.get_doc("BOM",bom_no).then(bom => { frappe.db.get_doc("BOM",bom_no).then(bom => {
const {name, items} = bom const {name, items} = bom

View File

@ -74,7 +74,8 @@
"total_amount", "total_amount",
"job_card", "job_card",
"amended_from", "amended_from",
"credit_note" "credit_note",
"is_return"
], ],
"fields": [ "fields": [
{ {
@ -611,6 +612,16 @@
"fieldname": "apply_putaway_rule", "fieldname": "apply_putaway_rule",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply Putaway Rule" "label": "Apply Putaway Rule"
},
{
"default": "0",
"fieldname": "is_return",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Return",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -618,7 +629,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-24 11:32:23.904307", "modified": "2021-05-26 17:07:58.015737",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -97,7 +97,6 @@ class StockEntry(StockController):
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
self.update_work_order() self.update_work_order()
self.validate_purchase_order() self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items() self.update_purchase_order_supplied_items()
self.make_gl_entries() self.make_gl_entries()
@ -117,8 +116,6 @@ class StockEntry(StockController):
self.set_material_request_transfer_status('Completed') self.set_material_request_transfer_status('Completed')
def on_cancel(self): def on_cancel(self):
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items() self.update_purchase_order_supplied_items()
if self.work_order and self.purpose == "Material Consumption for Manufacture": if self.work_order and self.purpose == "Material Consumption for Manufacture":
@ -1008,10 +1005,12 @@ class StockEntry(StockController):
if self.purchase_order and self.purpose == "Send to Subcontractor": if self.purchase_order and self.purpose == "Send to Subcontractor":
#Get PO Supplied Items Details #Get PO Supplied Items Details
item_wh = frappe._dict(frappe.db.sql(""" item_wh = frappe._dict(frappe.db.sql("""
select rm_item_code, reserve_warehouse SELECT
from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup rm_item_code, reserve_warehouse
where po.name = poitemsup.parent FROM
and po.name = %s""",self.purchase_order)) `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
WHERE
po.name = poitemsup.parent and po.name = %s """,self.purchase_order))
for item in itervalues(item_dict): for item in itervalues(item_dict):
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
@ -1294,7 +1293,8 @@ class StockEntry(StockController):
item_dict[item]["qty"] = 0 item_dict[item]["qty"] = 0
# delete items with 0 qty # delete items with 0 qty
for item in item_dict.keys(): list_of_items = item_dict.keys()
for item in list_of_items:
if not item_dict[item]["qty"]: if not item_dict[item]["qty"]:
del item_dict[item] del item_dict[item]
@ -1347,7 +1347,7 @@ class StockEntry(StockController):
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item", for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name"]: "expense_account", "description", "item_name", "serial_no", "batch_no"]:
if item_dict[d].get(field): if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field)) se_child.set(field, item_dict[d].get(field))
@ -1400,6 +1400,9 @@ class StockEntry(StockController):
.format(item.batch_no, item.item_code)) .format(item.batch_no, item.item_code))
def update_purchase_order_supplied_items(self): def update_purchase_order_supplied_items(self):
if (self.purchase_order and
(self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)):
#Get PO Supplied Items Details #Get PO Supplied Items Details
item_wh = frappe._dict(frappe.db.sql(""" item_wh = frappe._dict(frappe.db.sql("""
select rm_item_code, reserve_warehouse select rm_item_code, reserve_warehouse
@ -1407,19 +1410,9 @@ class StockEntry(StockController):
where po.name = poitemsup.parent where po.name = poitemsup.parent
and po.name = %s""", self.purchase_order)) and po.name = %s""", self.purchase_order))
#Update Supplied Qty in PO Supplied Items supplied_items = get_supplied_items(self.purchase_order)
for name, item in supplied_items.items():
frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos frappe.db.set_value('Purchase Order Item Supplied', name, item)
SET
pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
FROM
`tabStock Entry Detail` sed, `tabStock Entry` se
WHERE
pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code
AND pos.parent = se.purchase_order AND sed.docstatus = 1
AND se.name = sed.parent and se.purchase_order = %(po)s
), 0)
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
#Update reserved sub contracted quantity in bin based on Supplied Item Details and #Update reserved sub contracted quantity in bin based on Supplied Item Details and
for d in self.get("items"): for d in self.get("items"):
@ -1480,7 +1473,7 @@ class StockEntry(StockController):
cond += """ WHEN (parent = %s and name = %s) THEN %s cond += """ WHEN (parent = %s and name = %s) THEN %s
""" %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty)
if cond and stock_entries_child_list: if stock_entries_child_list:
frappe.db.sql(""" UPDATE `tabStock Entry Detail` frappe.db.sql(""" UPDATE `tabStock Entry Detail`
SET SET
transferred_qty = CASE {cond} END transferred_qty = CASE {cond} END
@ -1751,3 +1744,30 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None):
format(max_retain_qty, batch_no, item_code), alert=True) format(max_retain_qty, batch_no, item_code), alert=True)
sample_quantity = qty_diff sample_quantity = qty_diff
return sample_quantity return sample_quantity
def get_supplied_items(purchase_order):
fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`',
'`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`']
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]]
supplied_item_details = {}
for row in frappe.get_all('Stock Entry', fields = fields, filters = filters):
if not row.po_detail:
continue
key = row.po_detail
if key not in supplied_item_details:
supplied_item_details.setdefault(key,
frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0}))
supplied_item = supplied_item_details[key]
if row.is_return:
supplied_item.returned_qty += row.transfer_qty
else:
supplied_item.supplied_qty += row.transfer_qty
supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
return supplied_item_details

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, date_diff from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, cint
from frappe.model.document import Document from frappe.model.document import Document
from datetime import date from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@ -108,17 +108,18 @@ class StockLedgerEntry(Document):
self.stock_uom = item_det.stock_uom self.stock_uom = item_det.stock_uom
def check_stock_frozen_date(self): def check_stock_frozen_date(self):
stock_frozen_upto = frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto') or '' stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings')
if stock_frozen_upto:
stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role')
if getdate(self.posting_date) <= getdate(stock_frozen_upto) and not stock_auth_role in frappe.get_roles():
frappe.throw(_("Stock transactions before {0} are frozen").format(formatdate(stock_frozen_upto)), StockFreezeError)
stock_frozen_upto_days = int(frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto_days') or 0) if stock_settings.stock_frozen_upto:
if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
and stock_settings.stock_auth_role not in frappe.get_roles()):
frappe.throw(_("Stock transactions before {0} are frozen")
.format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError)
stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days)
if stock_frozen_upto_days: if stock_frozen_upto_days:
stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role')
older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today())
if older_than_x_days_ago and not stock_auth_role in frappe.get_roles(): if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles():
frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError)
def scrub_posting_time(self): def scrub_posting_time(self):

View File

@ -22,6 +22,7 @@ _exceptions = frappe.local('stockledger_exceptions')
# _exceptions = [] # _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries: if sl_entries:
from erpnext.stock.utils import update_bin from erpnext.stock.utils import update_bin
@ -30,6 +31,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
validate_cancellation(sl_entries) validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
args = get_args_for_future_sle(sl_entries[0])
future_sle_exists(args, sl_entries)
for sle in sl_entries: for sle in sl_entries:
if sle.serial_no: if sle.serial_no:
validate_serial_no(sle) validate_serial_no(sle)
@ -53,6 +57,14 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict() args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher) update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def get_args_for_future_sle(row):
return frappe._dict({
'voucher_type': row.get('voucher_type'),
'voucher_no': row.get('voucher_no'),
'posting_date': row.get('posting_date'),
'posting_time': row.get('posting_time')
})
def validate_serial_no(sle): def validate_serial_no(sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for sn in get_serial_nos(sle.serial_no): for sn in get_serial_nos(sle.serial_no):
@ -472,7 +484,7 @@ class update_entries_after(object):
frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes':
doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
doc.update_valuation_rate(reset_outgoing_rate=False) doc.update_valuation_rate(reset_outgoing_rate=False)
for d in (doc.items + doc.supplied_items): for d in (doc.items + doc.supplied_items):

View File

@ -177,7 +177,7 @@ def get_bin(item_code, warehouse):
return bin_obj return bin_obj
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
is_stock_item = frappe.db.get_value('Item', args.get("item_code"), 'is_stock_item') is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item: if is_stock_item:
bin = get_bin(args.get("item_code"), args.get("warehouse")) bin = get_bin(args.get("item_code"), args.get("warehouse"))
bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher) bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher)

View File

@ -13,9 +13,11 @@
{{ doc.items_preview }} {{ doc.items_preview }}
</div> </div>
</div> </div>
{% if doc.get('grand_total') %}
<div class="col-sm-3 text-right bold"> <div class="col-sm-3 text-right bold">
{{ doc.get_formatted("grand_total") }} {{ doc.get_formatted("grand_total") }}
</div> </div>
{% endif %}
</div> </div>
<a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a> <a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a>
</div> </div>

View File

@ -0,0 +1,877 @@
from __future__ import unicode_literals
import frappe
import unittest
import copy
from frappe.utils import cint
from collections import defaultdict
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry,
make_purchase_receipt, make_purchase_invoice, get_materials_from_supplier)
class TestSubcontracting(unittest.TestCase):
def setUp(self):
make_subcontract_items()
make_raw_materials()
make_bom_for_subcontracted_items()
def test_po_with_bom(self):
'''
- Set backflush based on BOM
- Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times.
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Create purchase receipt against the PO and check serial nos and batch no.
'''
set_backflush_based_on('BOM')
item_code = 'Subcontracted Item SA1'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100},
{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 5},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 5},
{'item_code': 'Subcontracted SRM Item 1', 'qty': 6},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 6},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 6}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
for field in ['qty', 'serial_no', 'batch_no']:
if value.get(field):
transfer, consumed = (transferred_detais.get(field), value.get(field))
if field == 'serial_no':
transfer, consumed = (sorted(transfer), sorted(consumed))
self.assertEqual(transfer, consumed)
def test_po_with_material_transfer(self):
'''
- Set backflush based on Material Transfer
- Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5.
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5.
- Create partial purchase receipt against the PO and check serial nos and batch no.
'''
set_backflush_based_on('Material Transferred for Subcontract')
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100},
{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
{'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'},
{'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name
make_stock_transfer_entry(po_no = po.name,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.remove(pr1.items[1])
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
for field in ['qty', 'serial_no', 'batch_no']:
if value.get(field):
self.assertEqual(value.get(field), transferred_detais.get(field))
pr2 = make_purchase_receipt(po.name)
pr2.submit()
for key, value in get_supplied_items(pr2).items():
transferred_detais = itemwise_details.get(key)
for field in ['qty', 'serial_no', 'batch_no']:
if value.get(field):
self.assertEqual(value.get(field), transferred_detais.get(field))
def test_subcontract_with_same_components_different_fg(self):
'''
- Set backflush based on Material Transfer
- Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3.
- Transfer the components from Stores to Supplier warehouse with serial nos.
- Transfer extra qty of components for the item Subcontracted Item SA2.
- Create partial purchase receipt against the PO and check serial nos and batch no.
'''
set_backflush_based_on('Material Transferred for Subcontract')
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100},
{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name
make_stock_transfer_entry(po_no = po.name,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 3
pr1.remove(pr1.items[1])
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 4)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4]))
pr2 = make_purchase_receipt(po.name)
pr2.items[0].qty = 2
pr2.remove(pr2.items[1])
pr2.submit()
for key, value in get_supplied_items(pr2).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 2)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6]))
pr3 = make_purchase_receipt(po.name)
pr3.submit()
for key, value in get_supplied_items(pr3).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 6)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12]))
def test_return_non_consumed_materials(self):
'''
- Set backflush based on Material Transfer
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the components from Stores to Supplier warehouse with serial nos.
- Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
- Create purchase receipt for full qty against the PO and change the qty of raw material.
- After that return the non consumed material back to the store from supplier's warehouse.
'''
set_backflush_based_on('Material Transferred for Subcontract')
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.save()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].serial_no = '\n'.join(sorted(
itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5]
))
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 5)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5]))
po.load_from_db()
self.assertEqual(po.supplied_items[0].consumed_qty, 5)
doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items])
self.assertEqual(doc.items[0].qty, 1)
self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC')
self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC')
self.assertEqual(get_serial_nos(doc.items[0].serial_no),
itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6])
def test_item_with_batch_based_on_bom(self):
'''
- Set backflush based on BOM
- Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer the components in multiple batches.
- Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the purchase receipt.
'''
set_backflush_based_on('BOM')
item_code = 'Subcontracted Item SA4'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 1}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 2
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 2
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 2
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def test_item_with_batch_based_on_material_transfer(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer the components in multiple batches with extra 2 qty for the batched item.
- Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA4'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 2
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
qty = 4 if key != 'Subcontracted SRM Item 3' else 6
self.assertEqual(value.qty, qty)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 2
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 2
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def test_partial_transfer_serial_no_components_based_on_material_transfer(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the partial components from Stores to Supplier warehouse with serial nos.
- Create partial purchase receipt against the PO and change the qty manually.
- Transfer the remaining components from Stores to Supplier warehouse with serial nos.
- Create purchase receipt for remaining qty against the PO and change the qty manually.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA2'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 5
pr1.save()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
pr1.load_from_db()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no'])
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
def test_incorrect_serial_no_components_based_on_material_transfer(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the serialized componenets to the supplier.
- Create purchase receipt and change the serial no which is not transferred.
- System should throw the error and not allowed to save the purchase receipt.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA2'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.save()
pr1.supplied_items[0].serial_no = 'ABCD'
self.assertRaises(frappe.ValidationError, pr1.save)
pr1.delete()
def test_partial_transfer_batch_based_on_material_transfer(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA6.
- Transfer the partial components from Stores to Supplier warehouse with batch.
- Create partial purchase receipt against the PO and change the qty manually.
- Transfer the remaining components from Stores to Supplier warehouse with batch.
- Create purchase receipt for remaining qty against the PO and change the qty manually.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA6'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 5
pr1.save()
transferred_batch_no = ''
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
transferred_batch_no = details.batch_no
self.assertEqual(value.batch_no, details.batch_no)
pr1.load_from_db()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer the components in multiple batches with extra 2 qty for the batched item.
- Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA4'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
qty = 4 if key != 'Subcontracted SRM Item 3' else 6
self.assertEqual(value.qty, qty)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.items[0].qty = 2
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the partial components from Stores to Supplier warehouse with serial nos.
- Create partial purchase receipt against the PO and change the qty manually.
- Transfer the remaining components from Stores to Supplier warehouse with serial nos.
- Create purchase receipt for remaining qty against the PO and change the qty manually.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA2'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 5
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
pr1.load_from_db()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no'])
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA6.
- Transfer the partial components from Stores to Supplier warehouse with batch.
- Create partial purchase receipt against the PO and change the qty manually.
- Transfer the remaining components from Stores to Supplier warehouse with batch.
- Create purchase receipt for remaining qty against the PO and change the qty manually.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA6'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 5
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
transferred_batch_no = ''
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
transferred_batch_no = details.batch_no
self.assertEqual(value.batch_no, details.batch_no)
pr1.load_from_db()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
def test_item_with_batch_based_on_bom_for_purchase_invoice(self):
'''
- Set backflush based on BOM
- Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer the components in multiple batches.
- Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the purchase receipt.
'''
set_backflush_based_on('BOM')
item_code = 'Subcontracted Item SA4'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 1}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def add_second_row_in_pr(pr):
item_dict = {}
for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom',
'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']:
item_dict[column] = pr.items[0].get(column)
pr.append('items', item_dict)
pr.set_missing_values()
def get_supplied_items(pr_doc):
supplied_items = {}
for row in pr_doc.get('supplied_items'):
if row.rm_item_code not in supplied_items:
supplied_items.setdefault(row.rm_item_code,
frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)}))
details = supplied_items[row.rm_item_code]
update_item_details(row, details)
return supplied_items
def make_stock_in_entry(**args):
args = frappe._dict(args)
items = {}
for row in args.rm_items:
row = frappe._dict(row)
doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC',
item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100)
if row.item_code not in items:
items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)}))
child_row = doc.items[0]
details = items[child_row.item_code]
update_item_details(child_row, details)
return items
def update_item_details(child_row, details):
details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail'
else child_row.get('consumed_qty'))
if child_row.serial_no:
details.serial_no.extend(get_serial_nos(child_row.serial_no))
if child_row.batch_no:
details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty'))
def make_stock_transfer_entry(**args):
args = frappe._dict(args)
items = []
for row in args.rm_items:
row = frappe._dict(row)
item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code,
'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100,
'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'}
item_details = args.itemwise_details.get(row.item_code)
if item_details and item_details.serial_no:
serial_nos = item_details.serial_no[0:cint(row.qty)]
item['serial_no'] = '\n'.join(serial_nos)
item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
if item_details and item_details.batch_no:
for batch_no, batch_qty in item_details.batch_no.items():
if batch_qty >= row.qty:
item['batch_no'] = batch_no
item_details.batch_no[batch_no] -= row.qty
break
items.append(item)
ste_dict = make_rm_stock_entry(args.po_no, items)
doc = frappe.get_doc(ste_dict)
doc.insert()
doc.submit()
return doc
def make_subcontract_items():
sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {},
'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'},
'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}}
for item, properties in sub_contracted_items.items():
if not frappe.db.exists('Item', item):
properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1})
make_item(item, properties)
def make_raw_materials():
raw_materials = {'Subcontracted SRM Item 1': {},
'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'},
'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'},
'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'},
'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}}
for item, properties in raw_materials.items():
if not frappe.db.exists('Item', item):
properties.update({'is_stock_item': 1})
make_item(item, properties)
def make_bom_for_subcontracted_items():
boms = {
'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'],
'Subcontracted Item SA2': ['Subcontracted SRM Item 2'],
'Subcontracted Item SA3': ['Subcontracted SRM Item 2'],
'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'],
'Subcontracted Item SA5': ['Subcontracted SRM Item 5'],
'Subcontracted Item SA6': ['Subcontracted SRM Item 3']
}
for item_code, raw_materials in boms.items():
if not frappe.db.exists('BOM', {'item': item_code}):
make_bom(item=item_code, raw_materials=raw_materials, rate=100)
def set_backflush_based_on(based_on):
frappe.db.set_value('Buying Settings', None,
'backflush_raw_materials_of_subcontract_based_on', based_on)