diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index 8cbaef43180..e41d1b0f768 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -4,7 +4,7 @@ import frappe from frappe import _, throw -from frappe.utils import cint, cstr +from frappe.utils import add_to_date, cint, cstr, pretty_date from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of import erpnext @@ -400,6 +400,7 @@ def validate_account_number(name, account_number, company): @frappe.whitelist() def update_account_number(name, account_name, account_number=None, from_descendant=False): + _ensure_idle_system() account = frappe.db.get_value("Account", name, "company", as_dict=True) if not account: return @@ -461,6 +462,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda @frappe.whitelist() def merge_account(old, new): + _ensure_idle_system() # Validate properties before merging new_account = frappe.get_cached_doc("Account", new) old_account = frappe.get_cached_doc("Account", old) @@ -514,3 +516,27 @@ def sync_update_account_number_in_child( for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True): update_account_number(d["name"], account_name, account_number, from_descendant=True) + + +def _ensure_idle_system(): + # Don't allow renaming if accounting entries are actively being updated, there are two main reasons: + # 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*. + # 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance. + + if frappe.flags.in_test: + return + + try: + # We also lock inserts to GL entry table with for_update here. + last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False) + except frappe.QueryTimeoutError: + # wait=False fails immediately if there's an active transaction. + last_gl_update = add_to_date(None, seconds=-1) + + if last_gl_update > add_to_date(None, minutes=-5): + frappe.throw( + _( + "Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying." + ).format(pretty_date(last_gl_update)), + title=_("System In Use"), + ) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 25b67e2db31..d40efc52d84 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1519,7 +1519,7 @@ class PaymentEntry(AccountsController): allocated_positive_outstanding = paid_amount + allocated_negative_outstanding - elif self.party_type in ("Supplier", "Employee"): + elif self.party_type in ("Supplier", "Customer"): if paid_amount > total_negative_outstanding: if total_negative_outstanding == 0: frappe.msgprint( diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index f5a9d17f088..7c13fe50f1a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1623,6 +1623,5 @@ "states": [], "timeline_field": "customer", "title_field": "title", - "track_changes": 1, - "track_seen": 1 -} \ No newline at end of file + "track_changes": 1 +} diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json index 46b430c6594..51dc3674594 100644 --- a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -13,17 +13,15 @@ "fields": [ { "fieldname": "voucher_type", - "fieldtype": "Link", + "fieldtype": "Data", "in_list_view": 1, - "label": "Voucher Type", - "options": "DocType" + "label": "Voucher Type" }, { "fieldname": "voucher_name", - "fieldtype": "Dynamic Link", + "fieldtype": "Data", "in_list_view": 1, - "label": "Voucher Name", - "options": "voucher_type" + "label": "Voucher Name" }, { "fieldname": "taxable_amount", @@ -36,7 +34,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-01-13 13:40:41.479208", + "modified": "2025-02-05 16:39:14.863698", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withheld Vouchers", diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index f6efc8a685c..dc6192e7544 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -27,6 +27,7 @@ def get_report_filters(report_filters): ["Purchase Invoice", "docstatus", "=", 1], ["Purchase Invoice", "per_received", "<", 100], ["Purchase Invoice", "update_stock", "=", 0], + ["Purchase Invoice", "is_opening", "!=", "Yes"], ] if report_filters.get("purchase_invoice"): diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index e540aa9993c..db42d23a839 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -263,6 +263,7 @@ def get_actual_details(name, filters): and ba.account=gl.account and b.{budget_against} = gl.{budget_against} and gl.fiscal_year between %s and %s + and gl.is_cancelled = 0 and b.{budget_against} = %s and exists( select diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index aa06dffb043..25d52998449 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -506,6 +506,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for dim in accounting_dimensions: keylist.append(gle.get(dim)) keylist.append(gle.get("cost_center")) + keylist.append(gle.get("project")) key = tuple(keylist) if key not in consolidated_gle: @@ -617,10 +618,11 @@ def get_columns(filters): {"label": _("Against Account"), "fieldname": "against", "width": 120}, {"label": _("Party Type"), "fieldname": "party_type", "width": 100}, {"label": _("Party"), "fieldname": "party", "width": 100}, - {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}, ] if filters.get("include_dimensions"): + columns.append({"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}) + for dim in get_accounting_dimensions(as_list=False): columns.append( {"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100} diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index b37658b7784..2f4c154cb00 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -801,7 +801,7 @@ class StockController(AccountsController): child_tab.item_code, child_tab.qty, ) - .where(parent_tab.docstatus < 2) + .where(parent_tab.docstatus == 1) ) if self.doctype == "Purchase Invoice": diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 14948d60e46..45ba4304985 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -4,7 +4,7 @@ app_publisher = "Frappe Technologies Pvt. Ltd." app_description = """ERP made simple""" app_icon = "fa fa-th" app_color = "#e74c3c" -app_email = "info@erpnext.com" +app_email = "hello@frappe.io" app_license = "GNU General Public License (v3)" source_link = "https://github.com/frappe/erpnext" app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" @@ -479,7 +479,7 @@ email_brand_image = "assets/erpnext/images/erpnext-logo.jpg" default_mail_footer = """ Sent via - + ERPNext diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ab842095412..cdec75b64ec 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -371,4 +371,4 @@ erpnext.patches.v14_0.update_stock_uom_in_work_order_item erpnext.patches.v14_0.disable_add_row_in_gross_profit execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.update_posting_datetime - +erpnext.stock.doctype.stock_ledger_entry.patches.ensure_sle_indexes diff --git a/erpnext/regional/address_template/templates/united_states.html b/erpnext/regional/address_template/templates/united_states.html index 77fce46b9d7..f00f99c1299 100644 --- a/erpnext/regional/address_template/templates/united_states.html +++ b/erpnext/regional/address_template/templates/united_states.html @@ -1,4 +1,4 @@ {{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%} -{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}
{% endif -%} +{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}{% endif -%}
{% if country != "United States" %}{{ country }}{% endif -%} diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index 7db9faacca2..6cb72d53b59 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -275,12 +275,12 @@ def get_past_order_list(search_term, status, limit=20): invoice_list = [] if search_term and status: - invoices_by_customer = frappe.db.get_all( + invoices_by_customer = frappe.db.get_list( "POS Invoice", filters={"customer": ["like", f"%{search_term}%"], "status": status}, fields=fields, ) - invoices_by_name = frappe.db.get_all( + invoices_by_name = frappe.db.get_list( "POS Invoice", filters={"name": ["like", f"%{search_term}%"], "status": status}, fields=fields, @@ -288,7 +288,7 @@ def get_past_order_list(search_term, status, limit=20): invoice_list = invoices_by_customer + invoices_by_name elif status: - invoice_list = frappe.db.get_all("POS Invoice", filters={"status": status}, fields=fields) + invoice_list = frappe.db.get_list("POS Invoice", filters={"status": status}, fields=fields) return invoice_list diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index dcf086f2d05..a5a75ebcce8 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -15,7 +15,7 @@ from erpnext.setup.doctype.incoterm.incoterm import create_incoterms from .default_success_action import get_default_success_action default_mail_footer = """
Sent via - ERPNext
""" + ERPNext""" def after_install(): diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 5d701a73393..ecc726fd86c 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -196,8 +196,17 @@ class InventoryDimension(Document): custom_fields["Stock Ledger Entry"] = dimension_field filter_custom_fields = {} + + ignore_doctypes = [ + "Pick List Item", + "Maintenance Visit Purpose", + ] + if custom_fields: for doctype, fields in custom_fields.items(): + if doctype in ignore_doctypes: + continue + if isinstance(fields, dict): fields = [fields] diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 38b1ca7f76e..8f74373ac8d 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -2,6 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on("Pick List", { + after_save(frm) { + setTimeout(() => { + // Added to fix the issue of locations table not getting updated after save + frm.reload_doc(); + }, 500); + }, + setup: (frm) => { frm.set_indicator_formatter("item_code", function (doc) { return doc.stock_qty === 0 ? "red" : "green"; diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index a2a07e6d11e..a94370c48a5 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -25,10 +25,38 @@ from erpnext.stock.get_item_details import get_conversion_factor class PickList(Document): def validate(self): + self.validate_expired_batches() self.validate_for_qty() self.validate_stock_qty() self.check_serial_no_status() + def validate_expired_batches(self): + batches = [] + for row in self.get("locations"): + if row.get("batch_no") and row.get("picked_qty"): + batches.append(row.batch_no) + + if batches: + batch = frappe.qb.DocType("Batch") + query = ( + frappe.qb.from_(batch) + .select(batch.name) + .where( + (batch.name.isin(batches)) + & (batch.expiry_date <= frappe.utils.nowdate()) + & (batch.expiry_date.isnotnull()) + ) + ) + + expired_batches = query.run(as_dict=True) + if expired_batches: + msg = "" + + frappe.throw( + _("The following batches are expired, please restock them:
{0}").format(msg), + title=_("Expired Batches"), + ) + def before_save(self): self.update_status() if not self.pick_manually: @@ -271,6 +299,7 @@ class PickList(Document): self.remove(row) updated_locations = frappe._dict() + len_idx = len(self.get("locations")) or 0 for item_doc in items: item_code = item_doc.item_code @@ -313,6 +342,8 @@ class PickList(Document): if location.picked_qty > location.stock_qty: location.picked_qty = location.stock_qty + len_idx += 1 + location.idx = len_idx self.append("locations", location) # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red @@ -321,6 +352,9 @@ class PickList(Document): for location in locations_replica: location.stock_qty = 0 location.picked_qty = 0 + + len_idx += 1 + location.idx = len_idx self.append("locations", location) frappe.msgprint( _( @@ -430,9 +464,11 @@ class PickList(Document): pi_item.item_code, pi_item.warehouse, pi_item.batch_no, - Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_( - "picked_qty" - ), + Sum( + Case() + .when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty) + .else_(pi_item.stock_qty) + ).as_("picked_qty"), Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), ) .where( @@ -465,8 +501,32 @@ class PickList(Document): else: picked_items[item_data.item_code][key] = data + self.update_picked_item_from_current_pick_list(picked_items) + return picked_items + def update_picked_item_from_current_pick_list(self, picked_items): + for row in self.get("locations"): + if flt(row.picked_qty) > 0: + key = (row.warehouse, row.batch_no) if row.batch_no else row.warehouse + serial_no = [x for x in row.serial_no.split("\n") if x] if row.serial_no else None + if row.item_code not in picked_items: + picked_items[row.item_code] = {} + + if key not in picked_items[row.item_code]: + picked_items[row.item_code][key] = frappe._dict( + { + "picked_qty": 0, + "serial_no": [], + "batch_no": row.batch_no or "", + "warehouse": row.warehouse, + } + ) + + picked_items[row.item_code][key]["picked_qty"] += flt(row.stock_qty) or flt(row.picked_qty) + if serial_no: + picked_items[row.item_code][key]["serial_no"].extend(serial_no) + def _get_product_bundles(self) -> dict[str, str]: # Dict[so_item_row: item_code] product_bundles = {} diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4a9e0d08bd8..e7e17924e12 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -865,15 +865,19 @@ def get_billed_amount_against_po(po_items): if not po_items: return {} + purchase_invoice = frappe.qb.DocType("Purchase Invoice") purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") query = ( frappe.qb.from_(purchase_invoice_item) + .inner_join(purchase_invoice) + .on(purchase_invoice_item.parent == purchase_invoice.name) .select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.po_detail) .where( (purchase_invoice_item.po_detail.isin(po_items)) - & (purchase_invoice_item.docstatus == 1) + & (purchase_invoice.docstatus == 1) & (purchase_invoice_item.pr_detail.isnull()) + & (purchase_invoice.update_stock == 0) ) .groupby(purchase_invoice_item.po_detail) ).run(as_dict=1) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index adb6e690596..ef690cda67d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2731,6 +2731,36 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(return_pr.per_billed, 100) self.assertEqual(return_pr.status, "Completed") + def test_pr_status_based_on_invoices_with_update_stock(self): + from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_invoice as _make_purchase_invoice, + ) + from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_receipt as _make_purchase_receipt, + ) + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, + ) + + item_code = "Test Item for PR Status Based on Invoices" + create_item(item_code) + + po = create_purchase_order(item_code=item_code, qty=10) + pi = _make_purchase_invoice(po.name) + pi.update_stock = 1 + pi.items[0].qty = 5 + pi.submit() + + po.reload() + self.assertEqual(po.per_billed, 50) + + pr = _make_purchase_receipt(po.name) + self.assertEqual(pr.items[0].qty, 5) + pr.submit() + pr.reload() + self.assertEqual(pr.status, "To Bill") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/stock_ledger_entry/patches/__init__.py b/erpnext/stock/doctype/stock_ledger_entry/patches/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/stock_ledger_entry/patches/ensure_sle_indexes.py b/erpnext/stock/doctype/stock_ledger_entry/patches/ensure_sle_indexes.py new file mode 100644 index 00000000000..7f29b27af3f --- /dev/null +++ b/erpnext/stock/doctype/stock_ledger_entry/patches/ensure_sle_indexes.py @@ -0,0 +1,9 @@ +from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import ( + on_doctype_update as create_sle_indexes, +) + + +def execute(): + """Ensure SLE Indexes""" + + create_sle_indexes() diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html index 8310063e575..fb73931d18e 100644 --- a/erpnext/templates/includes/footer/footer_powered.html +++ b/erpnext/templates/includes/footer/footer_powered.html @@ -1 +1 @@ -{{ _("Powered by {0}").format('ERPNext') }} +{{ _("Powered by {0}").format('ERPNext') }} diff --git a/package.json b/package.json index 6c11e9dddc7..2f819f83eaf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "git", "url": "git+https://github.com/frappe/erpnext.git" }, - "homepage": "https://erpnext.com", + "homepage": "https://frappe.io/erpnext", "author": "Frappe Technologies Pvt. Ltd.", "license": "GPL-3.0", "bugs": {