diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index 3ce867dc96e..9ea87ef0ae7 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -304,6 +304,7 @@ def create_payment_entry_bts( project=None, cost_center=None, allow_edit=None, + company_bank_account=None, ): # Create a new payment entry based on the bank transaction bank_transaction = frappe.db.get_values( @@ -345,6 +346,9 @@ def create_payment_entry_bts( pe.project = project pe.cost_center = cost_center + if company_bank_account: + pe.bank_account = company_bank_account + pe.validate() if allow_edit: diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4146b4aebb2..852fcc6807b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -435,6 +435,7 @@ frappe.ui.form.on("Payment Entry", { "paid_to", "references", "total_allocated_amount", + "party_name", ], function (i, field) { frm.set_value(field, null); diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 11614467472..0aeec6905eb 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -115,6 +115,10 @@ class RepostAccountingLedger(Document): def generate_preview(self): from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + if not self.vouchers: + frappe.msgprint(_("Add vouchers to generate preview.")) + return + gl_columns = [] gl_data = [] @@ -142,6 +146,7 @@ class RepostAccountingLedger(Document): account_repost_doc=self.name, is_async=True, job_name=job_name, + enqueue_after_commit=True, ) frappe.msgprint(_("Repost has started in the background")) else: diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 38dc1e7502d..b8bcc3a4160 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -1056,3 +1056,21 @@ def add_party_account(party_type, party, company, account): def render_address(address, check_permissions=True): return frappe.call(_render_address, address, check_permissions=check_permissions) + + +def validate_party_currency_before_merging(party_type, old_party, new_party): + for company in frappe.get_all("Company"): + old_party_currency = get_party_gle_currency(party_type, old_party, company.name) + new_party_currency = get_party_gle_currency(party_type, new_party, company.name) + + if old_party_currency and new_party_currency and old_party_currency != new_party_currency: + frappe.throw( + _( + "Cannot merge {0} '{1}' into '{2}' as both have existing accounting entries in different currencies for company '{3}'." + ).format( + party_type, + old_party, + new_party, + company.name, + ) + ) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 18a85af95be..6a043f5e185 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = { label: __("Revaluation Journals"), fieldtype: "Check", }, + { + fieldname: "show_gl_balance", + label: __("Show GL Balance"), + fieldtype: "Check", + }, ], onload: function (report) { diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 19fd7dc96ef..19d2faddf44 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) if self.filters.show_gl_balance: - gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company) + gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type) for party, party_dict in self.party_total.items(): if flt(party_dict.outstanding, self.currency_precision) == 0: @@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport): ) -def get_gl_balance(report_date, company): +def get_gl_balance(report_date, company, account_type): + if account_type == "Payable": + balance_calc_fields = ["party", "SUM(credit - debit) AS balance"] + else: + balance_calc_fields = ["party", "SUM(debit - credit) AS balance"] return frappe._dict( frappe.db.get_all( "GL Entry", - fields=["party", "sum(debit - credit)"], + fields=balance_calc_fields, filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company}, group_by="party", as_list=1, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 164c9820fe1..c9968c7a34d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -480,6 +480,7 @@ class Asset(AccountsController): def set_depreciation_rate(self): for d in self.get("finance_books"): + self.validate_asset_finance_books(d) d.rate_of_depreciation = flt( self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) @@ -488,6 +489,10 @@ class Asset(AccountsController): row.expected_value_after_useful_life = flt( row.expected_value_after_useful_life, self.precision("gross_purchase_amount") ) + + if flt(row.expected_value_after_useful_life) < 0: + frappe.throw(_("Row {0}: Expected Value After Useful Life cannot be negative").format(row.idx)) + if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): frappe.throw( _("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format( @@ -503,50 +508,71 @@ class Asset(AccountsController): title=_("Invalid Schedule"), ) row.depreciation_start_date = get_last_day(self.available_for_use_date) + self.validate_depreciation_start_date(row) + self.validate_total_number_of_depreciations_and_frequency(row) if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 self.opening_number_of_booked_depreciations = 0 else: - depreciable_amount = flt( - flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life), - self.precision("gross_purchase_amount"), - ) - if flt(self.opening_accumulated_depreciation) > depreciable_amount: + self.validate_opening_depreciation_values(row) + + def validate_depreciation_start_date(self, row): + if row.depreciation_start_date: + if getdate(row.depreciation_start_date) < getdate(self.purchase_date): frappe.throw( - _("Opening Accumulated Depreciation must be less than or equal to {0}").format( - depreciable_amount + _("Row #{0}: Next Depreciation Date cannot be before Purchase Date").format(row.idx) + ) + + if getdate(row.depreciation_start_date) < getdate(self.available_for_use_date): + frappe.throw( + _("Row #{0}: Next Depreciation Date cannot be before Available-for-use Date").format( + row.idx ) ) - - if self.opening_accumulated_depreciation: - if not self.opening_number_of_booked_depreciations: - frappe.throw(_("Please set Opening Number of Booked Depreciations")) - else: - self.opening_number_of_booked_depreciations = 0 - - if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations): - frappe.throw( - _( - "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" - ).format(row.idx), - title=_("Invalid Schedule"), - ) - - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): + else: frappe.throw( - _("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format( - row.idx + _("Row #{0}: Depreciation Start Date is required").format(row.idx), + title=_("Invalid Schedule"), + ) + + def validate_total_number_of_depreciations_and_frequency(self, row): + if row.total_number_of_depreciations <= 0: + frappe.throw( + _("Row #{0}: Total Number of Depreciations must be greater than zero").format(row.idx) + ) + + if row.frequency_of_depreciation <= 0: + frappe.throw(_("Row #{0}: Frequency of Depreciation must be greater than zero").format(row.idx)) + + def validate_opening_depreciation_values(self, row): + row.expected_value_after_useful_life = flt( + row.expected_value_after_useful_life, self.precision("gross_purchase_amount") + ) + + depreciable_amount = flt( + flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life), + self.precision("gross_purchase_amount"), + ) + if flt(self.opening_accumulated_depreciation) > depreciable_amount: + frappe.throw( + _("Opening Accumulated Depreciation must be less than or equal to {0}").format( + depreciable_amount ) ) - if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate( - self.available_for_use_date - ): + if self.opening_accumulated_depreciation: + if not self.opening_number_of_booked_depreciations: + frappe.throw(_("Please set Opening Number of Booked Depreciations")) + else: + self.opening_number_of_booked_depreciations = 0 + + if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations): frappe.throw( _( - "Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date" - ).format(row.idx) + "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations" + ).format(row.idx), + title=_("Invalid Schedule"), ) def set_total_booked_depreciations(self): diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index 07a2d31166b..f0e85523ea3 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -14,6 +14,7 @@ from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_ from erpnext.accounts.party import ( get_dashboard_info, validate_party_accounts, + validate_party_currency_before_merging, ) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -208,6 +209,10 @@ class Supplier(TransactionBase): delete_contact_and_address("Supplier", self.name) + def before_rename(self, olddn, newdn, merge=False): + if merge: + validate_party_currency_before_merging("Supplier", olddn, newdn) + def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("supp_master_name") == "Supplier Name": self.db_set("supplier_name", newdn) diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 1e2688daf4d..e17fa3d622c 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -17,6 +17,15 @@ class TestTimesheet(unittest.TestCase): def setUp(self): frappe.db.delete("Timesheet") + def test_timesheet_base_amount(self): + emp = make_employee("test_employee_6@salary.com") + timesheet = make_timesheet(emp, simulate=True, is_billable=1) + + self.assertEqual(timesheet.time_logs[0].base_billing_rate, 50) + self.assertEqual(timesheet.time_logs[0].base_costing_rate, 20) + self.assertEqual(timesheet.time_logs[0].base_billing_amount, 100) + self.assertEqual(timesheet.time_logs[0].base_costing_amount, 40) + def test_timesheet_billing_amount(self): emp = make_employee("test_employee_6@salary.com") timesheet = make_timesheet(emp, simulate=True, is_billable=1) @@ -236,4 +245,5 @@ def make_timesheet( def update_activity_type(activity_type): activity_type = frappe.get_doc("Activity Type", activity_type) activity_type.billing_rate = 50.0 + activity_type.costing_rate = 20.0 activity_type.save(ignore_permissions=True) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 0b4b99ba35b..ec58c55f020 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -296,6 +296,20 @@ class Timesheet(Document): data.billing_amount = data.billing_rate * hours data.costing_amount = data.costing_rate * costing_hours + exchange_rate = flt(self.get("exchange_rate")) or 1.0 + data.base_billing_rate = flt( + data.billing_rate * exchange_rate, data.precision("base_billing_rate") + ) + data.base_costing_rate = flt( + data.costing_rate * exchange_rate, data.precision("base_costing_rate") + ) + data.base_billing_amount = flt( + data.billing_amount * exchange_rate, data.precision("base_billing_amount") + ) + data.base_costing_amount = flt( + data.costing_amount * exchange_rate, data.precision("base_costing_amount") + ) + def update_time_rates(self, ts_detail): if not ts_detail.is_billable: ts_detail.billing_rate = 0.0 diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index bab25b6eca2..16d4e9971d8 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -361,6 +361,21 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mandatory_depends_on: "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'", }, + { + fieldname: "bank_account", + fieldtype: "Link", + label: "Company Bank Account", + options: "Bank Account", + depends_on: "eval:doc.party", + get_query: function () { + return { + filters: { + is_company_account: 1, + company: this.company, + }, + }; + }, + }, { fieldname: "project", fieldtype: "Link", @@ -511,6 +526,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { mode_of_payment: values.mode_of_payment, project: values.project, cost_center: values.cost_center, + company_bank_account: values?.bank_account || this?.bank_account, }, callback: (response) => { const alert_string = __("Bank Transaction {0} added as Payment Entry", [ @@ -582,6 +598,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { project: values.project, cost_center: values.cost_center, allow_edit: true, + company_bank_account: values?.bank_account || this?.bank_account, }, callback: (r) => { const doc = frappe.model.sync(r.message); diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 449b56de3b4..1c1ae08b280 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -18,7 +18,11 @@ from frappe.utils import cint, cstr, flt, get_formatted_email, today from frappe.utils.deprecations import deprecated from frappe.utils.user import get_users_with_role -from erpnext.accounts.party import get_dashboard_info, validate_party_accounts +from erpnext.accounts.party import ( + get_dashboard_info, + validate_party_accounts, + validate_party_currency_before_merging, +) from erpnext.controllers.website_list_for_contact import add_role_for_portal_user from erpnext.utilities.transaction_base import TransactionBase @@ -367,6 +371,10 @@ class Customer(TransactionBase): if self.lead_name: frappe.db.sql("update `tabLead` set status='Interested' where name=%s", self.lead_name) + def before_rename(self, olddn, newdn, merge=False): + if merge: + validate_party_currency_before_merging("Customer", olddn, newdn) + def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default("cust_master_name") == "Customer Name": self.db_set("customer_name", newdn) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 8d251d143a9..a4a335d2c5c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -383,35 +383,23 @@ class PickList(TransactionBase): picked_items = get_picked_items_qty(packed_items, contains_packed_items=True) self.validate_picked_qty(picked_items) - picked_qty = frappe._dict() + doc_updates = {} for d in picked_items: - picked_qty[d.product_bundle_item] = d.picked_qty + doc_updates[d.product_bundle_item] = {"picked_qty": flt(d.picked_qty)} - for packed_item in packed_items: - frappe.db.set_value( - "Packed Item", - packed_item, - "picked_qty", - flt(picked_qty.get(packed_item)), - update_modified=False, - ) + if doc_updates: + frappe.db.bulk_update("Packed Item", doc_updates, update_modified=False) def update_sales_order_item_qty(self, so_items): picked_items = get_picked_items_qty(so_items) self.validate_picked_qty(picked_items) - picked_qty = frappe._dict() + doc_updates = {} for d in picked_items: - picked_qty[d.sales_order_item] = d.picked_qty + doc_updates[d.sales_order_item] = {"picked_qty": flt(d.picked_qty)} - for so_item in so_items: - frappe.db.set_value( - "Sales Order Item", - so_item, - "picked_qty", - flt(picked_qty.get(so_item)), - update_modified=False, - ) + if doc_updates: + frappe.db.bulk_update("Sales Order Item", doc_updates, update_modified=False) def update_sales_order_picking_status(self) -> None: sales_orders = [] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index ce462f73039..b5c1c38729f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -17,13 +17,6 @@ frappe.ui.form.on("Purchase Receipt", { "Landed Cost Voucher": "Landed Cost Voucher", }; - frm.set_query("expense_account", "items", function () { - return { - query: "erpnext.controllers.queries.get_expense_account", - filters: { company: frm.doc.company }, - }; - }); - frm.set_query("wip_composite_asset", "items", function () { return { filters: { is_composite_asset: 1, docstatus: 0 }, @@ -171,6 +164,16 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend this.setup_accounting_dimension_triggers(); this.setup_posting_date_time_check(); super.setup(doc); + + this.frm.set_query("expense_account", "items", () => { + return { + query: "erpnext.controllers.queries.get_expense_account", + filters: { + company: this.frm.doc.company, + disabled: 0, + }, + }; + }); } refresh() { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 55facceae13..aa22e6b8bde 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -312,6 +312,30 @@ class SerialandBatchBundle(Document): SerialNoDuplicateError, ) + if ( + self.voucher_type == "Stock Entry" + and self.type_of_transaction == "Inward" + and frappe.get_cached_value("Stock Entry", self.voucher_no, "purpose") + in ["Manufacture", "Repack"] + ): + serial_nos = frappe.get_all( + "Serial No", filters={"name": ("in", serial_nos), "status": "Delivered"}, pluck="name" + ) + + if serial_nos: + if len(serial_nos) == 1: + frappe.throw( + _( + "Serial No {0} is already Delivered. You cannot use them again in Manufacture / Repack entry." + ).format(bold(serial_nos[0])) + ) + else: + frappe.throw( + _( + "Serial Nos {0} are already Delivered. You cannot use them again in Manufacture / Repack entry." + ).format(bold(", ".join(serial_nos))) + ) + def throw_error_message(self, message, exception=frappe.ValidationError): frappe.throw(_(message), exception, title=_("Error")) diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json index 924277b30ea..9dfa2cc15c0 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.json +++ b/erpnext/stock/doctype/serial_no/serial_no.json @@ -282,7 +282,7 @@ "icon": "fa fa-barcode", "idx": 1, "links": [], - "modified": "2025-07-15 13:40:21.938700", + "modified": "2025-12-24 20:14:52.942251", "modified_by": "Administrator", "module": "Stock", "name": "Serial No", diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 896323d6529..9622cc725f6 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -302,3 +302,7 @@ def get_serial_nos_for_outward(kwargs): return [] return [d.serial_no for d in serial_nos] + + +def on_doctype_update(): + frappe.db.add_index("Serial No", ["item_code", "warehouse"]) diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 87e018d6683..e12e816db7d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2088,6 +2088,45 @@ class TestStockEntry(FrappeTestCase): self.assertEqual(incoming_rate, 125.0) + def test_prevent_reuse_delivered_serial_no_in_repack(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item = "Test Prevent Reuse Delivered Serial No" + warehouse = "_Test Warehouse - _TC" + + item_doc = make_item(item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SHGJ.####"}) + + make_stock_entry(item_code="_Test Item", target=warehouse, qty=2, rate=100) + make_stock_entry(item_code=item, target=warehouse, qty=2, rate=100) + + dn = create_delivery_note(item_code=item, qty=2) + delivered_serial_no = get_serial_nos_from_bundle(dn.get("items")[0].serial_and_batch_bundle)[0] + + se = make_stock_entry( + item_code="_Test Item", source=warehouse, qty=1, purpose="Repack", do_not_save=True + ) + se.append( + "items", + { + "item_code": item_doc.name, + "item_name": item_doc.item_name, + "s_warehouse": None, + "t_warehouse": warehouse, + "description": item_doc.description, + "uom": item_doc.stock_uom, + "qty": 1, + "use_serial_batch_fields": 1, + "serial_no": delivered_serial_no, + }, + ) + + se.save() + status = frappe.db.get_value("Serial No", delivered_serial_no, "status") + + self.assertEqual(status, "Delivered") + self.assertEqual(se.purpose, "Repack") + self.assertRaises(frappe.ValidationError, se.submit) + def make_serialized_item(**args): args = frappe._dict(args) diff --git a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py index b62a6ee6fd8..9d313b477a3 100644 --- a/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py +++ b/erpnext/stock/report/purchase_receipt_trends/purchase_receipt_trends.py @@ -20,6 +20,9 @@ def execute(filters=None): def get_chart_data(data, filters): + def wrap_in_quotes(label): + return f"'{label}'" + if not data: return [] @@ -36,6 +39,9 @@ def get_chart_data(data, filters): data = data[:10] for row in data: + if row[0] == wrap_in_quotes(_("Total")): + continue + labels.append(row[0]) datapoints.append(row[-1]) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index dab6614b514..44d54141e09 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -406,12 +406,7 @@ class SerialBatchBundle: self.update_serial_no_status_warehouse(self.sle, serial_nos) - def update_serial_no_status_warehouse(self, sle, serial_nos): - warehouse = sle.warehouse if sle.actual_qty > 0 else None - - if isinstance(serial_nos, str): - serial_nos = [serial_nos] - + def get_status_for_serial_nos(self, sle): status = "Inactive" if sle.actual_qty < 0: status = "Delivered" @@ -425,6 +420,23 @@ class SerialBatchBundle: ]: status = "Consumed" + if sle.is_cancelled == 1 and ( + sle.voucher_type in ["Purchase Invoice", "Purchase Receipt"] or status == "Consumed" + ): + status = "Inactive" + + return status + + def update_serial_no_status_warehouse(self, sle, serial_nos): + warehouse = sle.warehouse if sle.actual_qty > 0 else None + + if isinstance(serial_nos, str): + serial_nos = [serial_nos] + + status = "Active" + if not warehouse: + status = self.get_status_for_serial_nos(sle) + customer = None if sle.voucher_type in ["Sales Invoice", "Delivery Note"] and sle.actual_qty < 0: customer = frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "customer")