diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index f07fc64737c..c2ba0b43c6e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -208,7 +208,7 @@ def set_address_details( ) if company_address: - party_details.update({"company_address": company_address}) + party_details.company_address = company_address else: party_details.update(get_company_address(company)) @@ -220,12 +220,37 @@ def set_address_details( get_regional_address_details(party_details, doctype, company) elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: - if party_details.company_address: - party_details["shipping_address"] = shipping_address or party_details["company_address"] - party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) + if shipping_address: party_details.update( - get_fetch_values(doctype, "shipping_address", party_details.shipping_address) + { + "shipping_address": shipping_address, + "shipping_address_display": get_address_display(shipping_address), + **get_fetch_values(doctype, "shipping_address", shipping_address), + } ) + + if party_details.company_address: + # billing address + party_details.update( + { + "billing_address": party_details.company_address, + "billing_address_display": ( + party_details.company_address_display or get_address_display(party_details.company_address) + ), + **get_fetch_values(doctype, "billing_address", party_details.company_address), + } + ) + + # shipping address - if not already set + if not party_details.shipping_address: + party_details.update( + { + "shipping_address": party_details.billing_address, + "shipping_address_display": party_details.billing_address_display, + **get_fetch_values(doctype, "shipping_address", party_details.billing_address), + } + ) + get_regional_address_details(party_details, doctype, company) return party_details.get(billing_address_field), party_details.shipping_address_name diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 17b45b29707..3cc25580013 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -15,9 +15,12 @@ frappe.ui.form.on("Request for Quotation",{ frm.fields_dict["suppliers"].grid.get_field("contact").get_query = function(doc, cdt, cdn) { let d = locals[cdt][cdn]; return { - query: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_contacts", - filters: {'supplier': d.supplier} - } + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { + link_doctype: "Supplier", + link_name: d.supplier || "" + } + }; } }, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 24555961666..45a2ad43e7c 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -287,18 +287,6 @@ def get_list_context(context=None): return list_context -@frappe.whitelist() -@frappe.validate_and_sanitize_search_inputs -def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql( - """select `tabContact`.name from `tabContact`, `tabDynamic Link` - where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s - and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent - limit %(start)s, %(page_len)s""", - {"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")}, - ) - - @frappe.whitelist() def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None): def postprocess(source, target_doc): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9c31ebfbb33..d0f81c2b270 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -86,6 +86,7 @@ class BuyingController(StockController, Subcontracting): company=self.company, party_address=self.get("supplier_address"), shipping_address=self.get("shipping_address"), + company_address=self.get("billing_address"), fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"), ignore_permissions=self.flags.ignore_permissions, ) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e90a4f62411..2eea0bde8c6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -33,6 +33,10 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass +class BatchExpiredError(frappe.ValidationError): + pass + + class StockController(AccountsController): def validate(self): super(StockController, self).validate() @@ -74,6 +78,10 @@ class StockController(AccountsController): def validate_serialized_batch(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + is_material_issue = False + if self.doctype == "Stock Entry" and self.purpose == "Material Issue": + is_material_issue = True + for d in self.get("items"): if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no: serial_nos = frappe.get_all( @@ -90,6 +98,9 @@ class StockController(AccountsController): ) ) + if is_material_issue: + continue + if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") @@ -97,7 +108,8 @@ class StockController(AccountsController): frappe.throw( _("Row #{0}: The batch {1} has already expired.").format( d.idx, get_link_to_form("Batch", d.get("batch_no")) - ) + ), + BatchExpiredError, ) def clean_serial_nos(self): diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index dd6c802aab4..4cf29866326 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -189,8 +189,8 @@ class BOM(WebsiteGenerator): self.validate_transfer_against() self.set_routing_operations() self.validate_operations() - self.update_exploded_items(save=False) self.calculate_cost() + self.update_exploded_items(save=False) self.update_stock_qty() self.validate_scrap_items() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index edb97937f02..3c91c6d4d9b 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -555,6 +555,34 @@ class TestBOM(FrappeTestCase): bom.reload() self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name) + def test_exploded_items_rate(self): + rm_item = make_item( + properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} + ).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_save=True) + + bom.rm_cost_as_per = "Last Purchase Rate" + bom.save() + self.assertEqual(bom.items[0].base_rate, 89) + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + + bom.rm_cost_as_per = "Price List" + bom.save() + self.assertEqual(bom.items[0].base_rate, 0.0) + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + + bom.rm_cost_as_per = "Valuation Rate" + bom.save() + self.assertEqual(bom.items[0].base_rate, 99) + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + + bom.submit() + self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) + def get_default_bom(item_code="_Test FG Item 2"): return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 3406215cbbb..33ca2d93d8f 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -185,6 +185,7 @@ "in_list_view": 1, "label": "Rate", "options": "currency", + "read_only": 1, "reqd": 1 }, { @@ -298,7 +299,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-24 16:57:57.020232", + "modified": "2022-07-28 10:20:51.559010", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index b7e3b4280d6..b6567ec15d8 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -482,7 +482,6 @@ class ProductionPlan(Document): "bom_no", "stock_uom", "bom_level", - "production_plan_item", "schedule_date", ]: if row.get(field): @@ -639,6 +638,9 @@ class ProductionPlan(Document): def get_sub_assembly_items(self, manufacturing_type=None): self.sub_assembly_items = [] for row in self.po_items: + if not row.item_code: + frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) + bom_data = [] get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index fa40b3d56ce..cbae275031e 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -11,8 +11,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_warehouse_list, ) from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError +from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as make_se_from_wo from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, @@ -536,9 +537,6 @@ class TestProductionPlan(FrappeTestCase): Test Prod Plan impact via: SO -> Prod Plan -> WO -> SE -> SE (cancel) """ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_se_from_wo, - ) make_stock_entry( item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 @@ -581,9 +579,6 @@ class TestProductionPlan(FrappeTestCase): def test_production_plan_pending_qty_independent_items(self): "Test Prod Plan impact if items are added independently (no from SO or MR)." from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record - from erpnext.manufacturing.doctype.work_order.work_order import ( - make_stock_entry as make_se_from_wo, - ) make_stock_entry( item_code="Raw Material Item 1", target="Work In Progress - _TC", qty=2, basic_rate=100 @@ -679,6 +674,57 @@ class TestProductionPlan(FrappeTestCase): for po_item, subassy_item in zip(pp.po_items, pp.sub_assembly_items): self.assertEqual(po_item.name, subassy_item.production_plan_item) + def test_produced_qty_for_multi_level_bom_item(self): + # Create Items and BOMs + rm_item = make_item(properties={"is_stock_item": 1}).name + sub_assembly_item = make_item(properties={"is_stock_item": 1}).name + fg_item = make_item(properties={"is_stock_item": 1}).name + + make_stock_entry( + item_code=rm_item, + qty=60, + to_warehouse="Work In Progress - _TC", + rate=99, + purpose="Material Receipt", + ) + + make_bom(item=sub_assembly_item, raw_materials=[rm_item], rm_qty=3) + make_bom(item=fg_item, raw_materials=[sub_assembly_item], rm_qty=4) + + # Step - 1: Create Production Plan + pln = create_production_plan(item_code=fg_item, planned_qty=5, skip_getting_mr_items=1) + pln.get_sub_assembly_items() + + # Step - 2: Create Work Orders + pln.make_work_order() + work_orders = frappe.get_all("Work Order", filters={"production_plan": pln.name}, pluck="name") + sa_wo = fg_wo = None + for work_order in work_orders: + wo_doc = frappe.get_doc("Work Order", work_order) + if wo_doc.production_plan_item: + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Finished Goods - _TC"} + ) + fg_wo = wo_doc.name + else: + wo_doc.update( + {"wip_warehouse": "Work In Progress - _TC", "fg_warehouse": "Work In Progress - _TC"} + ) + sa_wo = wo_doc.name + wo_doc.submit() + + # Step - 3: Complete Work Orders + se = frappe.get_doc(make_se_from_wo(sa_wo, "Manufacture")) + se.submit() + + se = frappe.get_doc(make_se_from_wo(fg_wo, "Manufacture")) + se.submit() + + # Step - 4: Check Production Plan Item Produced Qty + pln.load_from_db() + self.assertEqual(pln.status, "Completed") + self.assertEqual(pln.po_items[0].produced_qty, 5) + def create_production_plan(**args): """ diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 61f650b64b6..d7892be082a 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1,7 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.provide('erpnext.accounts.dimensions'); erpnext.TransactionController = erpnext.taxes_and_totals.extend({ setup: function() { @@ -910,24 +909,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ set_party_account(set_pricing); }); - // Get default company billing address in Purchase Invoice, Order and Receipt - if (this.frm.doc.company && frappe.meta.get_docfield(this.frm.doctype, "billing_address")) { - frappe.call({ - method: "erpnext.setup.doctype.company.company.get_default_company_address", - args: {name: this.frm.doc.company, existing_address: this.frm.doc.billing_address || ""}, - debounce: 2000, - callback: function(r) { - if (r.message) { - me.frm.set_value("billing_address", r.message); - } else { - if (frappe.meta.get_docfield(me.frm.doctype, 'company_address')) { - me.frm.set_value("company_address", ""); - } - } - } - }); - } - } else { set_party_account(set_pricing); } diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a492b32a9f6..58594b0a13d 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -3,25 +3,14 @@ frappe.provide("erpnext.utils"); +const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']; +const PURCHASE_DOCTYPES = ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; + erpnext.utils.get_party_details = function(frm, method, args, callback) { if (!method) { method = "erpnext.accounts.party.get_party_details"; } - if (args) { - if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) { - if (frm.doc.company_address && (!args.company_address)) { - args.company_address = frm.doc.company_address; - } - } - - if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) { - if (frm.doc.shipping_address && (!args.shipping_address)) { - args.shipping_address = frm.doc.shipping_address; - } - } - } - if (!args) { if ((frm.doctype != "Purchase Order" && frm.doc.customer) || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { @@ -45,41 +34,44 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { }; } - if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) { - if (!args) { + if (!args) { + if (in_list(SALES_DOCTYPES, frm.doc.doctype)) { args = { party: frm.doc.customer || frm.doc.party_name, party_type: 'Customer' - } - } - if (frm.doc.company_address && (!args.company_address)) { - args.company_address = frm.doc.company_address; + }; } - if (frm.doc.shipping_address_name &&(!args.shipping_address_name)) { - args.shipping_address_name = frm.doc.shipping_address_name; - } - } - - if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) { - if (!args) { + if (in_list(PURCHASE_DOCTYPES, frm.doc.doctype)) { args = { party: frm.doc.supplier, party_type: 'Supplier' - } - } - - if (frm.doc.shipping_address && (!args.shipping_address)) { - args.shipping_address = frm.doc.shipping_address; + }; } } - if (args) { - args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; - args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); + if (!args || !args.party) return; + + args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; + args.fetch_payment_terms_template = cint(!frm.doc.ignore_default_payment_terms_template); + } + + if (in_list(SALES_DOCTYPES, frm.doc.doctype)) { + if (!args.company_address && frm.doc.company_address) { + args.company_address = frm.doc.company_address; } } - if (!args || !args.party) return; + + if (in_list(PURCHASE_DOCTYPES, frm.doc.doctype)) { + if (!args.company_address && frm.doc.billing_address) { + args.company_address = frm.doc.billing_address; + } + + if (!args.shipping_address && frm.doc.shipping_address) { + args.shipping_address = frm.doc.shipping_address; + } + } + if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index e67504a5f5a..998c8e35c79 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -366,8 +366,14 @@ def make_new_batch(**args): "doctype": "Batch", "batch_id": args.batch_id, "item": args.item_code, + "expiry_date": args.expiry_date, } - ).insert() + ) + + if args.expiry_date: + batch.expiry_date = args.expiry_date + + batch.insert() except frappe.DuplicateEntryError: batch = frappe.get_doc("Batch", args.batch_id) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index aa217441c0b..5a04e7d2eec 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -786,10 +786,8 @@ { "fieldname": "expense_account", "fieldtype": "Link", - "hidden": 1, "label": "Expense Account", - "options": "Account", - "read_only": 1 + "options": "Account" }, { "fieldname": "accounting_dimensions_section", @@ -994,7 +992,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-04-11 13:07:32.061402", + "modified": "2022-07-28 19:27:54.880781", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index ad84c36b811..9cc8e237b6a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -581,18 +581,23 @@ frappe.ui.form.on('Stock Entry', { }, add_to_transit: function(frm) { - if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { - frm.set_value('to_warehouse', ''); + if(frm.doc.purpose=='Material Transfer') { + var filters = { + 'is_group': 0, + 'company': frm.doc.company + } + + if(frm.doc.add_to_transit){ + filters['warehouse_type'] = 'Transit'; + frm.set_value('to_warehouse', ''); + frm.trigger('set_transit_warehouse'); + } + frm.fields_dict.to_warehouse.get_query = function() { return { - filters:{ - 'warehouse_type' : 'Transit', - 'is_group': 0, - 'company': frm.doc.company - } + filters:filters }; }; - frm.trigger('set_transit_warehouse'); } }, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6156a6458b6..64ea0435e16 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, nowdate, nowtime +from frappe.utils import add_days, flt, nowdate, nowtime, today from six import iteritems from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -1546,6 +1546,31 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(obj.items[index].basic_rate, 200) self.assertEqual(obj.items[index].basic_amount, 2000) + def test_batch_expiry(self): + from erpnext.controllers.stock_controller import BatchExpiredError + from erpnext.stock.doctype.batch.test_batch import make_new_batch + + item_code = "Test Batch Expiry Test Item - 001" + item_doc = create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + + item_doc.has_batch_no = 1 + item_doc.save() + + batch = make_new_batch( + batch_id=frappe.generate_hash("", 5), item_code=item_doc.name, expiry_date=add_days(today(), -1) + ) + + se = make_stock_entry( + item_code=item_code, + purpose="Material Receipt", + qty=4, + to_warehouse="_Test Warehouse - _TC", + batch_no=batch.name, + do_not_save=True, + ) + + self.assertRaises(BatchExpiredError, se.save) + def make_serialized_item(**args): args = frappe._dict(args)