diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index a27e34819d..796a069651 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -442,7 +442,29 @@ class StockController(AccountsController): if not dimension: continue - if row.get(dimension.source_fieldname): + if self.doctype in [ + "Purchase Invoice", + "Purchase Receipt", + "Sales Invoice", + "Delivery Note", + "Stock Entry", + ]: + if (sl_dict.actual_qty > 0 and self.doctype in ["Purchase Invoice", "Purchase Receipt"]) or ( + sl_dict.actual_qty < 0 and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"] + ): + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + else: + fieldname_start_with = "to" + if self.doctype in ["Purchase Invoice", "Purchase Receipt"]: + fieldname_start_with = "from" + + fieldname = f"{fieldname_start_with}_{dimension.source_fieldname}" + sl_dict[dimension.target_fieldname] = row.get(fieldname) + + if not sl_dict.get(dimension.target_fieldname): + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) + + elif row.get(dimension.source_fieldname): sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent: diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json index 2b1807c965..39b2b3eaeb 100644 --- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.json +++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.json @@ -165,6 +165,7 @@ "fieldname": "slide_3_content_align", "fieldtype": "Select", "label": "Content Align", + "options": "Left\nCentre\nRight", "reqd": 0 }, { @@ -214,6 +215,7 @@ "fieldname": "slide_4_content_align", "fieldtype": "Select", "label": "Content Align", + "options": "Left\nCentre\nRight", "reqd": 0 }, { @@ -263,6 +265,7 @@ "fieldname": "slide_5_content_align", "fieldtype": "Select", "label": "Content Align", + "options": "Left\nCentre\nRight", "reqd": 0 }, { @@ -274,7 +277,7 @@ } ], "idx": 2, - "modified": "2021-02-24 15:57:05.889709", + "modified": "2023-05-12 15:03:57.604060", "modified_by": "Administrator", "module": "E-commerce", "name": "Hero Slider", diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index f147b46ca7..8c671e2644 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -48,7 +48,6 @@ frappe.ui.form.on("BOM", { return { query: "erpnext.manufacturing.doctype.bom.bom.item_query", filters: { - "item_code": doc.item, "include_item_in_manufacturing": 1, "is_fixed_asset": 0 } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 03c7b01856..e158df63ff 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -326,7 +326,7 @@ erpnext.patches.v13_0.update_docs_link erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance -erpnext.patches.v14_0.update_closing_balances +erpnext.patches.v14_0.update_closing_balances #10-05-2023 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index f47e730fd2..bb108ab827 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -11,6 +11,8 @@ from erpnext.accounts.utils import get_fiscal_year def execute(): + frappe.db.truncate("Account Closing Balance") + company_wise_order = {} get_opening_entries = True for pcv in frappe.db.get_all( @@ -35,7 +37,20 @@ def execute(): entry["closing_date"] = pcv_doc.posting_date entry["period_closing_voucher"] = pcv_doc.name - closing_entries = pcv_doc.get_grouped_gl_entries(get_opening_entries=get_opening_entries) + closing_entries = frappe.db.get_all( + "GL Entry", + filters={ + "is_cancelled": 0, + "voucher_no": ["!=", pcv.name], + "posting_date": ["<=", pcv.posting_date], + }, + fields=["*"], + ) + + for entry in closing_entries: + entry["closing_date"] = pcv_doc.posting_date + entry["period_closing_voucher"] = pcv_doc.name + make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) company_wise_order[pcv.company].append(pcv.posting_date) get_opening_entries = False diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index de63f6dec5..06467e51a6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -620,6 +620,8 @@ def make_project(source_name, target_doc=None): @frappe.whitelist() def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): + from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + def set_missing_values(source, target): target.run_method("set_missing_values") target.run_method("set_po_nos") @@ -634,6 +636,8 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): if target.company_address: target.update(get_fetch_values("Delivery Note", "company_address", target.company_address)) + make_packing_list(target) + def update_item(source, target, source_parent): target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) target.amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.rate) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ba8bbc2185..9854f159cf 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1909,6 +1909,75 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(mr.items[0].qty, 6) + def test_packed_items_for_partial_sales_order(self): + # test Update Items with product bundle + for product_bundle in [ + "_Test Product Bundle Item Partial 1", + "_Test Product Bundle Item Partial 2", + ]: + if not frappe.db.exists("Item", product_bundle): + bundle_item = make_item(product_bundle, {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + for product_bundle in ["_Packed Item Partial 1", "_Packed Item Partial 2"]: + if not frappe.db.exists("Item", product_bundle): + make_item(product_bundle, {"is_stock_item": 1, "stock_uom": "Nos"}) + + make_stock_entry(item=product_bundle, target="_Test Warehouse - _TC", qty=2, rate=10) + + make_product_bundle("_Test Product Bundle Item Partial 1", ["_Packed Item Partial 1"], 1) + + make_product_bundle("_Test Product Bundle Item Partial 2", ["_Packed Item Partial 2"], 1) + + so = make_sales_order( + item_code="_Test Product Bundle Item Partial 1", + warehouse="_Test Warehouse - _TC", + qty=1, + uom="Nos", + stock_uom="Nos", + conversion_factor=1, + transaction_date=nowdate(), + delivery_note=nowdate(), + do_not_submit=1, + ) + + so.append( + "items", + { + "item_code": "_Test Product Bundle Item Partial 2", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "uom": "Nos", + "stock_uom": "Nos", + "conversion_factor": 1, + "delivery_note": nowdate(), + }, + ) + + so.save() + so.submit() + + dn = make_delivery_note(so.name) + dn.remove(dn.items[1]) + dn.save() + dn.submit() + + self.assertEqual(len(dn.items), 1) + self.assertEqual(len(dn.packed_items), 1) + self.assertEqual(dn.items[0].item_code, "_Test Product Bundle Item Partial 1") + + so.load_from_db() + + dn = make_delivery_note(so.name) + dn.save() + + self.assertEqual(len(dn.items), 1) + self.assertEqual(len(dn.packed_items), 1) + self.assertEqual(dn.items[0].item_code, "_Test Product Bundle Item Partial 2") + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index db2b5d0a6b..8bff4d5147 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -75,7 +75,16 @@ class InventoryDimension(Document): self.delete_custom_fields() def delete_custom_fields(self): - filters = {"fieldname": self.source_fieldname} + filters = { + "fieldname": ( + "in", + [ + self.source_fieldname, + f"to_{self.source_fieldname}", + f"from_{self.source_fieldname}", + ], + ) + } if self.document_type: filters["dt"] = self.document_type @@ -88,6 +97,8 @@ class InventoryDimension(Document): def reset_value(self): if self.apply_to_all_doctypes: + self.type_of_transaction = "" + self.istable = 0 for field in ["document_type", "condition"]: self.set(field, None) @@ -111,12 +122,35 @@ class InventoryDimension(Document): def on_update(self): self.add_custom_fields() - def add_custom_fields(self): - dimension_fields = [ + @staticmethod + def get_insert_after_fieldname(doctype): + return frappe.get_all( + "DocField", + fields=["fieldname"], + filters={"parent": doctype}, + order_by="idx desc", + limit=1, + )[0].fieldname + + def get_dimension_fields(self, doctype=None): + if not doctype: + doctype = self.document_type + + label_start_with = "" + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + label_start_with = "Target" + elif doctype in ["Sales Invoice Item", "Delivery Note Item", "Stock Entry Detail"]: + label_start_with = "Source" + + label = self.dimension_name + if label_start_with: + label = f"{label_start_with} {self.dimension_name}" + + return [ dict( fieldname="inventory_dimension", fieldtype="Section Break", - insert_after="warehouse", + insert_after=self.get_insert_after_fieldname(doctype), label="Inventory Dimension", collapsible=1, ), @@ -125,24 +159,37 @@ class InventoryDimension(Document): fieldtype="Link", insert_after="inventory_dimension", options=self.reference_document, - label=self.dimension_name, + label=label, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), ] + def add_custom_fields(self): custom_fields = {} + dimension_fields = [] if self.apply_to_all_doctypes: for doctype in get_inventory_documents(): - if not field_exists(doctype[0], self.source_fieldname): - custom_fields.setdefault(doctype[0], dimension_fields) + if field_exists(doctype[0], self.source_fieldname): + continue + + dimension_fields = self.get_dimension_fields(doctype[0]) + self.add_transfer_field(doctype[0], dimension_fields) + custom_fields.setdefault(doctype[0], dimension_fields) elif not field_exists(self.document_type, self.source_fieldname): + dimension_fields = self.get_dimension_fields() + + self.add_transfer_field(self.document_type, dimension_fields) custom_fields.setdefault(self.document_type, dimension_fields) - if not frappe.db.get_value( - "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} - ) and not field_exists("Stock Ledger Entry", self.target_fieldname): + if ( + dimension_fields + and not frappe.db.get_value( + "Custom Field", {"dt": "Stock Ledger Entry", "fieldname": self.target_fieldname} + ) + and not field_exists("Stock Ledger Entry", self.target_fieldname) + ): dimension_field = dimension_fields[1] dimension_field["mandatory_depends_on"] = "" dimension_field["reqd"] = 0 @@ -152,6 +199,53 @@ class InventoryDimension(Document): if custom_fields: create_custom_fields(custom_fields) + def add_transfer_field(self, doctype, dimension_fields): + if doctype not in [ + "Stock Entry Detail", + "Sales Invoice Item", + "Delivery Note Item", + "Purchase Invoice Item", + "Purchase Receipt Item", + ]: + return + + fieldname_start_with = "to" + label_start_with = "Target" + display_depends_on = "" + + if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]: + fieldname_start_with = "from" + label_start_with = "Source" + display_depends_on = "eval:parent.is_internal_supplier == 1" + elif doctype != "Stock Entry Detail": + display_depends_on = "eval:parent.is_internal_customer == 1" + elif doctype == "Stock Entry Detail": + display_depends_on = "eval:parent.purpose != 'Material Issue'" + + fieldname = f"{fieldname_start_with}_{self.source_fieldname}" + label = f"{label_start_with} {self.dimension_name}" + + if field_exists(doctype, fieldname): + return + + dimension_fields.extend( + [ + dict( + fieldname="inventory_dimension_col_break", + fieldtype="Column Break", + insert_after=self.source_fieldname, + ), + dict( + fieldname=fieldname, + fieldtype="Link", + insert_after="inventory_dimension_col_break", + options=self.reference_document, + label=label, + depends_on=display_depends_on, + ), + ] + ) + def field_exists(doctype, fieldname) -> str or None: return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name") @@ -185,18 +279,19 @@ def get_evaluated_inventory_dimension(doc, sl_dict, parent_doc=None): dimensions = get_document_wise_inventory_dimensions(doc.doctype) filter_dimensions = [] for row in dimensions: - if ( - row.type_of_transaction == "Inward" - if doc.docstatus == 1 - else row.type_of_transaction != "Inward" - ) and sl_dict.actual_qty < 0: - continue - elif ( - row.type_of_transaction == "Outward" - if doc.docstatus == 1 - else row.type_of_transaction != "Outward" - ) and sl_dict.actual_qty > 0: - continue + if row.type_of_transaction: + if ( + row.type_of_transaction == "Inward" + if doc.docstatus == 1 + else row.type_of_transaction != "Inward" + ) and sl_dict.actual_qty < 0: + continue + elif ( + row.type_of_transaction == "Outward" + if doc.docstatus == 1 + else row.type_of_transaction != "Outward" + ) and sl_dict.actual_qty > 0: + continue evals = {"doc": doc} if parent_doc: diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index 28b1ed96f0..ae5f521f2b 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -12,6 +12,7 @@ from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( DoNotChangeError, delete_dimension, ) +from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse @@ -20,6 +21,7 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse class TestInventoryDimension(FrappeTestCase): def setUp(self): prepare_test_data() + create_store_dimension() def test_validate_inventory_dimension(self): # Can not be child doc @@ -73,6 +75,8 @@ class TestInventoryDimension(FrappeTestCase): self.assertFalse(custom_field) def test_inventory_dimension(self): + frappe.local.document_wise_inventory_dimensions = {} + warehouse = "Shelf Warehouse - _TC" item_code = "_Test Item" @@ -143,6 +147,8 @@ class TestInventoryDimension(FrappeTestCase): self.assertRaises(DoNotChangeError, inv_dim1.save) def test_inventory_dimension_for_purchase_receipt_and_delivery_note(self): + frappe.local.document_wise_inventory_dimensions = {} + inv_dimension = create_inventory_dimension( reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1 ) @@ -250,6 +256,97 @@ class TestInventoryDimension(FrappeTestCase): ) ) + def test_for_purchase_sales_and_stock_transaction(self): + create_inventory_dimension( + reference_document="Store", + type_of_transaction="Outward", + dimension_name="Store", + apply_to_all_doctypes=1, + ) + + item_code = "Test Inventory Dimension Item" + create_item(item_code) + warehouse = create_warehouse("Store Warehouse") + + # Purchase Receipt -> Inward in Store 1 + pr_doc = make_purchase_receipt( + item_code=item_code, warehouse=warehouse, qty=10, rate=100, do_not_submit=True + ) + + pr_doc.items[0].store = "Store 1" + pr_doc.save() + pr_doc.submit() + + entries = get_voucher_sl_entries(pr_doc.name, ["warehouse", "store", "incoming_rate"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 1") + + # Stock Entry -> Transfer from Store 1 to Store 2 + se_doc = make_stock_entry( + item_code=item_code, qty=10, from_warehouse=warehouse, to_warehouse=warehouse, do_not_save=True + ) + + se_doc.items[0].store = "Store 1" + se_doc.items[0].to_store = "Store 2" + + se_doc.save() + se_doc.submit() + + entries = get_voucher_sl_entries( + se_doc.name, ["warehouse", "store", "incoming_rate", "actual_qty"] + ) + + for entry in entries: + self.assertEqual(entry.warehouse, warehouse) + if entry.actual_qty > 0: + self.assertEqual(entry.store, "Store 2") + self.assertEqual(entry.incoming_rate, 100.0) + else: + self.assertEqual(entry.store, "Store 1") + + # Delivery Note -> Outward from Store 2 + + dn_doc = create_delivery_note(item_code=item_code, qty=10, warehouse=warehouse, do_not_save=True) + + dn_doc.items[0].store = "Store 2" + dn_doc.save() + dn_doc.submit() + + entries = get_voucher_sl_entries(dn_doc.name, ["warehouse", "store", "actual_qty"]) + + self.assertEqual(entries[0].warehouse, warehouse) + self.assertEqual(entries[0].store, "Store 2") + self.assertEqual(entries[0].actual_qty, -10.0) + + +def get_voucher_sl_entries(voucher_no, fields): + return frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": voucher_no}, fields=fields, order_by="creation" + ) + + +def create_store_dimension(): + if not frappe.db.exists("DocType", "Store"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Store", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:store_name", + "fields": [{"label": "Store Name", "fieldname": "store_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for store in ["Store 1", "Store 2"]: + if not frappe.db.exists("Store", store): + frappe.get_doc({"doctype": "Store", "store_name": store}).insert(ignore_permissions=True) + def prepare_test_data(): if not frappe.db.exists("DocType", "Shelf"): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index cc0923f37c..cd076d88db 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -9,7 +9,17 @@ import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate +from frappe.utils import ( + cint, + comma_or, + cstr, + flt, + format_time, + formatdate, + getdate, + month_diff, + nowdate, +) import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -151,6 +161,41 @@ class StockEntry(StockController): self.reset_default_field_value("from_warehouse", "items", "s_warehouse") self.reset_default_field_value("to_warehouse", "items", "t_warehouse") + def submit(self): + if self.is_enqueue_action(): + frappe.msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage" + ) + ) + self.queue_action("submit", timeout=2000) + else: + self._submit() + + def cancel(self): + if self.is_enqueue_action(): + frappe.msgprint( + _( + "The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage" + ) + ) + self.queue_action("cancel", timeout=2000) + else: + self._cancel() + + def is_enqueue_action(self, force=False) -> bool: + if force: + return True + + if frappe.flags.in_test: + return False + + # If line items are more than 100 or record is older than 6 months + if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6: + return True + + return False + def on_submit(self): self.update_stock_ledger() diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c43a1b1b81..de74fda687 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, today +from frappe.utils import add_days, add_to_date, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -1707,6 +1707,36 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(frappe.ValidationError, sr_doc.submit) + def test_enqueue_action(self): + frappe.flags.in_test = False + item_code = "Test Enqueue Item - 001" + create_item(item_code=item_code, is_stock_item=1, valuation_rate=10) + + doc = make_stock_entry( + item_code=item_code, + posting_date=add_to_date(today(), months=-7), + posting_time="00:00:00", + purpose="Material Receipt", + qty=10, + to_warehouse="_Test Warehouse - _TC", + do_not_submit=True, + ) + + self.assertTrue(doc.is_enqueue_action()) + + doc = make_stock_entry( + item_code=item_code, + posting_date=today(), + posting_time="00:00:00", + purpose="Material Receipt", + qty=10, + to_warehouse="_Test Warehouse - _TC", + do_not_submit=True, + ) + + self.assertFalse(doc.is_enqueue_action()) + frappe.flags.in_test = True + def make_serialized_item(**args): args = frappe._dict(args)