diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index c7e05f70d7f..abc4eec0ffb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -21,7 +21,6 @@ "party_name", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", - "advance_reconciliation_takes_effect_on", "column_break_11", "bank_account", "party_bank_account", @@ -786,18 +785,9 @@ "options": "No\nYes", "print_hide": 1, "search_index": 1 - }, - { - "default": "Oldest Of Invoice Or Advance", - "fetch_from": "company.reconciliation_takes_effect_on", - "fieldname": "advance_reconciliation_takes_effect_on", - "fieldtype": "Select", - "hidden": 1, - "label": "Advance Reconciliation Takes Effect On", - "no_copy": 1, - "options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date" } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [ @@ -809,7 +799,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2025-03-24 16:18:19.920701", + "modified": "2025-05-08 11:18:10.238085", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -849,6 +839,7 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1c91b9ef8e3..f96145b65d1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1492,9 +1492,12 @@ class PaymentEntry(AccountsController): else: # For backwards compatibility # Supporting reposting on payment entries reconciled before select field introduction - if self.advance_reconciliation_takes_effect_on == "Advance Payment Date": + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", self.company, "reconciliation_takes_effect_on" + ) + if reconciliation_takes_effect_on == "Advance Payment Date": posting_date = self.posting_date - elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" if invoice.reference_doctype in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -1504,7 +1507,7 @@ class PaymentEntry(AccountsController): if getdate(posting_date) < getdate(self.posting_date): posting_date = self.posting_date - elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date": + elif reconciliation_takes_effect_on == "Reconciliation Date": posting_date = nowdate() frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date) @@ -2361,7 +2364,7 @@ def get_outstanding_reference_documents(args, validate=False): accounts = get_party_account( args.get("party_type"), args.get("party"), args.get("company"), include_advance=True ) - advance_account = accounts[1] if len(accounts) >= 1 else None + advance_account = accounts[1] if len(accounts) > 1 else None if party_account == advance_account: party_account = accounts[0] diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 790ada3f63e..2d065632419 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -133,7 +133,12 @@ class PeriodClosingVoucher(AccountsController): self.make_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") + self.ignore_linked_doctypes = ( + "GL Entry", + "Stock Ledger Entry", + "Payment Ledger Entry", + "Account Closing Balance", + ) self.block_if_future_closing_voucher_exists() self.db_set("gle_processing_status", "In Progress") self.cancel_gl_entries() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 7c4ba47f9ba..a2c64765ed9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1695,6 +1695,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): # Configure Buying Settings to allow rate change frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + # Configure Accounts Settings to allow 300% over billing + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 300) + # Create PR: rate = 1000, qty = 5 pr = make_purchase_receipt( item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2) @@ -2756,6 +2759,43 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(invoice.grand_total, 300) + def test_pr_pi_over_billing(self): + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_purchase_invoice_from_pr, + ) + + # Configure Buying Settings to allow rate change + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + pr = make_purchase_receipt(qty=10, rate=10) + pi = make_purchase_invoice_from_pr(pr.name) + + pi.items[0].rate = 12 + + # Test 1 - This will fail because over billing is not allowed + self.assertRaises(frappe.ValidationError, pi.submit) + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + # Test 2 - This will now submit because over billing allowance is ignored when set_landed_cost_based_on_purchase_invoice_rate is checked + pi.submit() + + frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0) + frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 20) + pi.cancel() + pi = make_purchase_invoice_from_pr(pr.name) + pi.items[0].rate = 12 + + # Test 3 - This will now submit because over billing is allowed upto 20% + pi.submit() + + pi.reload() + pi.cancel() + pi = make_purchase_invoice_from_pr(pr.name) + pi.items[0].rate = 13 + + # Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail + self.assertRaises(frappe.ValidationError, pi.submit) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 28554125b67..0bb14604991 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -563,17 +563,20 @@ def get_account_type_map(company): def get_result_as_list(data, filters): - balance, _balance_in_account_currency = 0, 0 + balance = 0 for d in data: if not d.get("posting_date"): - balance, _balance_in_account_currency = 0, 0 + balance = 0 balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance d["account_currency"] = filters.account_currency + d["presentation_currency"] = filters.presentation_currency + return data @@ -599,11 +602,8 @@ def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] else: - if filters.get("company"): - currency = get_company_currency(filters["company"]) - else: - company = get_default_company() - currency = get_company_currency(company) + company = filters.get("company") or get_default_company() + filters["presentation_currency"] = currency = get_company_currency(company) columns = [ { @@ -624,19 +624,22 @@ def get_columns(filters): { "label": _("Debit ({0})").format(currency), "fieldname": "debit", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, { "label": _("Balance ({0})").format(currency), "fieldname": "balance", - "fieldtype": "Float", + "fieldtype": "Currency", + "options": "presentation_currency", "width": 130, }, ] diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 2b6280c74b5..ccb4d26f77b 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -35,7 +35,6 @@ def execute(filters=None): filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True, ) expense = get_data( @@ -46,7 +45,6 @@ def execute(filters=None): filters=filters, accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True, ) net_profit_loss = get_net_profit_loss( diff --git a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py index 816c2b9950f..137c4a86f9d 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/test_profit_and_loss_statement.py @@ -2,8 +2,9 @@ # MIT License. See license.txt import frappe +from frappe.desk.query_report import export_query from frappe.tests.utils import FrappeTestCase -from frappe.utils import getdate, today +from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.financial_statements import get_period_list @@ -57,7 +58,7 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase): period_end_date=fy.year_end_date, filter_based_on="Fiscal Year", periodicity="Monthly", - accumulated_vallues=True, + accumulated_values=False, ) def test_profit_and_loss_output_and_summary(self): @@ -90,3 +91,82 @@ class TestProfitAndLossStatement(AccountsTestMixin, FrappeTestCase): with self.subTest(current_period_key=current_period_key): self.assertEqual(acc[current_period_key], 150) self.assertEqual(acc["total"], 150) + + def test_p_and_l_export(self): + self.create_sales_invoice(qty=1, rate=150) + + filters = self.get_report_filters() + frappe.local.form_dict = frappe._dict( + { + "report_name": "Profit and Loss Statement", + "file_format_type": "CSV", + "filters": filters, + "visible_idx": [0, 1, 2, 3, 4, 5, 6], + } + ) + export_query() + contents = frappe.response["filecontent"].decode() + sales_account = frappe.db.get_value("Company", self.company, "default_income_account") + + self.assertIn(sales_account, contents) + + def test_accumulate_filter(self): + # ensure 2 fiscal years + cur_fy = self.get_fiscal_year() + find_for = add_days(cur_fy.year_start_date, -1) + _x = frappe.db.get_all( + "Fiscal Year", + filters={"disabled": 0, "year_start_date": ("<=", find_for), "year_end_date": (">=", find_for)}, + )[0] + prev_fy = frappe.get_doc("Fiscal Year", _x.name) + prev_fy.append("companies", {"company": self.company}) + prev_fy.save() + + # make SI on both of them + prev_fy_si = self.create_sales_invoice(qty=1, rate=450, do_not_submit=True) + prev_fy_si.posting_date = add_days(prev_fy.year_end_date, -1) + prev_fy_si.save().submit() + income_acc = prev_fy_si.items[0].income_account + + self.create_sales_invoice(qty=1, rate=120) + + # Unaccumualted + filters = frappe._dict( + company=self.company, + from_fiscal_year=prev_fy.name, + to_fiscal_year=cur_fy.name, + period_start_date=prev_fy.year_start_date, + period_end_date=cur_fy.year_end_date, + filter_based_on="Date Range", + periodicity="Yearly", + accumulated_values=False, + ) + result = execute(filters) + columns = [result[0][2], result[0][3]] + expected = { + "account": income_acc, + columns[0].get("fieldname"): 450.0, + columns[1].get("fieldname"): 120.0, + } + actual = [x for x in result[1] if x.get("account") == income_acc] + self.assertEqual(len(actual), 1) + actual = actual[0] + for key in expected.keys(): + with self.subTest(key=key): + self.assertEqual(expected.get(key), actual.get(key)) + + # accumualted + filters.update({"accumulated_values": True}) + expected = { + "account": income_acc, + columns[0].get("fieldname"): 450.0, + columns[1].get("fieldname"): 570.0, + } + result = execute(filters) + columns = [result[0][2], result[0][3]] + actual = [x for x in result[1] if x.get("account") == income_acc] + self.assertEqual(len(actual), 1) + actual = actual[0] + for key in expected.keys(): + with self.subTest(key=key): + self.assertEqual(expected.get(key), actual.get(key)) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b46e427382f..6fa0e3e9802 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -713,10 +713,13 @@ def update_reference_in_payment_entry( update_advance_paid = [] # Update Reconciliation effect date in reference + reconciliation_takes_effect_on = frappe.get_cached_value( + "Company", payment_entry.company, "reconciliation_takes_effect_on" + ) if payment_entry.book_advance_payments_in_separate_party_account: - if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date": + if reconciliation_takes_effect_on == "Advance Payment Date": reconcile_on = payment_entry.posting_date - elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": + elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": date_field = "posting_date" if d.against_voucher_type in ["Sales Order", "Purchase Order"]: date_field = "transaction_date" @@ -724,7 +727,7 @@ def update_reference_in_payment_entry( if getdate(reconcile_on) < getdate(payment_entry.posting_date): reconcile_on = payment_entry.posting_date - elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date": + elif reconciliation_takes_effect_on == "Reconciliation Date": reconcile_on = nowdate() reference_details.update({"reconcile_effect_on": reconcile_on}) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e1a5398db85..bae6defe6de 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -122,6 +122,7 @@ class Asset(AccountsController): # end: auto-generated types def validate(self): + self.validate_category() self.validate_precision() self.set_purchase_doc_row_item() self.validate_asset_values() @@ -343,6 +344,17 @@ class Asset(AccountsController): title=_("Missing Finance Book"), ) + def validate_category(self): + non_depreciable_category = frappe.db.get_value( + "Asset Category", self.asset_category, "non_depreciable_category" + ) + if self.calculate_depreciation and non_depreciable_category: + frappe.throw( + _( + "This asset category is marked as non-depreciable. Please disable depreciation calculation or choose a different category." + ) + ) + def validate_precision(self): if self.gross_purchase_amount: self.gross_purchase_amount = flt( diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index ea1a95e7a71..9b4ec6f67d8 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -330,45 +330,6 @@ def _make_journal_entry_for_depreciation( row.db_update() -def get_depreciation_accounts(asset_category, company): - fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None - - accounts = frappe.db.get_value( - "Asset Category Account", - filters={"parent": asset_category, "company_name": company}, - fieldname=[ - "fixed_asset_account", - "accumulated_depreciation_account", - "depreciation_expense_account", - ], - as_dict=1, - ) - - if accounts: - fixed_asset_account = accounts.fixed_asset_account - accumulated_depreciation_account = accounts.accumulated_depreciation_account - depreciation_expense_account = accounts.depreciation_expense_account - - if not accumulated_depreciation_account or not depreciation_expense_account: - accounts = frappe.get_cached_value( - "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] - ) - - if not accumulated_depreciation_account: - accumulated_depreciation_account = accounts[0] - if not depreciation_expense_account: - depreciation_expense_account = accounts[1] - - if not fixed_asset_account or not accumulated_depreciation_account or not depreciation_expense_account: - frappe.throw( - _("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format( - asset_category, company - ) - ) - - return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account - - def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account): root_type = frappe.get_value("Account", depreciation_expense_account, "root_type") @@ -721,8 +682,8 @@ def get_asset_details(asset, finance_book=None): value_after_depreciation = asset.get_value_after_depreciation(finance_book) accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(value_after_depreciation) - fixed_asset_account, accumulated_depr_account, _ = get_asset_accounts( - asset.asset_category, asset.company, accumulated_depr_amount + fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts( + asset.asset_category, asset.company ) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center @@ -738,9 +699,13 @@ def get_asset_details(asset, finance_book=None): ) -def get_asset_accounts(asset_category, company, accumulated_depr_amount): +def get_depreciation_accounts(asset_category, company): fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None + non_depreciable_category = frappe.db.get_value( + "Asset Category", asset_category, "non_depreciable_category" + ) + accounts = frappe.db.get_value( "Asset Category Account", filters={"parent": asset_category, "company_name": company}, @@ -760,7 +725,7 @@ def get_asset_accounts(asset_category, company, accumulated_depr_amount): if not fixed_asset_account: frappe.throw(_("Please set Fixed Asset Account in Asset Category {0}").format(asset_category)) - if accumulated_depr_amount: + if not non_depreciable_category: accounts = frappe.get_cached_value( "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] ) diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json index a25f5469039..575b7a0c45f 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.json +++ b/erpnext/assets/doctype/asset_category/asset_category.json @@ -12,6 +12,7 @@ "column_break_3", "depreciation_options", "enable_cwip_accounting", + "non_depreciable_category", "finance_book_detail", "finance_books", "section_break_2", @@ -63,10 +64,16 @@ "fieldname": "enable_cwip_accounting", "fieldtype": "Check", "label": "Enable Capital Work in Progress Accounting" + }, + { + "default": "0", + "fieldname": "non_depreciable_category", + "fieldtype": "Check", + "label": "Non Depreciable Category" } ], "links": [], - "modified": "2021-02-24 15:05:38.621803", + "modified": "2025-05-13 15:33:03.791814", "modified_by": "Administrator", "module": "Assets", "name": "Asset Category", @@ -111,8 +118,9 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 8c2d301a895..16e564ad1bd 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -17,15 +17,14 @@ class AssetCategory(Document): if TYPE_CHECKING: from frappe.types import DF - from erpnext.assets.doctype.asset_category_account.asset_category_account import ( - AssetCategoryAccount, - ) + from erpnext.assets.doctype.asset_category_account.asset_category_account import AssetCategoryAccount from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook accounts: DF.Table[AssetCategoryAccount] asset_category_name: DF.Data enable_cwip_accounting: DF.Check finance_books: DF.Table[AssetFinanceBook] + non_depreciable_category: DF.Check # end: auto-generated types def validate(self): diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 1c8a80a0ed6..b78eae5f109 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -111,6 +111,13 @@ frappe.ui.form.on("Opportunity", { }, __("Create") ); + + let company_currency = erpnext.get_currency(frm.doc.company); + if (company_currency != frm.doc.currency) { + frm.add_custom_button(__("Fetch Latest Exchange Rate"), function () { + frm.trigger("currency"); + }); + } } if (!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus == 0) { @@ -152,7 +159,7 @@ frappe.ui.form.on("Opportunity", { currency: function (frm) { let company_currency = erpnext.get_currency(frm.doc.company); - if (company_currency != frm.doc.company) { + if (company_currency != frm.doc.currency) { frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { @@ -278,7 +285,6 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { } this.setup_queries(); - this.frm.trigger("currency"); } refresh() { diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index dfa02c03772..cd57c7c24f8 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2665,6 +2665,81 @@ class TestWorkOrder(FrappeTestCase): ) frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on) + def test_operations_time_planning_calculation(self): + from erpnext.manufacturing.doctype.routing.test_routing import create_routing, setup_operations + + operations = [ + {"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 1}, + {"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 4}, + {"operation": "Test Operation C", "workstation": "Test Workstation A", "time_in_mins": 3}, + {"operation": "Test Operation D", "workstation": "Test Workstation A", "time_in_mins": 2}, + ] + setup_operations(operations) + routing_doc = create_routing(routing_name="Testing Route", operations=operations) + bom = make_bom( + item="_Test FG Item", raw_materials=["_Test Item"], with_operations=1, routing=routing_doc.name + ) + + wo = make_wo_order_test_record( + item="_Test FG Item", + bom_no=bom.name, + qty=5, + source_warehouse="_Test Warehouse 1 - _TC", + skip_transfer=1, + fg_warehouse="_Test Warehouse 2 - _TC", + ) + + # Initial check + self.assertEqual(wo.operations[0].operation, "Test Operation A") + self.assertEqual(wo.operations[1].operation, "Test Operation B") + self.assertEqual(wo.operations[2].operation, "Test Operation C") + self.assertEqual(wo.operations[3].operation, "Test Operation D") + + wo = frappe.copy_doc(wo) + wo.operations[3].sequence_id = 2 + wo.submit() + + # Test 2 : Sort line items in child table based on sequence ID + self.assertEqual(wo.operations[0].operation, "Test Operation A") + self.assertEqual(wo.operations[1].operation, "Test Operation B") + self.assertEqual(wo.operations[2].operation, "Test Operation D") + self.assertEqual(wo.operations[3].operation, "Test Operation C") + + wo = frappe.copy_doc(wo) + wo.operations[3].sequence_id = 1 + wo.submit() + + self.assertEqual(wo.operations[0].operation, "Test Operation A") + self.assertEqual(wo.operations[1].operation, "Test Operation C") + self.assertEqual(wo.operations[2].operation, "Test Operation B") + self.assertEqual(wo.operations[3].operation, "Test Operation D") + + wo = frappe.copy_doc(wo) + wo.operations[0].sequence_id = 3 + wo.submit() + + self.assertEqual(wo.operations[0].operation, "Test Operation C") + self.assertEqual(wo.operations[1].operation, "Test Operation B") + self.assertEqual(wo.operations[2].operation, "Test Operation D") + self.assertEqual(wo.operations[3].operation, "Test Operation A") + + wo = frappe.copy_doc(wo) + wo.operations[1].sequence_id = 0 + + # Test 3 - Error should be thrown if any one operation does not have sequence id but others do + self.assertRaises(frappe.ValidationError, wo.submit) + + workstation = frappe.get_doc("Workstation", "Test Workstation A") + workstation.production_capacity = 4 + workstation.save() + + wo = frappe.copy_doc(wo) + wo.operations[1].sequence_id = 2 + wo.submit() + + # Test 4 - If Sequence ID is same then planned start time for both operations should be same + self.assertEqual(wo.operations[1].planned_start_time, wo.operations[2].planned_start_time) + def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 63c74b61c4d..8231e924cb0 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-01-10 16:34:16", + "creation": "2025-04-09 12:09:40.634472", "doctype": "DocType", "document_type": "Setup", "engine": "InnoDB", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 0bf383de285..270c23e913d 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -624,19 +624,30 @@ class WorkOrder(Document): enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 - for index, row in enumerate(self.operations): + if all([op.sequence_id for op in self.operations]): + self.operations = sorted(self.operations, key=lambda op: op.sequence_id) + for idx, op in enumerate(self.operations): + op.idx = idx + 1 + elif any([op.sequence_id for op in self.operations]): + frappe.throw( + _( + "Row #{0}: Incorrect Sequence ID. If any single operation has a Sequence ID then all other operations must have one too." + ).format(next((op.idx for op in self.operations if not op.sequence_id), None)) + ) + + for idx, row in enumerate(self.operations): qty = self.qty while qty > 0: qty = split_qty_based_on_batch_size(self, row, qty) if row.job_card_qty > 0: - self.prepare_data_for_job_card(row, index, plan_days, enable_capacity_planning) + self.prepare_data_for_job_card(row, idx, plan_days, enable_capacity_planning) planned_end_date = self.operations and self.operations[-1].planned_end_time if planned_end_date: self.db_set("planned_end_date", planned_end_date) - def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning): - self.set_operation_start_end_time(index, row) + def prepare_data_for_job_card(self, row, idx, plan_days, enable_capacity_planning): + self.set_operation_start_end_time(row, idx) job_card_doc = create_job_card( self, row, auto_create=True, enable_capacity_planning=enable_capacity_planning @@ -661,12 +672,24 @@ class WorkOrder(Document): row.db_update() - def set_operation_start_end_time(self, idx, row): + def set_operation_start_end_time(self, row, idx): """Set start and end time for given operation. If first operation, set start as `planned_start_date`, else add time diff to end time of earlier operation.""" if idx == 0: # first operation at planned_start date row.planned_start_time = self.planned_start_date + elif self.operations[idx - 1].sequence_id: + if self.operations[idx - 1].sequence_id == row.sequence_id: + row.planned_start_time = self.operations[idx - 1].planned_start_time + else: + last_ops_with_same_sequence_ids = sorted( + [op for op in self.operations if op.sequence_id == self.operations[idx - 1].sequence_id], + key=lambda op: get_datetime(op.planned_end_time), + ) + row.planned_start_time = ( + get_datetime(last_ops_with_same_sequence_ids[-1].planned_end_time) + + get_mins_between_operations() + ) else: row.planned_start_time = ( get_datetime(self.operations[idx - 1].planned_end_time) + get_mins_between_operations() diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json index de1f67f13fd..0185812a4b6 100644 --- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json +++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json @@ -1,6 +1,6 @@ { "actions": [], - "creation": "2014-10-16 14:35:41.950175", + "creation": "2025-04-09 12:12:19.824560", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -102,13 +102,15 @@ "fieldname": "planned_start_time", "fieldtype": "Datetime", "label": "Planned Start Time", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "planned_end_time", "fieldtype": "Datetime", "label": "Planned End Time", - "no_copy": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "column_break_10", @@ -191,7 +193,6 @@ { "fieldname": "sequence_id", "fieldtype": "Int", - "hidden": 1, "label": "Sequence ID", "print_hide": 1 }, @@ -219,10 +220,11 @@ "read_only": 1 } ], + "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-06-09 14:03:01.612909", + "modified": "2025-04-09 16:21:47.110564", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order Operation", @@ -232,4 +234,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0bf4128551e..cf8ae44a8d7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -389,7 +389,6 @@ erpnext.patches.v15_0.enable_allow_existing_serial_no erpnext.patches.v15_0.update_cc_in_process_statement_of_accounts erpnext.patches.v15_0.update_asset_status_to_work_in_progress erpnext.patches.v15_0.rename_manufacturing_settings_field -erpnext.patches.v15_0.migrate_checkbox_to_select_for_reconciliation_effect erpnext.patches.v15_0.sync_auto_reconcile_config execute:frappe.db.set_single_value("Accounts Settings", "exchange_gain_loss_posting_date", "Payment") erpnext.patches.v14_0.disable_add_row_in_gross_profit diff --git a/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py b/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py deleted file mode 100644 index 883921cfdf8..00000000000 --- a/erpnext/patches/v15_0/migrate_checkbox_to_select_for_reconciliation_effect.py +++ /dev/null @@ -1,18 +0,0 @@ -import frappe - - -def execute(): - """ - A New select field 'reconciliation_takes_effect_on' has been added to control Advance Payment Reconciliation dates. - Migrate old checkbox configuration to new select field on 'Company' and 'Payment Entry' - """ - companies = frappe.db.get_all("Company", fields=["name", "reconciliation_takes_effect_on"]) - for x in companies: - new_value = ( - "Advance Payment Date" if x.reconcile_on_advance_payment_date else "Oldest Of Invoice Or Advance" - ) - frappe.db.set_value("Company", x.name, "reconciliation_takes_effect_on", new_value) - - frappe.db.sql( - """update `tabPayment Entry` set advance_reconciliation_takes_effect_on = if(reconcile_on_advance_payment_date = 0, 'Oldest Of Invoice Or Advance', 'Advance Payment Date')""" - ) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 7d194b7c37e..758b25f0de0 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -535,7 +535,7 @@ def get_timesheets_list(doctype, txt, filters, limit_start, limit_page_length=20 table.name, child_table.activity_type, table.status, - table.total_billable_hours, + child_table.billing_hours, (table.sales_invoice | child_table.sales_invoice).as_("sales_invoice"), child_table.project, ) diff --git a/erpnext/public/js/event.js b/erpnext/public/js/event.js index a6733915a5c..2950ace888d 100644 --- a/erpnext/public/js/event.js +++ b/erpnext/public/js/event.js @@ -47,7 +47,7 @@ frappe.ui.form.on("Event", { frm.add_custom_button( __("Add Sales Partners"), function () { - new frappe.desk.eventParticipants(frm, "Sales Partners"); + new frappe.desk.eventParticipants(frm, "Sales Partner"); }, __("Add Participants") ); 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 9be5a656a8e..640577bb72c 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -147,10 +147,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te bin_join_selection, bin_join_condition = "", "" if hide_unavailable_items: - bin_join_selection = ", `tabBin` bin" - bin_join_condition = ( - "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" - ) + bin_join_selection = "LEFT JOIN `tabBin` bin ON bin.item_code = item.name" + bin_join_condition = "AND item.is_stock_item = 0 OR (item.is_stock_item = 1 AND bin.warehouse = %(warehouse)s AND bin.actual_qty > 0)" items_data = frappe.db.sql( """ diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py index f8cd61d50ea..3cc405aa658 100644 --- a/erpnext/setup/setup_wizard/operations/taxes_setup.py +++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py @@ -214,13 +214,12 @@ def get_or_create_account(company_name, account): default_root_type = "Liability" root_type = account.get("root_type", default_root_type) + or_filters = {"account_name": account.get("account_name")} + if account.get("account_number"): + or_filters.update({"account_number": account.get("account_number")}) + existing_accounts = frappe.get_all( - "Account", - filters={"company": company_name, "root_type": root_type}, - or_filters={ - "account_name": account.get("account_name"), - "account_number": account.get("account_number"), - }, + "Account", filters={"company": company_name, "root_type": root_type}, or_filters=or_filters ) if existing_accounts: diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b7cf97589af..6d4a348ef9d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1081,6 +1081,7 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty buying_settings = frappe.get_single("Buying Settings") + over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -1119,6 +1120,14 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) + elif item.billed_amt > item.amount: + per_over_billed = (flt(item.billed_amt / item.amount, 2) * 100) - 100 + if per_over_billed > over_billing_allowance: + frappe.throw( + _("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format( + item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance + ) + ) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ecbb10b6cb5..06598aa285b 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -342,7 +342,7 @@ def remove_attached_file(docname): if file_name := frappe.db.get_value( "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name" ): - frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) + frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True, force=True) def repost_sl_entries(doc): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a23ac8342a0..ca5e3f7eb10 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -6,7 +6,7 @@ import json from collections import defaultdict import frappe -from frappe import _ +from frappe import _, bold from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum from frappe.utils import ( @@ -526,6 +526,14 @@ class StockEntry(StockController): OpeningEntryAccountError, ) + if self.purpose != "Material Issue" and acc_details.account_type == "Cost of Goods Sold": + frappe.msgprint( + _( + "At row {0}: You have selected the Difference Account {1}, which is a Cost of Goods Sold type account. Please select a different account" + ).format(d.idx, bold(get_link_to_form("Account", d.expense_account))), + title=_("Warning : Cost of Goods Sold Account"), + ) + def validate_warehouse(self): """perform various (sometimes conditional) validations on warehouse""" diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 76a540f3e92..4ea683a904d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -203,9 +203,19 @@ class StockReconciliation(StockController): ) ) - if self.docstatus == 1: - bundle.voucher_no = self.name - bundle.submit() + if ( + self.docstatus == 1 + and item.current_serial_and_batch_bundle + and frappe.db.get_value( + "Serial and Batch Bundle", item.current_serial_and_batch_bundle, "docstatus" + ) + == 0 + ): + sabb_doc = frappe.get_doc( + "Serial and Batch Bundle", item.current_serial_and_batch_bundle + ) + sabb_doc.voucher_no = self.name + sabb_doc.submit() item.db_set( { diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.js b/erpnext/stock/report/available_serial_no/available_serial_no.js index 17f8c666e04..c69c6503de8 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.js +++ b/erpnext/stock/report/available_serial_no/available_serial_no.js @@ -51,49 +51,11 @@ frappe.query_reports["Available Serial No"] = { }; }, }, - { - fieldname: "item_group", - label: __("Item Group"), - fieldtype: "Link", - options: "Item Group", - }, - { - fieldname: "batch_no", - label: __("Batch No"), - fieldtype: "Link", - options: "Batch", - on_change() { - const batch_no = frappe.query_report.get_filter_value("batch_no"); - if (batch_no) { - frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 1); - } else { - frappe.query_report.set_filter_value("segregate_serial_batch_bundle", 0); - } - }, - }, - { - fieldname: "brand", - label: __("Brand"), - fieldtype: "Link", - options: "Brand", - }, { fieldname: "voucher_no", label: __("Voucher #"), fieldtype: "Data", }, - { - fieldname: "project", - label: __("Project"), - fieldtype: "Link", - options: "Project", - }, - { - fieldname: "include_uom", - label: __("Include UOM"), - fieldtype: "Link", - options: "UOM", - }, { fieldname: "valuation_field_type", label: __("Valuation Field Type"), diff --git a/erpnext/stock/report/available_serial_no/available_serial_no.py b/erpnext/stock/report/available_serial_no/available_serial_no.py index bdde9c7f3b6..6911b979ae4 100644 --- a/erpnext/stock/report/available_serial_no/available_serial_no.py +++ b/erpnext/stock/report/available_serial_no/available_serial_no.py @@ -3,108 +3,62 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum from frappe.utils import cint, flt -from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_serial_nos_from_sle_list -from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for from erpnext.stock.report.stock_ledger.stock_ledger import ( - check_inventory_dimension_filters_applied, get_item_details, - get_item_group_condition, get_opening_balance, - get_opening_balance_from_batch, get_stock_ledger_entries, ) -from erpnext.stock.utils import ( - is_reposting_item_valuation_in_progress, - update_included_uom_in_report, -) +from erpnext.stock.utils import is_reposting_item_valuation_in_progress def execute(filters=None): is_reposting_item_valuation_in_progress() - include_uom = filters.get("include_uom") columns = get_columns(filters) items = get_items(filters) sl_entries = get_stock_ledger_entries(filters, items) - item_details = get_item_details(items, sl_entries, include_uom) + item_details = get_item_details(items, sl_entries, False) - opening_row, actual_qty, stock_value = get_opening_balance_data(filters, columns, sl_entries) + opening_row = get_opening_balance_data(filters, columns, sl_entries) precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) - data, conversion_factors = process_stock_ledger_entries( - filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision - ) + data = process_stock_ledger_entries(sl_entries, item_details, opening_row, precision) - update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data def get_opening_balance_data(filters, columns, sl_entries): - if filters.get("batch_no"): - opening_row = get_opening_balance_from_batch(filters, columns, sl_entries) - else: - opening_row = get_opening_balance(filters, columns, sl_entries) - - actual_qty = opening_row.get("qty_after_transaction") if opening_row else 0 - stock_value = opening_row.get("stock_value") if opening_row else 0 - return opening_row, actual_qty, stock_value + opening_row = get_opening_balance(filters, columns, sl_entries) + return opening_row -def process_stock_ledger_entries( - filters, sl_entries, item_details, opening_row, actual_qty, stock_value, precision -): +def process_stock_ledger_entries(sl_entries, item_details, opening_row, precision): data = [] - conversion_factors = [] if opening_row: data.append(opening_row) - conversion_factors.append(0) - batch_balance_dict = frappe._dict({}) + available_serial_nos = {} + if sabb_list := [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle]: + available_serial_nos = get_serial_nos_from_sle_list(sabb_list) - if actual_qty and filters.get("batch_no"): - batch_balance_dict[filters.batch_no] = [actual_qty, stock_value] - - available_serial_nos = get_serial_nos_from_sle_list( - [sle.serial_and_batch_bundle for sle in sl_entries if sle.serial_and_batch_bundle] - ) + if not available_serial_nos: + return [], [] for sle in sl_entries: - update_stock_ledger_entry( - sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision - ) + update_stock_ledger_entry(sle, item_details, precision) update_available_serial_nos(available_serial_nos, sle) data.append(sle) - if filters.get("include_uom"): - conversion_factors.append(item_details[sle.item_code].conversion_factor) - - return data, conversion_factors + return data -def update_stock_ledger_entry( - sle, item_details, filters, actual_qty, stock_value, batch_balance_dict, precision -): +def update_stock_ledger_entry(sle, item_details, precision): item_detail = item_details[sle.item_code] sle.update(item_detail) - if filters.get("batch_no") or check_inventory_dimension_filters_applied(filters): - actual_qty += flt(sle.actual_qty, precision) - stock_value += sle.stock_value_difference - - if sle.batch_no: - batch_balance_dict.setdefault(sle.batch_no, [0, 0]) - batch_balance_dict[sle.batch_no][0] += sle.actual_qty - - if sle.voucher_type == "Stock Reconciliation" and not sle.actual_qty: - actual_qty = sle.qty_after_transaction - stock_value = sle.stock_value - - sle.update({"qty_after_transaction": actual_qty, "stock_value": stock_value}) - sle.update({"in_qty": max(sle.actual_qty, 0), "out_qty": min(sle.actual_qty, 0)}) if sle.actual_qty: @@ -120,13 +74,10 @@ def update_available_serial_nos(available_serial_nos, sle): else available_serial_nos.get(sle.serial_and_batch_bundle) ) key = (sle.item_code, sle.warehouse) + sle.serial_no = "\n".join(serial_nos) if serial_nos else "" if key not in available_serial_nos: - stock_balance = get_stock_balance_for( - sle.item_code, sle.warehouse, sle.posting_date, sle.posting_time - ) - serials = get_serial_nos(stock_balance["serial_nos"]) if stock_balance["serial_nos"] else [] - available_serial_nos.setdefault(key, serials) - sle.balance_serial_no = "\n".join(serials) + available_serial_nos.setdefault(key, serial_nos) + sle.balance_serial_no = "\n".join(serial_nos) return existing_serial_no = available_serial_nos[key] @@ -151,25 +102,14 @@ def get_columns(filters): }, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, { - "label": _("Stock UOM"), + "label": _("UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", - "width": 90, + "width": 60, }, ] - for dimension in get_inventory_dimensions(): - columns.append( - { - "label": _(dimension.doctype), - "fieldname": dimension.fieldname, - "fieldtype": "Link", - "options": dimension.doctype, - "width": 110, - } - ) - columns.extend( [ { @@ -201,20 +141,11 @@ def get_columns(filters): "width": 150, }, { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 100, + "label": _("Serial No (In/Out)"), + "fieldname": "serial_no", + "width": 150, }, - { - "label": _("Brand"), - "fieldname": "brand", - "fieldtype": "Link", - "options": "Brand", - "width": 100, - }, - {"label": _("Description"), "fieldname": "description", "width": 200}, + {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 150}, { "label": _("Incoming Rate"), "fieldname": "incoming_rate", @@ -257,28 +188,6 @@ def get_columns(filters): "width": 110, "options": "Company:company:default_currency", }, - {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, - { - "label": _("Voucher #"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 100, - }, - { - "label": _("Batch"), - "fieldname": "batch_no", - "fieldtype": "Link", - "options": "Batch", - "width": 100, - }, - { - "label": _("Serial No"), - "fieldname": "serial_no", - "fieldtype": "Link", - "options": "Serial No", - "width": 100, - }, { "label": _("Serial and Batch Bundle"), "fieldname": "serial_and_batch_bundle", @@ -286,12 +195,12 @@ def get_columns(filters): "options": "Serial and Batch Bundle", "width": 100, }, - {"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100}, + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, { - "label": _("Project"), - "fieldname": "project", - "fieldtype": "Link", - "options": "Project", + "label": _("Voucher #"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", "width": 100, }, { @@ -310,19 +219,8 @@ def get_columns(filters): def get_items(filters): item = frappe.qb.DocType("Item") query = frappe.qb.from_(item).select(item.name).where(item.has_serial_no == 1) - conditions = [] if item_code := filters.get("item_code"): - conditions.append(item.name == item_code) - else: - if brand := filters.get("brand"): - conditions.append(item.brand == brand) - if item_group := filters.get("item_group"): - if condition := get_item_group_condition(item_group, item): - conditions.append(condition) - - if conditions: - for condition in conditions: - query = query.where(condition) + query = query.where(item.name == item_code) return query.run(pluck=True) diff --git a/erpnext/templates/includes/timesheet/timesheet_row.html b/erpnext/templates/includes/timesheet/timesheet_row.html index 0f9cc77e89d..8905262a88e 100644 --- a/erpnext/templates/includes/timesheet/timesheet_row.html +++ b/erpnext/templates/includes/timesheet/timesheet_row.html @@ -5,7 +5,7 @@ {{ doc.name }} -
{{ doc.total_billable_hours }}
+
{{ doc.billing_hours }}
{{ doc.project or '' }}
{{ doc.sales_invoice or '' }}
{{ _(doc.activity_type) }}