diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 5ba0131f6c0..a33892e044d 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document): frappe.scrub(row.party_type): row.party, "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "update_stock": 0, + "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559 "invoice_number": row.invoice_number, "disable_rounded_total": 1 }) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 6700e9b975d..3eaf6a28f37 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -1,11 +1,7 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe -from frappe.cache_manager import clear_doctype_cache -from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( create_dimension, @@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( get_temporary_opening_account, ) +from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] -class TestOpeningInvoiceCreationTool(unittest.TestCase): - def setUp(self): +class TestOpeningInvoiceCreationTool(ERPNextTestCase): + @classmethod + def setUpClass(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() create_dimension() + return super().setUpClass() def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): doc = frappe.get_single("Opening Invoice Creation Tool") @@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): return doc.make_invoices() def test_opening_sales_invoice_creation(self): - property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") - try: - invoices = self.make_invoices(company="_Test Opening Invoice Company") + invoices = self.make_invoices(company="_Test Opening Invoice Company") - self.assertEqual(len(invoices), 2) - expected_value = { - "keys": ["customer", "outstanding_amount", "status"], - 0: ["_Test Customer", 300, "Overdue"], - 1: ["_Test Customer 1", 250, "Overdue"], - } - self.check_expected_values(invoices, expected_value) + self.assertEqual(len(invoices), 2) + expected_value = { + "keys": ["customer", "outstanding_amount", "status"], + 0: ["_Test Customer", 300, "Overdue"], + 1: ["_Test Customer 1", 250, "Overdue"], + } + self.check_expected_values(invoices, expected_value) - si = frappe.get_doc("Sales Invoice", invoices[0]) + si = frappe.get_doc("Sales Invoice", invoices[0]) - # Check if update stock is not enabled - self.assertEqual(si.update_stock, 0) - - finally: - property_setter.delete() - clear_doctype_cache("Sales Invoice") + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 80f61d4f02e..a9134022b53 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1078,7 +1078,7 @@ def get_outstanding_reference_documents(args): if d.voucher_type in ("Purchase Invoice"): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") - # Get all SO / PO which are not fully billed or aginst which full advance not paid + # Get all SO / PO which are not fully billed or against which full advance not paid orders_to_be_billed = [] if (args.get("party_type") != "Student"): orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 2d6bef5bca5..9d585411582 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice): self.paid_amount = 0 def set_account_for_mode_of_payment(self): - self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] for pay in self.payments: if not pay.account: pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index b5909447dc8..1d30934df92 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) - validate_account_head(tax, doc) + validate_account_head(tax.idx, tax.account_head, doc.company) validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 4f4f180e542..907964720ff 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) def validate_party_accounts(doc): - + from erpnext.controllers.accounts_controller import validate_account_head companies = [] for account in doc.get("accounts"): @@ -331,6 +331,9 @@ def validate_party_accounts(doc): if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) + # validate if account is mapped for same company + validate_account_head(account.idx, account.account, account.company) + @frappe.whitelist() def get_due_date(posting_date, party_type, party, company=None, bill_date=None): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index d554d52a718..3fe6b2d0d5d 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -68,6 +68,28 @@ frappe.ui.form.on('Asset Repair', { }); frappe.ui.form.on('Asset Repair Consumed Item', { + item_code: function(frm, cdt, cdn) { + var item = locals[cdt][cdn]; + + let item_args = { + 'item_code': item.item_code, + 'warehouse': frm.doc.warehouse, + 'qty': item.consumed_quantity, + 'serial_no': item.serial_no, + 'company': frm.doc.company + }; + + frappe.call({ + method: 'erpnext.stock.utils.get_incoming_rate', + args: { + args: item_args + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message); + } + }); + }, + consumed_quantity: function(frm, cdt, cdn) { var row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index f63add12356..4685a09db63 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -13,12 +13,10 @@ ], "fields": [ { - "fetch_from": "item.valuation_rate", "fieldname": "valuation_rate", "fieldtype": "Currency", "in_list_view": 1, - "label": "Valuation Rate", - "read_only": 1 + "label": "Valuation Rate" }, { "fieldname": "consumed_quantity", @@ -49,7 +47,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-11-11 18:23:00.492483", + "modified": "2022-02-08 17:37:20.028290", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3513b0ad663..f146071c0bd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1567,13 +1567,12 @@ def validate_taxes_and_charges(tax): tax.rate = None -def validate_account_head(tax, doc): - company = frappe.get_cached_value('Account', - tax.account_head, 'company') +def validate_account_head(idx, account, company): + account_company = frappe.get_cached_value('Account', account, 'company') - if company != doc.company: + if account_company != company: frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) + .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) def validate_cost_center(tax, doc): diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index d88e10a564c..2359815813d 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -9,6 +9,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_sales_orders, get_warehouse_list, ) +from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError 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.stock_entry.test_stock_entry import make_stock_entry @@ -466,26 +467,29 @@ class TestProductionPlan(ERPNextTestCase): bom = make_bom(item=item, raw_materials=raw_materials) # Create Production Plan - pln = create_production_plan(item_code=bom.item, planned_qty=10) + pln = create_production_plan(item_code=bom.item, planned_qty=5) # All the created Work Orders wo_list = [] - # Create and Submit 1st Work Order for 5 qty - create_work_order(item, pln, 5) + # Create and Submit 1st Work Order for 3 qty + create_work_order(item, pln, 3) + pln.reload() + self.assertEqual(pln.po_items[0].ordered_qty, 3) + + # Create and Submit 2nd Work Order for 2 qty + create_work_order(item, pln, 2) pln.reload() self.assertEqual(pln.po_items[0].ordered_qty, 5) - # Create and Submit 2nd Work Order for 3 qty - create_work_order(item, pln, 3) - pln.reload() - self.assertEqual(pln.po_items[0].ordered_qty, 8) + # Overproduction + self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2) # Cancel 1st Work Order wo1 = frappe.get_doc("Work Order", wo_list[0]) wo1.cancel() pln.reload() - self.assertEqual(pln.po_items[0].ordered_qty, 3) + self.assertEqual(pln.po_items[0].ordered_qty, 2) # Cancel 2nd Work Order wo2 = frappe.get_doc("Work Order", wo_list[1]) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f50c82c66b6..204a6df6289 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -632,6 +632,21 @@ class WorkOrder(Document): if not self.qty > 0: frappe.throw(_("Quantity to Manufacture must be greater than 0.")) + if self.production_plan and self.production_plan_item: + qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1) + + allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings", + "overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0) + + max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0) + + if max_qty < 1: + frappe.throw(_("Cannot produce more item for {0}") + .format(self.production_item), OverProductionError) + elif self.qty > max_qty: + frappe.throw(_("Cannot produce more than {0} items for {1}") + .format(max_qty, self.production_item), OverProductionError) + def validate_transfer_against(self): if not self.docstatus == 1: # let user configure operations until they're ready to submit diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4801b333df9..a89776250f2 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2265,13 +2265,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, coupon_code: function() { - var me = this; - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() - ]); + if (this.frm.doc.coupon_code || this.frm._last_coupon_code) { + // reset pricing rules if coupon code is set or is unset + const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule; + return frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => this.frm.trigger('ignore_pricing_rule'), + () => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule, + () => this.frm.trigger('apply_pricing_rule'), + () => this.frm._last_coupon_code = this.frm.doc.coupon_code + ]); + } } }); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js index 831626aa915..a585aa614fb 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js @@ -304,12 +304,13 @@ erpnext.HierarchyChart = class { } get_child_nodes(node_id) { + let me = this; return new Promise(resolve => { frappe.call({ - method: this.method, + method: me.method, args: { parent: node_id, - company: this.company + company: me.company } }).then(r => resolve(r.message)); }); @@ -350,12 +351,13 @@ erpnext.HierarchyChart = class { } get_all_nodes() { + let me = this; return new Promise(resolve => { frappe.call({ method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', args: { - method: this.method, - company: this.company + method: me.method, + company: me.company }, callback: (r) => { resolve(r.message); @@ -427,8 +429,8 @@ erpnext.HierarchyChart = class { add_connector(parent_id, child_id) { // using pure javascript for better performance - const parent_node = document.querySelector(`#${parent_id}`); - const child_node = document.querySelector(`#${child_id}`); + const parent_node = document.getElementById(`${parent_id}`); + const child_node = document.getElementById(`${child_id}`); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js index 0a8ba78f643..52236e7df96 100644 --- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js +++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js @@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class { let me = this; return new Promise(resolve => { frappe.call({ - method: this.method, + method: me.method, args: { parent: node_id, company: me.company, @@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class { } add_connector(parent_id, child_id) { - const parent_node = document.querySelector(`#${parent_id}`); - const child_node = document.querySelector(`#${child_id}`); + const parent_node = document.getElementById(`${parent_id}`); + const child_node = document.getElementById(`${child_id}`); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); @@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class { level.nextAll('li').remove(); let node_object = this.nodes[node.id]; - let current_node = level.find(`#${node.id}`).detach(); + let current_node = level.find(`[id="${node.id}"]`).detach(); + current_node.removeClass('active-child active-path'); node_object.expanded = 0; diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 4d75e6ef1bf..1e9f6d7d920 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule) { - if (frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } + if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); } }); diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 557e07bf0a2..7bc875ac12f 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -399,6 +399,7 @@ class Item(Document): if merge: self.validate_properties_before_merge(new_name) + self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) def after_rename(self, old_name, new_name, merge): @@ -463,6 +464,20 @@ class Item(Document): msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): + "Block merge if both old and new items have product bundles." + old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) + new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name}) + + if old_bundle and new_bundle: + bundle_link = get_link_to_form("Product Bundle", old_bundle) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) + + msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( + bundle_link, old_name, new_name + ) + frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ Block merge if both old and new items have website items against them. @@ -480,8 +495,9 @@ class Item(Document): old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] web_item_link = get_link_to_form("Website Item", old_web_item) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) - msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}" + msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}" frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) def set_last_purchase_rate(self, new_name): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 150cead465b..c912101a4ac 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import ( get_variant, ) from erpnext.stock.doctype.item.item import ( + DataValidationError, InvalidBarcode, StockExistsForTemplate, get_item_attribute, @@ -387,6 +388,26 @@ class TestItem(ERPNextTestCase): self.assertTrue(frappe.db.get_value("Bin", {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + def test_item_merging_with_product_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + create_item("Test Item Bundle Item 1", is_stock_item=False) + create_item("Test Item Bundle Item 2", is_stock_item=False) + create_item("Test Item inside Bundle") + bundle_items = ["Test Item inside Bundle"] + + # make bundles for both items + bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2) + make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2) + + with self.assertRaises(DataValidationError): + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + bundle1.delete() + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) + def test_uom_conversion_factor(self): if frappe.db.exists('Item', 'Test Item UOM'): frappe.delete_doc('Item', 'Test Item UOM') diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index fc9d1ed98f7..61466cff032 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', { frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.get_warehouse_details(frm, cdt, cdn); }); + + // set allow_zero_valuation_rate to 0 if s_warehouse is selected. + let item = frappe.get_doc(cdt, cdn); + if (item.s_warehouse) { + item.allow_zero_valuation_rate = 0; + } }, t_warehouse: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 3d936c50f7a..3c34d4795cb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -45,6 +45,7 @@ def get_sle(**args): class TestStockEntry(ERPNextTestCase): def tearDown(self): + frappe.db.rollback() frappe.set_user("Administrator") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") @@ -566,6 +567,7 @@ class TestStockEntry(ERPNextTestCase): st1.set_stock_entry_type() st1.insert() st1.submit() + st1.cancel() frappe.set_user("Administrator") remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") @@ -690,6 +692,8 @@ class TestStockEntry(ERPNextTestCase): bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1}) + make_item_variant() # make variant of _Test Variant Item if absent + work_order = frappe.new_doc("Work Order") work_order.update({ "company": "_Test Company", @@ -1101,13 +1105,10 @@ class TestStockEntry(ERPNextTestCase): # Check if FG cost is calculated based on RM total cost # RM total cost = 200, FG rate = 200/4(FG qty) = 50 - self.assertEqual(se.items[1].basic_rate, 50) + self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4)) self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) - # teardown - se.delete() - def make_serialized_item(**args): args = frappe._dict(args) se = frappe.copy_doc(test_records[0]) diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index 2282b6aa167..a6b21891715 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "hash", - "creation": "2013-03-29 18:22:12", + "creation": "2022-02-05 00:17:49.860824", "doctype": "DocType", "document_type": "Other", "editable_grid": 1, @@ -340,13 +340,13 @@ "label": "More Information" }, { - "allow_on_submit": 1, "default": "0", "fieldname": "allow_zero_valuation_rate", "fieldtype": "Check", "label": "Allow Zero Valuation Rate", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval:doc.s_warehouse" }, { "allow_on_submit": 1, @@ -556,12 +556,14 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-22 16:47:11.268975", + "modified": "2022-02-26 00:51:24.963653", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 48753b0edd4..8a6210cc2be 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -60,6 +60,9 @@ def add_invariant_check_fields(sles): fifo_qty += qty fifo_value += qty * rate + if sle.actual_qty < 0: + sle.consumption_rate = sle.stock_value_difference / sle.actual_qty + balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: @@ -145,9 +148,9 @@ def get_columns(): "label": "Incoming Rate", }, { - "fieldname": "outgoing_rate", + "fieldname": "consumption_rate", "fieldtype": "Float", - "label": "Outgoing Rate", + "label": "Consumption Rate", }, { "fieldname": "qty_after_transaction", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b47097553ba..c2cb2ca6746 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -23,7 +23,6 @@ class NegativeStockError(frappe.ValidationError): pass class SerialNoExistsInFutureTransaction(frappe.ValidationError): pass -_exceptions = frappe.local('stockledger_exceptions') def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): from erpnext.controllers.stock_controller import future_sle_exists @@ -459,6 +458,8 @@ class update_entries_after(object): # rounding as per precision self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision) + if not self.wh_data.qty_after_transaction: + self.wh_data.stock_value = 0.0 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value self.wh_data.prev_stock_value = self.wh_data.stock_value @@ -622,9 +623,7 @@ class update_entries_after(object): if not self.wh_data.valuation_rate and sle.voucher_detail_no: allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_rate: - self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_incoming_value_for_serial_nos(self, sle, serial_nos): # get rate from serial nos within same company @@ -690,9 +689,7 @@ class update_entries_after(object): if not self.wh_data.valuation_rate and sle.voucher_detail_no: allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: - self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + self.wh_data.valuation_rate = self.get_fallback_rate(sle) def get_fifo_values(self, sle): incoming_rate = flt(sle.incoming_rate) @@ -723,9 +720,7 @@ class update_entries_after(object): # Get valuation rate from last sle if exists or from valuation rate field in item master allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no) if not allow_zero_valuation_rate: - _rate = get_valuation_rate(sle.item_code, sle.warehouse, - sle.voucher_type, sle.voucher_no, self.allow_zero_rate, - currency=erpnext.get_company_currency(sle.company), company=sle.company) + _rate = self.get_fallback_rate(sle) else: _rate = 0 @@ -788,6 +783,13 @@ class update_entries_after(object): else: return 0 + def get_fallback_rate(self, sle) -> float: + """When exact incoming rate isn't available use any of other "average" rates as fallback. + This should only get used for negative stock.""" + return get_valuation_rate(sle.item_code, sle.warehouse, + sle.voucher_type, sle.voucher_no, self.allow_zero_rate, + currency=erpnext.get_company_currency(sle.company), company=sle.company) + def get_sle_before_datetime(self, args): """get previous stock ledger entry before current time-bucket""" sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index d46ffb56096..6e70bc579e7 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -3726,7 +3726,7 @@ Earliest Age,Frühestes Alter, Edit Details,Details bearbeiten, Edit Profile,Profil bearbeiten, Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich, -Email,Email, +Email,E-Mail, Email Campaigns,E-Mail-Kampagnen, Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft, Employee Tax and Benefits,Mitarbeitersteuern und -leistungen, @@ -6481,7 +6481,7 @@ Select Users,Wählen Sie Benutzer aus, Send Emails At,Die E-Mails senden um, Reminder,Erinnerung, Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer, -email,Email, +email,E-Mail, Parent Department,Elternabteilung, Leave Block List,Urlaubssperrenliste, Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",