Skip to content

Fix due date orders#24

Closed
ljain112 wants to merge 159 commits into
developfrom
fix-due-date-orders
Closed

Fix due date orders#24
ljain112 wants to merge 159 commits into
developfrom
fix-due-date-orders

Conversation

@ljain112

@ljain112 ljain112 commented Jul 31, 2025

Copy link
Copy Markdown
Owner

The due date on the payment schedule should be dynamically derived based on the payment terms (credit days) on the PO and Bill date or the posting date of the invoice. Currently, it's just copying whatever due date is on the PO/SO schedule. For blanket orders, the PO / SO date could be sometime in past (say 1st Jan), and the due date could be 31st Jan (30 days). However, when the invoice is submitted on 1st Mar, the system should calculate the due date as 31st Mar (1st + 30 days) instead of updating the due date as 30th Jan (doing the latter marks the invoice as overdue on submit).

New behaviour:

  • Storing fields at the invoice level because they are also present in the Payment terms template.

    • due_date_based_on
    • credit_days,credit_months
    • discount_validity_based_on
    • discount_validity
  • fields will be read only

  • If the due_date or discount_date is updated after auto-setting, fields will be set as empty.

  • On mapping, if automatically_fetch_payment_terms is enabled, then due_date and discount_date will be recalculated.

no-docs

Further enhancement that can be done:

  • Allowing setting these conditions at the doc level and updating due_date based on that.

Summary by CodeRabbit

  • New Features

    • Payment schedules display due-date/credit and discount-validity metadata; selective child-item mapping for document imports; warehouse-aware barcode scanning with “Last Scanned Warehouse”; POS-driven default payment behavior; BOM semi-finished goods and workstation operating-cost components.
  • Bug Fixes

    • Harder due/discount date alignment; prevent duplicate reverse journal creation; validate bundle item quantities; prevent saving assets linked to draft purchase docs.
  • Tests

    • Added/updated tests for dunning, workstation costs, payment-term flows and stock/material transfer scenarios.

@coderabbitai

coderabbitai Bot commented Jul 31, 2025

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

Adds payment schedule metadata and propagation of computed due/discount dates, makes barcode scanning warehouse-aware with virtual "last_scanned_warehouse" fields and UI, enables selective child-item mapping via JSON args.filtered_children, migrates workstation operating components to a table model with migration, refactors many raw SQL queries to Query Builder, and adjusts dunning/payment flows and related tests.

Changes

Cohort / File(s) Change Summary
Payment Schedule & Accounts Controller
erpnext/accounts/doctype/payment_schedule/payment_schedule.json, erpnext/accounts/doctype/payment_schedule/payment_schedule.py, erpnext/controllers/accounts_controller.py, erpnext/accounts/utils.py
Added read-only due/discount metadata fields and reordered fields in Payment Schedule; extended PaymentSchedule typings; accounts_controller computes and propagates due_date/discount_date (get_due_date default_date, new get_discount_date); update_voucher_outstanding now invokes update_linked_dunnings.
Frontend: payment-term UI
erpnext/public/js/controllers/transaction.js
Added discount_date handler; updated due_date and payment_term handlers to clear dependent fields and update rows directly; refresh payment_schedule after fetch.
Tests: automatically_fetch_payment_terms → decorator
Multiple tests under erpnext/*/doctype/*/test_*.py (sales_order, purchase_order, purchase_invoice, delivery_note, purchase_receipt)
Replaced manual helper toggles with @IntegrationTestCase.change_settings decorators; removed helper; compare logic updated to compute due_date via get_due_date when needed.
Dunning & tests
erpnext/accounts/doctype/dunning/dunning.py, erpnext/accounts/doctype/dunning/test_dunning.py
Replaced resolve_dunning with update_linked_dunnings(doc, previous_outstanding_amount); batch-check/update of linked Dunnings; added tests covering credit-note interactions.
Selective child-item mapping (filtered_children)
Multiple mapping entrypoints: quotation.py, sales_order.py, delivery_note.py, purchase_order.py, purchase_receipt.py, purchase_invoice.py, pick_list.py, stock/*
Added optional args/kwargs (support JSON string) and per-call filtered_children; mapping conditions updated to call select_item to allow selecting specific child rows in mapping flows.
Barcode scanner & Last Scanned Warehouse (JS + JSON + CSS + doctypes)
erpnext/public/js/utils/barcode_scanner.js, erpnext/public/scss/erpnext.scss, many */doctype/*/*.json and form JS files (POS/Sales/Purchase/Stock/Delivery/Quotation/Material Request/etc.), StockEntry/StockReconciliation
Made barcode scanner warehouse-aware: last_scanned_warehouse state, warehouse-scan handling, clear UI control/button, ctx.set_warehouse in API calls, warehouse-aware row matching and per-row set_warehouse; added virtual last_scanned_warehouse fields to many doctypes and reused persistent scanner instances on forms.
Workstation operating components & migration
erpnext/manufacturing/doctype/workstation_type/*, erpnext/manufacturing/doctype/workstation/*, new workstation_cost and workstation_operating_component doctypes, tests, erpnext/patches/v16_0/make_workstation_operating_components.py
Replaced discrete hour_rate_* fields with workstation_costs table; new Workstation Cost and Workstation Operating Component doctypes; validation to prevent duplicate components; migration patch to populate table from existing fields; test scaffolds added.
BOM / sub-assembly handling & UI
erpnext/manufacturing/doctype/bom/*, bom_item, bom_explosion_item, production_plan
Introduced is_sub_assembly_item flag across exploded items and BOM flows, UI toggles for track_semi_finished_goods, adjusted production plan logic to skip/handle BOMs with tracking.
Asset depreciation report adjustments
erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
Added asset value adjustment SQL maps (by category and by asset) and apply per-category/asset adjustments to depreciation/balance computations.
Journal / Payment entry and hooks
erpnext/accounts/doctype/journal_entry/journal_entry.py, erpnext/accounts/doctype/payment_entry/payment_entry.py, erpnext/hooks.py
Prevent duplicate submitted reverse journal entries via pre-check; reordered update_payment_schedule calls on submit/cancel; removed dunning resolve handler from Payment Entry hooks.
Query Builder refactors (SQL → QB)
Multiple files: chart_of_accounts_importer.py, request_for_quotation.py, controllers/queries.py, projects/.../timesheet.py, stock/.../material_request.py
Replaced raw SQL with frappe.qb query builder calls and improved parameter/ordering handling.
Stock / transfers / pick list / stock entry changes
erpnext/stock/doctype/putaway_rule/putaway_rule.py, stock_entry/*, pick_list/*, stock_reconciliation/*
Prefer t_warehouse in no-rule path; persistent barcode scanners on StockEntry/StockReconciliation; added test for material transfer additional_costs; pick_manually toggles warehouse editability; delivery note / pick list / PR mappings accept filtered-child selection and kwargs.
Taxes & totals, async changes, JS formatting
erpnext/public/js/controllers/taxes_and_totals.js, many public JS controllers
calculate_taxes_and_totals made async and awaits shipping charges; inclusive tax validation added; widespread JS formatting and signature normalization.
Misc: CI, pre-commit, CODEOWNERS, small fixes
.github/workflows/*, .pre-commit-config.yaml, .coderabbit.yml, CODEOWNERS, erpnext/patches.txt, small Python sanitization changes
CI workflows updated to ignore extra config files; Prettier excludes removed for some JS files; coderabbit config added; CODEOWNERS entry updated; patches appended; minor sanitization refactors (brand/item defaults, etc.).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as transaction.js
    participant Controller as AccountsController
    participant DB

    User->>UI: change payment term / edit due or discount date
    UI->>UI: clear dependent fields (due_date_based_on, credit_days/months, discount_validity_*)
    UI->>Controller: fetch payment term details (term, posting_date/bill_date)
    Controller->>DB: load term/template and schedules
    Controller->>Controller: compute due_date via get_due_date(...), discount_date via get_discount_date(...)
    Controller-->>UI: return term_details including computed dates and metadata
    UI->>UI: update payment_schedule rows and refresh UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Fix ple #9 — overlaps on changes to update_voucher_outstanding in erpnext/accounts/utils.py; strong code-level relation.

Suggested labels

needs-tests

Poem

I nibble code beneath the moon,
I add small fields and hum a tune.
Scanners learn where warehouses hide,
Schedules store dates they once would hide.
A rabbit's patch—tidy, quick, and bright. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-due-date-orders

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
erpnext/stock/doctype/delivery_note/test_delivery_note.py (1)

1318-1319: Prefer a single decorator style throughout the file

Everywhere else the test suite uses the short alias @change_settings, but here the fully-qualified @IntegrationTestCase.change_settings form is introduced.
Both are equivalent, yet mixing styles makes the codebase a little noisier.

-@IntegrationTestCase.change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
+@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})

Sticking to the already-imported alias keeps the file uniform and a tad easier to scan.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 848d4d3 and 81dd6d3.

📒 Files selected for processing (10)
  • erpnext/accounts/doctype/payment_schedule/payment_schedule.json (3 hunks)
  • erpnext/accounts/doctype/payment_schedule/payment_schedule.py (1 hunks)
  • erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json (2 hunks)
  • erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py (1 hunks)
  • erpnext/buying/doctype/purchase_order/test_purchase_order.py (3 hunks)
  • erpnext/controllers/accounts_controller.py (4 hunks)
  • erpnext/public/js/controllers/transaction.js (3 hunks)
  • erpnext/selling/doctype/sales_order/test_sales_order.py (3 hunks)
  • erpnext/stock/doctype/delivery_note/test_delivery_note.py (1 hunks)
  • erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
erpnext/controllers/accounts_controller.py (1)
erpnext/accounts/party.py (1)
  • get_due_date (625-646)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Patch Test
🔇 Additional comments (22)
erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json (2)

138-144: Discount validity no longer mandatory when a discount exists – deliberate or oversight?

By dropping the former mandatory_depends_on constraint, the system now allows a discount to be defined without any validity period.
If invoices still rely on discount_validity for early-payment logic, this can silently introduce perpetual discounts.

Server-side validation (e.g., in validate()) that raises if discount > 0 and discount_validity is empty would keep data quality intact.


160-160: Check framework compatibility for row_format: "Dynamic"

The row_format flag is only recognised from Frappe v15 onward. Sites on v13/v14 will ignore it or, worse, error when loading the grid.

Verify the minimum supported Frappe version in this project and, if needed, wrap this change in a version-guarded patch/migration.

erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py (1)

1204-1204: Excellent refactoring to use settings decorator!

This change improves test isolation by using the @IntegrationTestCase.change_settings decorator instead of manual setup/teardown of the automatically_fetch_payment_terms setting. The decorator ensures proper cleanup and makes the test intent clearer, aligning well with the PR's payment terms automation objectives.

erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py (1)

2150-2150: LGTM! Excellent refactoring to use settings decorator.

This change improves test organization by using a declarative decorator approach instead of manual setting enable/disable calls. This ensures proper cleanup and is consistent with the broader standardization effort across test modules.

erpnext/accounts/doctype/payment_schedule/payment_schedule.py (4)

20-21: LGTM! Clean addition of credit duration fields.

The credit_days and credit_months fields are properly typed and have descriptive names that clearly indicate their purpose for credit term calculations.


26-26: LGTM! Appropriate addition of discount validity duration field.

The discount_validity field is properly typed as an integer and complements the discount validity calculation functionality.


27-32: LGTM! Well-designed literal field for discount validity calculation methods.

The discount_validity_based_on field provides comprehensive and clearly named options for different discount validity calculation approaches. The literal values are descriptive and cover the expected use cases.


35-40: LGTM! Critical field addition that addresses the main PR objective.

The due_date_based_on field directly addresses the PR's core issue by providing flexible options for due date calculation methods. This enables proper recalculation of due dates based on invoice date rather than incorrectly copying from PO/SO schedules, which was the main problem described in the PR objectives.

erpnext/selling/doctype/sales_order/test_sales_order.py (3)

14-14: LGTM: Import addition supports new due date calculation logic.

The addition of get_due_date import is necessary for the enhanced payment schedule comparison and follows proper import conventions.


1681-1681: LGTM: Cleaner test configuration using decorator pattern.

The use of @IntegrationTestCase.change_settings decorator is a better approach than manual setting manipulation, ensuring proper cleanup and consistency across test files.


2571-2576: LGTM: Implements dynamic due date calculation as per PR objectives.

The enhanced logic correctly:

  • Determines posting_date from available date fields (bill_date, posting_date, transaction_date)
  • Conditionally recalculates due_date using get_due_date when due_date_based_on is set
  • Uses computed due_date for comparison instead of stored values

This aligns perfectly with the PR goal to fix due date calculations by recalculating based on payment terms rather than copying stale dates from orders.

erpnext/public/js/controllers/transaction.js (3)

1106-1112: LGTM! Well-implemented discount date handler.

The new discount_date method correctly clears related discount validity fields when the discount date is manually changed, maintaining consistency with the payment terms auto-calculation logic.


1114-1122: LGTM! Proper handling of due date changes in child tables.

The enhanced due_date method correctly identifies child table changes and clears the related auto-calculated fields (due_date_based_on, credit_days, credit_months) when the due date is manually modified in payment schedule rows.


2648-2678: LGTM! Efficient payment term handling with proper field management.

The enhanced payment_term method correctly:

  • Clears all relevant date condition fields before fetching new payment term details
  • Uses efficient direct property assignment instead of multiple frappe.model.set_value calls
  • Properly refreshes the payment schedule field to update the UI

The direct assignment approach is appropriate here since these are read-only fields populated from the backend.

erpnext/accounts/doctype/payment_schedule/payment_schedule.json (2)

13-18: LGTM! Well-structured due date calculation fields.

The new due date related fields are properly configured:

  • due_date_based_on provides clear options for date calculation methods
  • credit_days and credit_months have appropriate conditional visibility based on the selected calculation method
  • All fields are correctly marked as read-only and fetch from the linked Payment Term
  • Field validation with non_negative constraint is appropriate

Also applies to: 181-207


22-25: LGTM! Consistent discount validity field implementation.

The new discount validity fields follow the same well-structured pattern as the due date fields:

  • discount_validity_based_on properly depends on the presence of a discount
  • discount_validity has appropriate conditional visibility
  • Both fields correctly fetch from the linked Payment Term and are read-only
  • Field ordering logically groups discount-related fields together

Also applies to: 208-226

erpnext/buying/doctype/purchase_order/test_purchase_order.py (3)

543-543: LGTM: Decorator correctly enables automatic payment terms fetching.

The decorator appropriately sets up the test environment for validating payment terms copying from Purchase Order to Purchase Invoice.


715-715: LGTM: Decorator correctly disables automatic payment terms fetching.

The decorator appropriately configures the test to validate the negative scenario where payment terms should not be automatically copied when the setting is disabled.


909-909: LGTM: Decorator correctly enables automatic payment terms fetching.

The decorator properly sets up the test environment to validate that payment terms are automatically fetched when creating purchase invoices with purchase order references.

erpnext/controllers/accounts_controller.py (3)

2597-2597: LGTM!

The posting date extraction logic correctly prioritizes bill_date over posting_date over transaction_date, which aligns with the PR objective of using the current document's date for due date calculations.


2486-2490: LGTM!

The new payment term fields are correctly populated into the term_details dictionary, following the same pattern as other field assignments in this function. This provides the necessary metadata for due date and discount date calculations.


3505-3506: LGTM!

The function signature extension correctly adds an optional default_date parameter while maintaining backward compatibility. This allows for more flexible due date calculations when a specific starting date is needed.

Comment thread erpnext/controllers/accounts_controller.py
frappe-pr-bot and others added 22 commits August 19, 2025 00:23
…adjustments-in-report

refactor: consider asset value adjustments in report
fix: sync translations from crowdin
…e-property

fix(pick list): update warehouse property on refresh
fix: improve queries with query builder and input sanitization
…r-fieldname

fix: fetch fieldname in accounting dimension filter
fix(quotation): update currency on duplicate
…cate

fix: prevent duplicate reverse Journal Entry
…-operating-component

refactor: workstation operating component
fix(stock): don't override t_warehouse if no rules found
…t-company

fix: company issue in setup wizard
style: format controllers with prettier

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 32

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py (1)

713-716: Typo and mismatched key: “expene_account” should be “expense_account” (breaks downstream usage).

prepare_data_for_internal_transfer() returns expene_account, but call sites expect data.expense_account. With frappe._dict, the missing attr silently becomes None, which can mask issues and change accounting side-effects in tests.

Fix the variable and returned key:

-	expene_account = frappe.db.get_value(
+	expense_account = frappe.db.get_value(
 		"Company", company, "stock_adjustment_account"
 	) or frappe.db.get_value("Account", {"company": company, "account_type": "Expense Account"}, "name")
@@
 	return frappe._dict(
 		{
 			"from_warehouse": warehouse,
 			"to_warehouse": to_warehouse,
 			"customer": customer,
 			"supplier": supplier,
 			"company": company,
 			"cost_center": cost_center,
-			"expene_account": expene_account,
+			"expense_account": expense_account,
 			"store_warehouse": frappe.db.get_value(
 				"Warehouse", {"name": ("like", "Store%"), "company": company}, "name"
 			),
 		}
 	)

Also applies to: 717-729

erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py (1)

91-99: Add fallback for missing fieldname in get_dimension_filter_map

Pre-existing filters may have a NULL fieldname, causing entries to be keyed by (None, account) and disabling those filters. Derive a safe key from the dimension label when f.fieldname is falsy:

• File: erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
Method: get_dimension_filter_map (around line 91)

-       for f in filters:
-           build_map(
-               dimension_filter_map,
-               f.fieldname,
-               f.applicable_on_account,
-               f.dimension_value,
-               f.allow_or_restrict,
-               f.is_mandatory,
-           )
+       for f in filters:
+           # Fallback to scrub(dimension) if fieldname wasn’t backfilled
+           dimension_key = f.fieldname or frappe.scrub(f.accounting_dimension)
+           build_map(
+               dimension_filter_map,
+               dimension_key,
+               f.applicable_on_account,
+               f.dimension_value,
+               f.allow_or_restrict,
+               f.is_mandatory,
+           )
erpnext/stock/doctype/pick_list/pick_list.py (1)

1296-1319: Bug: kwargs reassignment breaks select_item filtering

create_dn_from_so defines select_item based on kwargs.filtered_children, then reassigns kwargs to a new dict. Because Python closures capture by reference, select_item sees the overwritten dict and filtered_children is lost, so no filtering occurs.

Apply this diff to preserve original kwargs for filtering and pass an augmented copy to the SO→DN mapper:

-def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
+def create_dn_from_so(pick_list, sales_order_list, delivery_note=None, kwargs=None):
     if not sales_order_list:
         return delivery_note
 
-    def select_item(d):
-        filtered_items = kwargs.get("filtered_children", [])
+    kwargs = kwargs or {}
+
+    def select_item(d):
+        filtered_items = kwargs.get("filtered_children", [])
         child_filter = d.name in filtered_items if filtered_items else True
         return child_filter
 
     item_table_mapper = {
         "doctype": "Delivery Note Item",
         "field_map": {
             "rate": "rate",
             "name": "so_detail",
             "parent": "against_sales_order",
         },
         "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty)
         and doc.delivered_by_supplier != 1
         and select_item(doc),
     }
 
-    kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
+    delivery_note_kwargs = {
+        **kwargs,
+        "skip_item_mapping": True,
+        "ignore_pricing_rule": pick_list.ignore_pricing_rule,
+    }
 
     delivery_note = create_delivery_note_from_sales_order(
-        next(iter(sales_order_list)), delivery_note, kwargs=kwargs
+        next(iter(sales_order_list)), delivery_note, kwargs=delivery_note_kwargs
     )
erpnext/public/js/controllers/taxes_and_totals.js (2)

212-215: Fix incorrect use of this inside $.each callback (breaks validation calls).

Inside initialize_taxes(), this refers to the current tax row (jQuery’s $.each), not the TaxesAndTotals instance. This causes this.discount_amount_applied to be undefined and passes undefined instead of frm to validate_inclusive_tax. Use the captured me.

Apply this diff:

-			if (!this.discount_amount_applied) {
-				erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name);
-				erpnext.accounts.taxes.validate_inclusive_tax(tax, this.frm);
-			}
+			if (!me.discount_amount_applied) {
+				erpnext.accounts.taxes.validate_taxes_and_charges(tax.doctype, tax.name);
+				erpnext.accounts.taxes.validate_inclusive_tax(tax, me.frm);
+			}

997-1009: Same total_amount_to_pay re-declaration issue here.

Mirror the fix in set_total_amount_to_default_mop().

Apply this diff:

 		if (this.frm.doc.party_account_currency == this.frm.doc.currency) {
-			var total_amount_to_pay = flt(
+			let total_amount_to_pay = flt(
 				grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount,
 				precision("grand_total")
 			);
 		} else {
-			var total_amount_to_pay = flt(
+			let total_amount_to_pay = flt(
 				flt(base_grand_total, precision("base_grand_total")) -
 					this.frm.doc.total_advance -
 					this.frm.doc.base_write_off_amount,
 				precision("base_grand_total")
 			);
 		}
erpnext/accounts/doctype/pos_invoice/pos_invoice.py (1)

921-955: Critical: Using child row docstatus breaks reserved-qty computation. Filter by parent docstatus instead.

Child tables typically aren’t submitted independently; their docstatus is 0. This condition will zero-out the SUM and misreport availability. Filter POS Invoice docstatus instead.

Apply:

 	reserved_qty = (
 		frappe.qb.from_(p_inv)
 		.from_(p_item)
 		.select(Sum(p_item[qty_column]).as_("stock_qty"))
 		.where(
 			(p_inv.name == p_item.parent)
 			& (IfNull(p_inv.consolidated_invoice, "") == "")
-			& (p_item.docstatus == 1)
+			& (p_inv.docstatus == 1)
 			& (p_item.item_code == item_code)
 			& (p_item.warehouse == warehouse)
 		)
 	).run(as_dict=True)
🧹 Nitpick comments (80)
erpnext/selling/doctype/product_bundle/product_bundle.py (1)

92-100: Harden the qty check (type safety) and add a clearer error title; consider a more explicit method name

Two minor improvements:

  • Use flt() to guard against None/strings and avoid type issues when comparing to 0.
  • Provide a title for the thrown error for better UX and consistency with other validations.

Optional naming nit: “non_zero” suggests only != 0, but you’re enforcing strictly positive. Consider renaming to validate_child_items_qty_positive.

Apply this diff within this block to harden the check and improve the error:

 def validate_child_items_qty_non_zero(self):
   for item in self.items:
-    if item.qty <= 0:
-      frappe.throw(
-        _(
-          "Row #{0}: Quantity cannot be a non-positive number. Please increase the quantity or remove the Item {1}"
-        ).format(item.idx, frappe.bold(item.item_code))
-      )
+    if flt(item.qty) <= 0:
+      frappe.throw(
+        _(
+          "Row #{0}: Quantity must be greater than 0. Increase the quantity or remove {1}."
+        ).format(item.idx, frappe.bold(item.item_code)),
+        title=_("Invalid Quantity"),
+      )

Additionally, update the import to support flt (outside this hunk):

from frappe.utils import get_link_to_form, flt

Optional rename for clarity (apply both diffs if you choose to rename):

-    self.validate_child_items_qty_non_zero()
+    self.validate_child_items_qty_positive()
-def validate_child_items_qty_non_zero(self):
+def validate_child_items_qty_positive(self):

Also consider adding unit tests covering:

  • qty == 0 → throws
  • qty < 0 → throws
  • qty as string/numeric string → correctly validated
  • qty > 0 (including fractional) → passes
erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py (1)

231-233: No-op call to load_from_db; either call it or remove the line.

doc.load_from_db without parentheses is a function reference, not an invocation. If reload is desired, call it; otherwise, drop the line.

Apply one of the following:

-		doc.load_from_db
+		doc.load_from_db()

or

-		doc.load_from_db
erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py (1)

358-366: Switching to an IN-subquery for MOP filtering fixes row-duplication; simplify with DISTINCT (optional).

Good call moving MOP filtering into a subquery to avoid join-induced row multiplication. You can simplify the subquery by using DISTINCT instead of GROUP BY on parent for clarity and (typically) equivalent plans.

Apply this diff:

-        subquery = (
-            frappe.qb.from_(sip)
-            .select(sip.parent)
-            .where(sip.mode_of_payment == filters.get("mode_of_payment"))
-            .groupby(sip.parent)
-        )
+        subquery = (
+            frappe.qb.from_(sip)
+            .select(sip.parent)
+            .distinct()
+            .where(sip.mode_of_payment == filters.get("mode_of_payment"))
+        )
         query = query.where(si.name.isin(subquery))

If you notice performance issues on large datasets, consider EXISTS as an alternative predicate (only if supported/ergonomic with our QB), and ensure there’s an index aiding the predicate on Sales Invoice Payment (at least on parent; mode_of_payment is a nice-to-have).

erpnext/patches/v16_0/set_invoice_type_in_pos_settings.py (1)

4-6: Guard against missing DocType to make the patch more defensive

On atypical sites or partial app installs, the DocType might be absent; this guard makes the patch no-op instead of erroring.

Apply this diff:

 def execute():
-	if not frappe.db.get_single_value("POS Settings", "invoice_type"):
-		frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
+	# Skip if the DocType is not available on this site
+	if not frappe.db.exists("DocType", "POS Settings"):
+		return
+
+	current = frappe.db.get_single_value("POS Settings", "invoice_type")
+	if not current:
+		frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
erpnext/accounts/report/gross_profit/gross_profit.py (1)

255-258: Drop the unused Supplier master-name lookup

This report is customer-only. Checking Buying Settings’ supp_master_name here is misleading and only used to over-constrain the condition. Keep it focused on Selling Settings.

-	supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
-	customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
+	customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
erpnext/accounts/report/general_ledger/general_ledger.py (1)

350-362: Optimize get_party_name_map to fetch only required parties

Fetching all Customers, Suppliers, and Employees can be heavy on large datasets. Consider limiting to parties present in gl_entries to reduce memory and query time.

Sketch of a targeted approach (requires minor wiring in get_gl_entries):

# Inside get_gl_entries, after fetching gl_entries:
from collections import defaultdict
parties_by_type = defaultdict(set)
for e in gl_entries:
    if e.party_type and e.party:
        parties_by_type[e.party_type].add(e.party)

party_name_map = get_party_name_map(parties_by_type)

# Adjust signature to accept parties_by_type:
def get_party_name_map(parties_by_type):
    party_map = {}

    if parties_by_type.get("Customer"):
        customers = frappe.get_all(
            "Customer",
            fields=["name", "customer_name"],
            filters={"name": ["in", list(parties_by_type["Customer"])]},
        )
        party_map["Customer"] = {c.name: c.customer_name for c in customers}

    if parties_by_type.get("Supplier"):
        suppliers = frappe.get_all(
            "Supplier",
            fields=["name", "supplier_name"],
            filters={"name": ["in", list(parties_by_type["Supplier"])]},
        )
        party_map["Supplier"] = {s.name: s.supplier_name for s in suppliers}

    if parties_by_type.get("Employee"):
        employees = frappe.get_all(
            "Employee",
            fields=["name", "employee_name"],
            filters={"name": ["in", list(parties_by_type["Employee"])]},
        )
        party_map["Employee"] = {e.name: e.employee_name for e in employees}

    return party_map

This avoids full-table scans and scales better.

erpnext/stock/doctype/putaway_rule/putaway_rule.py (1)

134-138: Simplify the ternary for readability

The same logic can be expressed more readably with a single or-chain. Also, a short comment helps future readers understand the precedence.

Apply this diff:

-			warehouse = (
-				(source_warehouse or item.get("warehouse"))
-				if not item.get("t_warehouse")
-				else item.get("t_warehouse")
-			)
+			# Prefer scanned/target warehouse if present; otherwise fall back to source or legacy item.warehouse
+			warehouse = item.get("t_warehouse") or source_warehouse or item.get("warehouse")
erpnext/assets/doctype/asset/asset.py (1)

1111-1114: Avoid unused variable in get_depreciation_accounts destructuring

fixed_asset_account is captured but never used in this function. Prefer discarding it to avoid noise and potential linter warnings.

-    (
-        fixed_asset_account,
-        accumulated_depreciation_account,
-        depreciation_expense_account,
-    ) = get_depreciation_accounts(asset.asset_category, asset.company)
+    (
+        _,
+        accumulated_depreciation_account,
+        depreciation_expense_account,
+    ) = get_depreciation_accounts(asset.asset_category, asset.company)
erpnext/accounts/doctype/loyalty_program/loyalty_program.py (2)

8-8: Import Coalesce to ensure SUMs never return None

Follow-up to the query change below: guard aggregated values against NULLs at the DB level.

-from frappe.query_builder.functions import Sum
+from frappe.query_builder.functions import Sum, Coalesce

59-81: Optional: remove redundant GROUP BY for a simpler and slightly faster query

Since you filter by a single customer, GROUP BY adds no value and can be removed (as in the diff above).

erpnext/setup/doctype/item_group/item_group.py (1)

4-4: Remove unused import

copy is no longer used after the change. Clean it up.

-import copy
erpnext/setup/doctype/brand/brand.py (1)

5-5: Remove unused import

copy is unused; please drop it.

-import copy
erpnext/stock/doctype/item/item.py (3)

1280-1283: Consistent sanitization; make pop safe

The switch to as_dict(no_private_properties=True) here mirrors the other doctypes—good. Consider a safe pop to avoid KeyError if name is ever excluded by future changes.

-			row = d.as_dict(no_private_properties=True)
-			row.pop("name")
+			row = d.as_dict(no_private_properties=True)
+			row.pop("name", None)

4-4: Remove unused import

copy is not used in this module anymore.

-import copy

1274-1278: Optional: also sanitize the top-level Item dict for consistency

If the intent is to avoid leaking private/internal fields broadly, consider sanitizing the top-level Item dict as well. This may be a behavioral change; verify consumers don’t rely on default/private fields from get_item_defaults.

-	out = item.as_dict()
+	out = item.as_dict(no_private_properties=True)

Please verify:

  • No downstream callers rely on default/private fields (e.g., owner, creation, etc.) from this function.
  • Integration tests covering API responses still pass.
erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json (1)

95-100: Harden the hidden field against accidental edits and copies

Since fieldname is derived and used internally, mark it read-only and no-copy to avoid accidental mutation via API or during copy/duplicate flows.

   {
    "fieldname": "fieldname",
    "fieldtype": "Data",
    "hidden": 1,
-   "label": "Fieldname"
+   "label": "Fieldname",
+   "read_only": 1,
+   "no_copy": 1
   }
erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py (3)

22-22: Type hint for accounting_dimension looks off

accounting_dimension: DF.Literal[None] suggests the field only accepts None, which is incorrect for a required Select. If the generator can’t enumerate options, consider a broader type such as DF.Data | None or DF.Select | None. At minimum, avoid Literal[None] to prevent misleading type checks.

Proposed hint (non-blocking, manual adjustment outside the auto-generated block if your generator can’t be updated):

-		accounting_dimension: DF.Literal[None]
+		accounting_dimension: DF.Data | None

39-42: Guard validate() against missing AD entries and avoid redundant writes

Logic is fine; small robustness improvement: compute once, fallback to scrub, and only assign if changed. This reduces unnecessary dirtying and defends if accounting_dimension is ever empty (even though it’s required).

-		self.fieldname = frappe.db.get_value(
-			"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
-		) or frappe.scrub(self.accounting_dimension)  # scrub to handle default accounting dimension
+		if self.accounting_dimension:
+			value = frappe.db.get_value(
+				"Accounting Dimension",
+				{"document_type": self.accounting_dimension},
+				"fieldname",
+			) or frappe.scrub(self.accounting_dimension)
+			if self.fieldname != value:
+				self.fieldname = value

5-7: Remove unused import scrub

scrub is imported but not used (you’re calling frappe.scrub instead). Clean up to appease linters.

-from frappe import _, scrub
+from frappe import _
erpnext/patches/v15_0/update_fieldname_in_accounting_dimension_filter.py (1)

28-36: Optional: drop default_accounting_dimension() after adopting LEFT JOIN

LEFT JOIN + fallback makes this redundant. If you prefer simplicity, remove the pre-update and the function.

-def execute():
-    default_accounting_dimension()
+def execute():
     ADF = DocType("Accounting Dimension Filter")
     AD = DocType("Accounting Dimension")
@@
-def default_accounting_dimension():
-    ADF = DocType("Accounting Dimension Filter")
-    for dim in ("Cost Center", "Project"):
-        (
-            frappe.qb.update(ADF)
-            .set(ADF.fieldname, frappe.scrub(dim))
-            .where(ADF.accounting_dimension == dim)
-            .run()
-        )
+def default_accounting_dimension():
+    # No-op after LEFT JOIN change; retained only if you want explicit defaults.
+    pass
erpnext/manufacturing/doctype/bom_creator/bom_creator.py (1)

496-501: Guard parent_row_no assignment & early-return before looping items

  • Move the if name == doc.name check (currently at lines 496–501) to immediately before the for name in doc.items loop so you short-circuit without scanning the entire list.
  • Since parent_row_no is defined as a Data field in bom_creator_item.json, passing None will be stored as null. In the add_sub_assembly function (in the same bom_creator.py), only include or set parent_row_no when it’s truthy. For example:
     # before: always writing None
  • child = {
  • "item_code": code,
  • "parent_row_no": parent_row_no,
  • }
  • child = {
  • "item_code": code,
  • }
  • if parent_row_no:
  • child["parent_row_no"] = parent_row_no
This ensures empty parent references aren’t serialized as null in the child table.

</blockquote></details>
<details>
<summary>erpnext/manufacturing/doctype/workstation_cost/workstation_cost.js (1)</summary><blockquote>

`1-8`: **Replace commented scaffold with a clear intent marker**

This file currently contains only commented-out scaffold. Either remove the dead code or add a concise “intentionally blank” note to avoid confusion in future diffs.


Apply this diff to keep the header and add an explicit note:

```diff
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt

-// frappe.ui.form.on("Workstation Cost", {
-// 	refresh(frm) {
-//
-// 	},
-// });
+// Intentionally left blank. Client-side handlers will be added when UI interactions are implemented.
erpnext/manufacturing/doctype/workstation_cost/test_workstation_cost.py (1)

1-21: Add a minimal meta-level test to validate the new DocType

Since this is a new DocType, a lightweight meta test helps catch regressions (field renames, child-table status). Suggest asserting it’s a child table and that expected fields exist.

Apply this diff:

 # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 
-# import frappe
+import frappe
 from frappe.tests import IntegrationTestCase
 
 # On IntegrationTestCase, the doctype test records and all
 # link-field test record dependencies are recursively loaded
 # Use these module variables to add/remove to/from that list
 EXTRA_TEST_RECORD_DEPENDENCIES = []  # eg. ["User"]
 IGNORE_TEST_RECORD_DEPENDENCIES = []  # eg. ["User"]
 
 
 class IntegrationTestWorkstationCost(IntegrationTestCase):
 	"""
 	Integration tests for WorkstationCost.
 	Use this class for testing interactions between multiple components.
 	"""
 
-	pass
+	def test_meta_defines_expected_fields(self):
+		meta = frappe.get_meta("Workstation Cost")
+		self.assertTrue(meta.istable, "Workstation Cost should be a child table")
+		fieldnames = {df.fieldname for df in meta.fields}
+		self.assertIn("operating_component", fieldnames)
+		self.assertIn("operating_cost", fieldnames)
erpnext/manufacturing/doctype/workstation_operating_component_account/test_workstation_operating_component_account.py (1)

1-21: Add a minimal meta-load test to validate DocType presence

A simple smoke test ensures the DocType metadata loads (useful when refactoring or renaming).

Apply this diff:

 # Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 
-# import frappe
+import frappe
 from frappe.tests import IntegrationTestCase
 
 # On IntegrationTestCase, the doctype test records and all
 # link-field test record dependencies are recursively loaded
 # Use these module variables to add/remove to/from that list
 EXTRA_TEST_RECORD_DEPENDENCIES = []  # eg. ["User"]
 IGNORE_TEST_RECORD_DEPENDENCIES = []  # eg. ["User"]
 
 
 class IntegrationTestWorkstationOperatingComponentAccount(IntegrationTestCase):
 	"""
 	Integration tests for WorkstationOperatingComponentAccount.
 	Use this class for testing interactions between multiple components.
 	"""
 
-	pass
+	def test_meta_loads(self):
+		# Validate that the DocType exists and metadata is retrievable
+		meta = frappe.get_meta("Workstation Operating Component Account")
+		self.assertIsNotNone(meta)
erpnext/manufacturing/doctype/workstation_cost/workstation_cost.json (2)

14-19: Currency field should declare currency options

Without options, Currency fields may render with defaults inconsistent with company currency. Consider binding to company currency (if available on the parent) or the generic currency field.

Apply one of these, depending on parent context:

Option A: If parent has a company field

   {
    "fieldname": "operating_cost",
    "fieldtype": "Currency",
    "in_list_view": 1,
    "label": "Operating Cost",
+   "options": "Company:company:default_currency",
    "reqd": 1
   },

Option B: If a generic currency field is available in parent

   {
    "fieldname": "operating_cost",
    "fieldtype": "Currency",
    "in_list_view": 1,
    "label": "Operating Cost",
+   "options": "currency",
    "reqd": 1
   },

30-31: Disable web page search indexing for child table

Child tables are not meant to be discoverable via website search. Turn off indexing.

- "index_web_pages_for_search": 1,
+ "index_web_pages_for_search": 0,
erpnext/manufacturing/doctype/workstation_operating_component_account/workstation_operating_component_account.json (3)

22-27: Make Expense Account required (or validate server-side) and constrain to expense leaf accounts

Leaving the expense account optional can yield incomplete mappings; also, users can currently select non-expense or group accounts.

Minimal JSON change:

   {
     "fieldname": "expense_account",
     "fieldtype": "Link",
     "in_list_view": 1,
     "label": "Expense Account",
-    "options": "Account"
+    "options": "Account",
+    "reqd": 1
   }

Additionally, add a link filter via client script to enforce:

  • is_group = 0
  • root_type = "Expense"
  • company = row.company

Example (workstation_operating_component_account.js):

frappe.ui.form.on('Workstation Operating Component', {
  setup(frm) {
    frm.set_query('expense_account', 'accounts', function(doc, cdt, cdn) {
      const row = locals[cdt][cdn];
      return {
        filters: {
          company: row.company,
          is_group: 0,
          root_type: 'Expense',
        },
      };
    });
  },
});

If you prefer server-side enforcement, add a validate hook to reject invalid accounts.


30-31: Disable web search indexing for child table

Same rationale as other child tables: turn off indexing.

- "index_web_pages_for_search": 1,
+ "index_web_pages_for_search": 0,

12-20: Ensure uniqueness per company row under a component

Prevent duplicate company rows under the same parent to avoid ambiguous account resolution. Implement a validate in Workstation Operating Component to enforce one account row per company.

I can draft the validate() hook to enforce uniqueness if you confirm the parent Doctype file path you want to place it in.

erpnext/manufacturing/doctype/bom_item/bom_item.json (1)

1-329: Add tests to assert propagation of is_sub_assembly_item

Recommend adding tests for:

  • BOM explosion includes is_sub_assembly_item on relevant rows.
  • Toggling do_not_explode and sub-assembly behavior doesn’t regress.

I can scaffold pytest cases for BOM explosion asserting the flag propagation; say the word.

erpnext/manufacturing/doctype/workstation/test_workstation.py (1)

4-4: Drop unused translation import once _() usages are removed.

After switching to literals, this import is no longer needed.

-from frappe import _
erpnext/manufacturing/doctype/bom/bom.js (1)

92-109: SFG toggle handler is straightforward and correct.

Updating read_only for finished_good, finished_good_qty, bom_no on the Operations grid is appropriate.

If these fields are already controlled via depends_on in JSON, consider consolidating logic to a single mechanism (either JSON depends_on for hidden/mandatory/read-only or JS) to reduce drift.

erpnext/manufacturing/doctype/workstation_operating_component/test_workstation_operating_component.py (3)

4-6: Drop commented import and keep imports minimal.

The commented-out import frappe can be removed to keep the test module tidy; re-add it when needed.


7-12: Scaffold variables are fine but can be omitted until used.

The EXTRA/IGNORE dependency hooks are OK to keep, but consider adding them only when a test needs them to reduce churn.


14-20: Add at least one smoke test to assert doc creation/fixtures.

A basic test ensures the Doctype wiring works and prevents regressions.

Apply this diff to replace the pass with a minimal smoke test:

 class IntegrationTestWorkstationOperatingComponent(IntegrationTestCase):
 	"""
 	Integration tests for WorkstationOperatingComponent.
 	Use this class for testing interactions between multiple components.
 	"""
-
-	pass
+	def test_can_load_component_doctype(self):
+		# sanity: ensure doctype exists and can be listed
+		names = frappe.get_all("Workstation Operating Component", pluck="name")
+		self.assertIsInstance(names, list)
erpnext/manufacturing/doctype/production_plan/production_plan.py (4)

1004-1004: Rename sentinel to reflect intent (tracks processing, not BOM setting).

track_semi_finished_goods here is used as a “processed-any-bom” flag. The name is misleading and makes the condition at Line 1040 harder to read.

Apply this diff to improve readability:

-		track_semi_finished_goods = True
+		any_bom_processed = False

1015-1022: Avoid spamming the user with repeated warnings.

This message will print once per PO item whose BOM has Track Semi Finished Goods enabled. If many rows match, users get spammed.

Aggregate and print a single message summarizing all skipped rows/BOMs, or deduplicate per BOM. If you want, I can provide a patch that collects and prints once.


1025-1025: Flip the sentinel with clearer semantics.

This line signals that at least one BOM got processed. With the proposed rename, set the flag to True.

Apply this diff:

-			track_semi_finished_goods = False
+			any_bom_processed = True

1040-1044: Condition reads clearer when keyed off “processed-any-bom”.

The note should show only when something was processed, nothing was added, and skipping is enabled.

Apply this diff:

-		if (
-			not track_semi_finished_goods
-			and not sub_assembly_items_store
-			and self.skip_available_sub_assembly_item
-		):
+		if (any_bom_processed and not sub_assembly_items_store and self.skip_available_sub_assembly_item):
erpnext/manufacturing/doctype/workstation_type/workstation_type.py (1)

30-41: Use a set for duplicate detection; simpler and faster.

Current list-based accumulation is O(n^2) worst-case and more verbose.

Apply this diff:

 	def validate_duplicate_operating_component(self):
-		components = []
-		for row in self.workstation_costs:
-			if row.operating_component not in components:
-				components.append(row.operating_component)
-			else:
-				frappe.throw(
-					_("Duplicate Operating Component {0} found in Operating Components").format(
-						bold(row.operating_component)
-					)
-				)
+		seen = set()
+		for row in self.workstation_costs:
+			comp = row.operating_component
+			if comp in seen:
+				frappe.throw(
+					_("Duplicate Operating Component {0} found in Operating Components").format(bold(comp))
+				)
+			seen.add(comp)
erpnext/patches/v16_0/make_workstation_operating_components.py (1)

5-12: Guard against missing default accounts.

If a Company has neither default operating cost nor expense account, you’ll insert child rows with a null expense_account, which may violate mandatory constraints.

Apply this diff to make account assignment safe:

 def get_operating_cost_account(company):
 	company_details = frappe.db.get_value(
 		"Company", company, ["default_operating_cost_account", "default_expense_account"], as_dict=True
 	)
 
-	return company_details.get("default_operating_cost_account") or company_details.get(
-		"default_expense_account"
-	)
+	return company_details.get("default_operating_cost_account") or company_details.get("default_expense_account")

And below (Lines 31-36):

-			for company in companies:
-				operating_cost_account = get_operating_cost_account(company)
-
-				doc.append("accounts", {"company": company, "expense_account": operating_cost_account})
+			for company in companies:
+				operating_cost_account = get_operating_cost_account(company)
+				if operating_cost_account:
+					doc.append("accounts", {"company": company, "expense_account": operating_cost_account})
+				# else: skip; avoid inserting invalid rows

Please confirm whether Workstation Operating Component Account.expense_account is mandatory; if it is, the guard is essential.

erpnext/manufacturing/doctype/workstation_operating_component/workstation_operating_component.json (1)

27-31: Nit: Pluralize the table field label.

The "accounts" field is a Table; consider pluralizing the label for clarity.

Apply this diff:

-  "label": "Component Expense Account",
+  "label": "Component Expense Accounts",
erpnext/manufacturing/doctype/bom_operation/bom_operation.json (1)

197-206: UI gating for semi-finished flow is coherent; consider making key fields mandatory when visible.

Finished Good, BOM No, and Qty to Produce are conditionally shown when track_semi_finished_goods is enabled. It may be beneficial to make some of them mandatory when visible to prevent incomplete operation setups.

Apply this diff to add mandatory_depends_on:

 {
   "columns": 3,
   "depends_on": "eval:parent.track_semi_finished_goods === 1",
   "fieldname": "finished_good",
   "fieldtype": "Link",
   "in_list_view": 1,
   "label": "FG / Semi FG Item",
   "options": "Item",
+  "mandatory_depends_on": "eval:parent.track_semi_finished_goods === 1"
 },
 {
   "columns": 2,
   "depends_on": "eval:parent.track_semi_finished_goods === 1",
   "fieldname": "bom_no",
   "fieldtype": "Link",
   "in_list_view": 1,
   "label": "BOM No",
   "options": "BOM",
+  "mandatory_depends_on": "eval:parent.track_semi_finished_goods === 1"
 },
 {
   "columns": 2,
   "default": "1",
   "depends_on": "eval:parent.track_semi_finished_goods === 1",
   "fieldname": "finished_good_qty",
   "fieldtype": "Float",
   "in_list_view": 1,
   "label": "Qty to Produce",
+  "mandatory_depends_on": "eval:parent.track_semi_finished_goods === 1"
 }

Also applies to: 205-211, 214-221, 223-230, 275-279

erpnext/manufacturing/doctype/workstation/workstation.py (1)

69-83: Prevent false duplicate detection for empty components.

If operating_component is missing on multiple rows, current logic flags duplicates. Skip empty/null values.

Apply this diff:

 def validate_duplicate_operating_component(self):
     components = []
     for row in self.workstation_costs:
-        if row.operating_component not in components:
+        if not row.operating_component:
+            continue
+        if row.operating_component not in components:
             components.append(row.operating_component)
         else:
             frappe.throw(
                 _("Duplicate Operating Component {0} found in Operating Components").format(
                     bold(row.operating_component)
                 )
             )
erpnext/accounts/doctype/pos_invoice/pos_invoice.js (2)

59-67: Harden get_value path and coerce to boolean

Guard on r.message instead of r.exc (which may not be set) and coerce the flag to a boolean.

Apply this diff:

-        if (this.frm.doc.pos_profile) {
-            frappe.db
-                .get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
-                .then((r) => {
-                    if (!r.exc) {
-                        this.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
-                    }
-                });
-        }
+        if (this.frm.doc.pos_profile) {
+            frappe.db
+                .get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
+                .then((r) => {
+                    if (r && r.message) {
+                        this.frm.set_default_payment = !!r.message.set_grand_total_to_default_mop;
+                    }
+                });
+        }

134-134: Use fallback and boolean coercion for set_default_payment

Depending on backend changes, the payload may expose set_default_payment or the legacy set_grand_total_to_default_mop. Use a nullish-coalescing fallback and coerce to boolean.

Apply this diff:

-                        this.frm.set_default_payment = r.message.set_default_payment;
+                        this.frm.set_default_payment = !!(r.message.set_default_payment ?? r.message.set_grand_total_to_default_mop);

Also confirm that downstream consumers read this frm-level property (set_default_payment) to affect payment behavior; otherwise, this will be a no-op.

erpnext/stock/doctype/purchase_receipt/purchase_receipt.json (1)

1291-1296: Mark virtual field as read-only to match display-only intent

Since last_scanned_warehouse is virtual and display-only, mark it read_only to prevent user edits that won’t persist.

Apply this diff:

   {
     "depends_on": "eval: doc.last_scanned_warehouse",
     "fieldname": "last_scanned_warehouse",
     "fieldtype": "Data",
     "is_virtual": 1,
-    "label": "Last Scanned Warehouse"
+    "label": "Last Scanned Warehouse",
+    "read_only": 1
   }
erpnext/selling/doctype/sales_order/sales_order.json (1)

1686-1691: Make the virtual field read-only for clarity

As with other DocTypes, the Last Scanned Warehouse virtual field should be read-only to reflect its display-only purpose.

Apply this diff:

   {
     "depends_on": "eval: doc.last_scanned_warehouse",
     "fieldname": "last_scanned_warehouse",
     "fieldtype": "Data",
     "is_virtual": 1,
-    "label": "Last Scanned Warehouse"
+    "label": "Last Scanned Warehouse",
+    "read_only": 1
   }
erpnext/stock/doctype/delivery_note/delivery_note.json (1)

1415-1421: Mark the virtual field as read-only and hide from print

Since last_scanned_warehouse is UI-only (is_virtual = 1), make it read-only and hidden on print to avoid accidental edits and noisy printouts.

Apply this diff:

 {
   "depends_on": "eval: doc.last_scanned_warehouse",
   "fieldname": "last_scanned_warehouse",
   "fieldtype": "Data",
   "is_virtual": 1,
-  "label": "Last Scanned Warehouse"
+  "label": "Last Scanned Warehouse",
+  "read_only": 1,
+  "print_hide": 1
 }
erpnext/stock/doctype/delivery_note/delivery_note.py (1)

887-891: Clarify semantics for empty selection lists

Currently, if args.filtered_children is present but empty, all items will pass the condition (treats empty as “select all”). If the intent of providing filtered_children is to explicitly control inclusion, consider treating an empty list as “select none”, and only include all when the key is absent.

- def select_item(d):
-   filtered_items = args.get("filtered_children", [])
-   child_filter = d.name in filtered_items if filtered_items else True
-   return child_filter
+ def select_item(d):
+   filtered_items = args.get("filtered_children", None)
+   # Include all if the key is absent; if present, respect the explicit selection (even if empty).
+   return True if filtered_items is None else d.name in filtered_items
erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.json (1)

183-189: Make the virtual field read-only and hidden on print

Same rationale as other doctypes: it’s UI-only and should not be editable or printed.

Apply this diff:

 {
   "depends_on": "eval: doc.last_scanned_warehouse",
   "fieldname": "last_scanned_warehouse",
   "fieldtype": "Data",
   "is_virtual": 1,
-  "label": "Last Scanned Warehouse"
+  "label": "Last Scanned Warehouse",
+  "read_only": 1,
+  "print_hide": 1
 }
erpnext/selling/doctype/quotation/quotation.json (1)

1103-1110: Make virtual field read-only to avoid UX confusion

Since last_scanned_warehouse is virtual (non-persistent), setting it read-only prevents user edits that won’t be saved.

Apply:

   {
     "depends_on": "eval: doc.last_scanned_warehouse",
     "fieldname": "last_scanned_warehouse",
     "fieldtype": "Data",
     "is_virtual": 1,
-    "label": "Last Scanned Warehouse"
+    "label": "Last Scanned Warehouse",
+    "read_only": 1
   }
erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json (1)

1658-1664: Make virtual field read-only for clarity

Marking the virtual “Last Scanned Warehouse” as read-only avoids editable-but-unsaved UX.

Apply:

   {
     "depends_on": "eval: doc.last_scanned_warehouse",
     "fieldname": "last_scanned_warehouse",
     "fieldtype": "Data",
     "is_virtual": 1,
-    "label": "Last Scanned Warehouse"
+    "label": "Last Scanned Warehouse",
+    "read_only": 1
   },
erpnext/accounts/doctype/sales_invoice/sales_invoice.json (1)

2225-2231: Mark virtual ‘Last Scanned Warehouse’ as read-only

Consistent with other doctypes, make the virtual field read-only.

Apply:

   {
     "depends_on": "eval: doc.last_scanned_warehouse",
     "fieldname": "last_scanned_warehouse",
     "fieldtype": "Data",
     "is_virtual": 1,
-    "label": "Last Scanned Warehouse"
+    "label": "Last Scanned Warehouse",
+    "read_only": 1
   }
erpnext/accounts/doctype/pos_invoice/pos_invoice.json (1)

1599-1605: Make the virtual field read-only (and hidden in print) to avoid accidental edits

This is a computed, non-persistent display. Marking it read-only (and print-hidden) prevents confusion and unintended edits/printing.

Apply this diff:

 {
   "depends_on": "eval: doc.last_scanned_warehouse",
   "fieldname": "last_scanned_warehouse",
   "fieldtype": "Data",
   "is_virtual": 1,
-  "label": "Last Scanned Warehouse"
+  "label": "Last Scanned Warehouse",
+  "read_only": 1,
+  "print_hide": 1
 }
erpnext/stock/doctype/purchase_receipt/purchase_receipt.py (1)

1329-1333: Harden JSON args parsing

Guard against invalid JSON to avoid tracebacks when args is a malformed string.

Apply this diff:

 @frappe.whitelist()
 def make_purchase_invoice(source_name, target_doc=None, args=None):
 	if args is None:
 		args = {}
 	if isinstance(args, str):
-		args = json.loads(args)
+		try:
+			args = json.loads(args)
+		} except Exception:
+			args = {}
erpnext/public/js/controllers/buying.js (1)

485-486: Simplify condition for readability

The “> 0” comparison adds noise without changing semantics. Prefer a clear boolean.

Apply this diff:

-						item.type_of_transaction = !doc.is_return > 0 ? "Inward" : "Outward";
+						item.type_of_transaction = !doc.is_return ? "Inward" : "Outward";
erpnext/buying/doctype/purchase_order/purchase_order.json (1)

44-47: Virtual “Last Scanned Warehouse”: add read_only and hide from print

The field is virtual status text; mark it read-only and excluded from print to prevent accidental edits and leakage into print formats. Placement near scan_barcode is sensible; moving set_from_warehouse after items_col_break is fine.

Apply this diff:

   "last_scanned_warehouse",
 ...
 {
   "depends_on": "eval: doc.last_scanned_warehouse",
   "fieldname": "last_scanned_warehouse",
   "fieldtype": "Data",
   "is_virtual": 1,
-  "label": "Last Scanned Warehouse"
+  "label": "Last Scanned Warehouse",
+  "read_only": 1,
+  "print_hide": 1
 }

Also applies to: 1312-1318

erpnext/stock/doctype/material_request/material_request.json (1)

23-26: Virtual “Last Scanned Warehouse”: mark read-only and hide from print; field reordering LGTM

Adding the non-persistent field near scan_barcode aligns with the intended UX. Suggest making it read-only and not printable.

Apply this diff:

  "last_scanned_warehouse",
  "column_break5",
  "set_from_warehouse",
 ...
 {
   "depends_on": "eval: doc.last_scanned_warehouse",
   "fieldname": "last_scanned_warehouse",
   "fieldtype": "Data",
   "is_virtual": 1,
-  "label": "Last Scanned Warehouse"
+  "label": "Last Scanned Warehouse",
+  "read_only": 1,
+  "print_hide": 1
 }

Also applies to: 359-365

erpnext/accounts/doctype/dunning/dunning.py (1)

167-227: Dunning state updater: deduplicate fetched names and consider robustness of outstanding check

Overall approach—toggling Dunning status based on invoice/payment-schedule outstanding—is sound and removes docstatus coupling. Two improvements:

  • Avoid duplicate loads/saves when the join returns the same Dunning multiple times.
  • Consider OR semantics to be conservative when determining “Unresolved” to avoid prematurely resolving if either the invoice or the specific overdue payment still shows outstanding due to timing/order of updates.

Apply this diff to deduplicate loaded dunnings:

-	dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
+	dunning_names = {d.name for d in dunnings}
+	dunnings = [frappe.get_doc("Dunning", name) for name in dunning_names]

Optionally, make the outstanding check conservative:

-			has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
+			# conservative: if either shows outstanding, keep unresolved
+			has_outstanding = (invoice_outstanding > 0) or (ps_outstanding > 0)

If you keep the stricter AND, please ensure Overdue Payment rows always have a valid payment_schedule and that schedule.outstanding is updated synchronously with invoice.outstanding to prevent false “Resolved” states.

erpnext/patches/v14_0/single_to_multi_dunning.py (1)

50-52: OK to bypass link validation during data patch; consider hardening save context

Setting dunning.flags.ignore_links = True immediately before save() is reasonable in a patch to avoid link validation noise on migrated docs. Two small hardening tweaks you may consider:

  • Save with explicit permission bypass to avoid patch failures in restrictive environments: dunning.save(ignore_permissions=True).
  • Add a short comment explaining why link validation is intentionally bypassed here (will help future maintainers).
-		dunning.flags.ignore_links = True
-		dunning.save()
+		# Skip link validations for migration-save; permissions bypass is intentional in patches.
+		dunning.flags.ignore_links = True
+		dunning.save(ignore_permissions=True)
erpnext/selling/doctype/sales_order/sales_order.py (1)

996-1000: Parse kwargs when passed as JSON (consistency with other APIs)

kwargs may arrive as a JSON string from client calls. You already parse args similarly elsewhere; mirror that here to avoid subtle runtime issues.

 	if not kwargs:
 		kwargs = {
 			"for_reserved_stock": frappe.flags.args and frappe.flags.args.for_reserved_stock,
 			"skip_item_mapping": frappe.flags.args and frappe.flags.args.skip_item_mapping,
 		}
 
-	kwargs = frappe._dict(kwargs)
+	# Support both dict and JSON string inputs for kwargs.
+	if isinstance(kwargs, str):
+		kwargs = json.loads(kwargs)
+	kwargs = frappe._dict(kwargs)
erpnext/public/js/controllers/accounts.js (1)

26-35: Avoid redeclaration of account_type (lint error)

Redeclaring var account_type in each branch triggers noRedeclare. Define once and assign per-branch.

-					frm.set_query("account_head", "taxes", function (doc) {
-						if (frm.cscript.tax_table == "Sales Taxes and Charges") {
-							var account_type = ["Tax", "Chargeable", "Expense Account"];
-						} else {
-							var account_type = [
+					frm.set_query("account_head", "taxes", function (doc) {
+						let account_type;
+						if (frm.cscript.tax_table == "Sales Taxes and Charges") {
+							account_type = ["Tax", "Chargeable", "Expense Account"];
+						} else {
+							account_type = [
 								"Tax",
 								"Chargeable",
 								"Income Account",
 								"Expenses Included In Valuation",
 							];
erpnext/public/js/controllers/taxes_and_totals.js (1)

1052-1058: Don’t use Array.prototype.find for side-effects; use forEach.

find() is meant to locate one element and stops early; here we mutate all rows. Use forEach for clarity and correctness.

Apply this diff:

-		this.frm.doc.payments.find((payment) => {
-			if (payment.default) {
-				payment.amount = total_amount_to_pay;
-			} else {
-				payment.amount = 0;
-			}
-		});
+		this.frm.doc.payments.forEach((payment) => {
+			payment.amount = payment.default ? total_amount_to_pay : 0;
+		});
erpnext/public/scss/erpnext.scss (1)

622-628: LGTM; ensure parent container is positioned for correct absolute layout.

The button relies on absolute positioning. Confirm the field wrapper has position: relative; otherwise placement may be off in some forms.

erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js (1)

10-11: Optional: detach scanner listeners on form teardown.

If BarcodeScanner attaches DOM listeners, consider disposing it on form unload to prevent leaks when navigating between forms frequently.

I can draft a small helper to support .dispose() on the scanner and hook it into the form’s on_close if the utility supports it.

erpnext/stock/doctype/stock_entry/stock_entry.json (1)

695-702: Make Last Scanned Warehouse read-only and normalize depends_on expression.

Since this is a virtual, system-populated field, it should be read-only. Also, normalize the depends_on expression for consistency.

Apply this diff:

-  {
-   "depends_on": "eval: doc.last_scanned_warehouse",
-   "fieldname": "last_scanned_warehouse",
-   "fieldtype": "Data",
-   "is_virtual": 1,
-   "label": "Last Scanned Warehouse"
-  }
+  {
+   "depends_on": "eval:doc.last_scanned_warehouse",
+   "fieldname": "last_scanned_warehouse",
+   "fieldtype": "Data",
+   "is_virtual": 1,
+   "read_only": 1,
+   "label": "Last Scanned Warehouse"
+  }
erpnext/stock/doctype/stock_entry/test_stock_entry.py (1)

2086-2149: Avoid pre-submit valuation assertions to reduce flakiness; keep the post-submit checks.

Distribution of additional_costs to valuation_rate may run on validate/submit depending on hooks. The pre-submit assertion on valuation_rate can intermittently fail.

Suggested tweak:

-		self.assertEqual(se.items[0].basic_rate, 100)
-		self.assertEqual(se.items[0].valuation_rate, 125)
+		self.assertEqual(se.items[0].basic_rate, 100)

Optional: use assertAlmostEqual for the final incoming_rate assertion to be resilient to precision:

-		self.assertEqual(incoming_rate, 125.0)
+		self.assertAlmostEqual(incoming_rate, 125.0, places=4)
erpnext/accounts/utils.py (1)

1943-1944: Verify availability and usage of update_linked_dunnings.

The inline import and usage look fine and help avoid circular imports, but please confirm:

  • erpnext.accounts.doctype.dunning.dunning exports update_linked_dunnings(doc, previous_outstanding_amount),
  • No unintended import cycle in production,
  • The function tolerates receiving a LazyDocument.

If needed, guard the call to fail-safe in environments without Dunning enabled.

Example guard:

-	from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings
+	try:
+		from erpnext.accounts.doctype.dunning.dunning import update_linked_dunnings
+	except Exception:
+		update_linked_dunnings = None
...
-	update_linked_dunnings(ref_doc, previous_outstanding_amount)
+	if update_linked_dunnings:
+		update_linked_dunnings(ref_doc, previous_outstanding_amount)

Also applies to: 1974-1989

erpnext/buying/doctype/request_for_quotation/request_for_quotation.py (2)

586-604: Robustness and readability: use an explicit join, handle None filters before building the qb query

  • Using inner_join makes intent clearer than cross join + where.
  • Guard against filters being None for defensive safety.

Apply:

-    rfq = frappe.qb.DocType("Request for Quotation")
-    rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
-
-    query = (
-        frappe.qb.from_(rfq)
-        .from_(rfq_supplier)
-        .select(rfq.name)
-        .distinct()
-        .select(rfq.transaction_date, rfq.company)
-        .where(
-            (rfq.name == rfq_supplier.parent)
-            & (rfq_supplier.supplier == filters.get("supplier"))
-            & (rfq.docstatus == 1)
-            & (rfq.company == filters.get("company"))
-        )
-        .orderby(rfq.transaction_date, order=Order.asc)
-        .limit(page_len)
-        .offset(start)
-    )
+    rfq = frappe.qb.DocType("Request for Quotation")
+    rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
+
+    filters = filters or {}
+
+    query = (
+        frappe.qb.from_(rfq)
+        .inner_join(rfq_supplier)
+        .on(rfq.name == rfq_supplier.parent)
+        .select(rfq.name)
+        .distinct()
+        .select(rfq.transaction_date, rfq.company)
+        .where(
+            (rfq_supplier.supplier == filters.get("supplier"))
+            & (rfq.docstatus == 1)
+            & (rfq.company == filters.get("company"))
+        )
+        .orderby(rfq.transaction_date, order=Order.asc)
+        .limit(page_len)
+        .offset(start)
+    )

606-608: Nit: LIKE pattern can use single % delimiters

Double % isn’t harmful, but conventional and clearer to use one on each side.

-        query = query.where(rfq.name.like(f"%%{txt}%%"))
+        query = query.where(rfq.name.like(f"%{txt}%"))
erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py (2)

2083-2088: Expanded API for make_purchase_receipt: args parsing looks fine

Accepting args (dict or JSON string) provides flexibility for selective mapping. Consider documenting expected shape in the docstring for discoverability.


2126-2127: Guard against None in condition with robust numeric conversion

If received_qty or qty can ever be None, abs(None) will raise. Using flt guards against this gracefully (consistent with update_item).

-                "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
+                "condition": lambda doc: flt(doc.received_qty) < flt(doc.qty) and select_item(doc),
erpnext/accounts/doctype/pos_invoice/pos_invoice.py (2)

414-416: Nit: Improve error message wording.

Minor copy-edit for clarity and consistency.

Apply:

-						_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
-							d.idx, item_code, warehouse
-						),
+						_("Row #{}: Insufficient stock for Item Code {} in warehouse {}.").format(
+							d.idx, item_code, warehouse
+						),

879-883: Guard against division by zero when computing bundle availability.

If a bundle component row (unexpectedly) has qty == 0, this will raise; add a safe guard.

-		max_available_bundles = item_bin_qty / item.qty
+		max_available_bundles = (item_bin_qty / item.qty) if item.qty else 0
erpnext/buying/doctype/purchase_order/purchase_order.py (2)

751-756: Harden args parsing against non-dict inputs.

If a caller passes a JSON array/string accidentally, json.loads will return a list and args.get will crash. Normalize to a dict.

 def make_purchase_receipt(source_name, target_doc=None, args=None):
 	if args is None:
 		args = {}
 	if isinstance(args, str):
-		args = json.loads(args)
+		args = json.loads(args) or {}
+	if not isinstance(args, dict):
+		args = {}

If you prefer, use frappe.utils.parse_json which already handles strings safely.


830-835: Mirror args normalization in get_mapped_purchase_invoice.

Same robustness improvement as above.

 def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions=False, args=None):
 	if args is None:
 		args = {}
 	if isinstance(args, str):
-		args = json.loads(args)
+		args = json.loads(args) or {}
+	if not isinstance(args, dict):
+		args = {}
erpnext/controllers/accounts_controller.py (1)

3420-3422: Guard against None posting_date when adjusting due_date.

If posting_date is None, getdate(None) can misbehave. Add a simple guard.

-	if getdate(term_details.due_date) < getdate(posting_date):
-		term_details.due_date = posting_date
+	if posting_date and term_details.due_date and getdate(term_details.due_date) < getdate(posting_date):
+		term_details.due_date = posting_date
erpnext/public/js/utils/barcode_scanner.js (1)

55-66: Clarify error message when scanning a warehouse on unsupported forms.

Current logic shows “Cannot find Item with this Barcode” even when a Warehouse barcode is scanned on a doctype that lacks last_scanned_warehouse. Consider a clearer message and/or guiding users.

Example tweak:

  • If data.warehouse && !this.has_last_scanned_warehouse: show “Warehouse scanning is not supported on this form.”
erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py (1)

299-317: Avoid StopIteration when no per-asset depreciation row is found.

Using next(...) without a default can raise StopIteration if assets is empty or the asset is absent. Provide a default empty dict to keep flt(...) downstream safe.

-		row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
+		asset_row = next(
+			(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")),
+			{},
+		)
+		row.update(asset_row)

Additionally, consider applying the same pattern to the category path where you do row.update(next(...)) for asset_categories to make both code paths robust (outside this hunk).

Comment on lines +1799 to +1806
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
if existing_reverse:
frappe.throw(
_("A Reverse Journal Entry {0} already exists for this Journal Entry.").format(
get_link_to_form("Journal Entry", existing_reverse)
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prevent duplicate reverse entries under concurrency by validating on submit

The pre-check blocking a second submitted reversal is a solid improvement. However, there’s still a race window: two users can generate draft reversals concurrently and both proceed to submit, causing duplicate reversals. Add a server-side check on submit to guarantee uniqueness.

Suggested additions outside this hunk (in JournalEntry.before_submit or validate):

# In JournalEntry.before_submit (or validate, but submit is stricter)
def before_submit(self):
    # existing logic...
    # ...
    if self.reversal_of:
        existing = frappe.db.exists(
            "Journal Entry",
            {
                "reversal_of": self.reversal_of,
                "docstatus": 1,
                "name": ["!=", self.name],
            },
        )
        if existing:
            frappe.throw(
                _("A submitted Reverse Journal Entry {0} already exists for {1}.").format(
                    get_link_to_form("Journal Entry", existing),
                    get_link_to_form("Journal Entry", self.reversal_of),
                )
            )

Optional refinement:

  • If you want to also avoid multiple drafts, block when a non-cancelled reversal already exists (docstatus in [0,1]) or return the existing draft instead of creating a new one.

Tests: add one that creates a reversal, submits it, then attempts to create another reversal and asserts a throw.

I can add the before_submit guard and wire up a test case in the JE test suite. Want me to draft the patch?

Comment on lines +59 to +81
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")

query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)

if company:
condition = " and company=%s " % frappe.db.escape(company)
query = query.where(LoyaltyPointEntry.company == company)

if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date

loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
)
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)

loyalty_point_details = query.run(as_dict=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Exclude non-expiring points bug, potential None sums, and missing docstatus filter

  • Entries with no expiry_date are currently excluded when include_expired_entry is False. Those should count as valid (non-expiring) points.
  • SUM() can return NULLs, which can bubble into lp_details.total_spent and break arithmetic later (e.g., addition in Lines 106–115). Coalesce at the DB level to ensure 0s.
  • Consider filtering only submitted entries (docstatus = 1) to avoid drafts/cancelled data skewing balances.
  • The GROUP BY is redundant since you already filter by a specific customer.

Apply this diff to the query block:

- LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
-
- query = (
-     frappe.qb.from_(LoyaltyPointEntry)
-     .select(
-         Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
-         Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
-     )
-     .where(
-         (LoyaltyPointEntry.customer == customer)
-         & (LoyaltyPointEntry.loyalty_program == loyalty_program)
-         & (LoyaltyPointEntry.posting_date <= expiry_date)
-     )
-     .groupby(LoyaltyPointEntry.customer)
- )
+ LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
+
+ query = (
+     frappe.qb.from_(LoyaltyPointEntry)
+     .select(
+         Coalesce(Sum(LoyaltyPointEntry.loyalty_points), 0).as_("loyalty_points"),
+         Coalesce(Sum(LoyaltyPointEntry.purchase_amount), 0).as_("total_spent"),
+     )
+     .where(
+         (LoyaltyPointEntry.customer == customer)
+         & (LoyaltyPointEntry.loyalty_program == loyalty_program)
+         & (LoyaltyPointEntry.posting_date <= expiry_date)
+         & (LoyaltyPointEntry.docstatus == 1)
+     )
+ )
@@
- if company:
-     query = query.where(LoyaltyPointEntry.company == company)
+ if company:
+     query = query.where(LoyaltyPointEntry.company == company)
@@
- if not include_expired_entry:
-     query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
+ if not include_expired_entry:
+     # include non-expiring entries too
+     query = query.where(
+         (LoyaltyPointEntry.expiry_date >= expiry_date)
+         | (LoyaltyPointEntry.expiry_date.isnull())
+     )
@@
- loyalty_point_details = query.run(as_dict=True)
+ loyalty_point_details = query.run(as_dict=True)

Notes:

  • If docstatus is not applicable for this DocType, drop that predicate. Otherwise it prevents draft/cancelled entries from affecting totals.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)
if company:
condition = " and company=%s " % frappe.db.escape(company)
query = query.where(LoyaltyPointEntry.company == company)
if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date
loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
)
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
loyalty_point_details = query.run(as_dict=True)
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Coalesce(Sum(LoyaltyPointEntry.loyalty_points), 0).as_("loyalty_points"),
Coalesce(Sum(LoyaltyPointEntry.purchase_amount), 0).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
& (LoyaltyPointEntry.docstatus == 1)
)
)
if company:
query = query.where(LoyaltyPointEntry.company == company)
if not include_expired_entry:
# include non-expiring entries too
query = query.where(
(LoyaltyPointEntry.expiry_date >= expiry_date)
| (LoyaltyPointEntry.expiry_date.isnull())
)
loyalty_point_details = query.run(as_dict=True)

Comment on lines +61 to +67
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against missing POS Profile and null r.message to avoid runtime errors

If pos_profile is unset or get_value returns no record, r.message will be undefined and r.message.set_grand_total_to_default_mop access will throw. Add a guard and bail early when pos_profile is not available.

Apply this diff:

-			frappe.db
-				.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
-				.then((r) => {
-					if (!r.exc) {
-						me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
-					}
-				});
+			if (this.frm.doc.pos_profile) {
+				frappe.db
+					.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
+					.then((r) => {
+						if (!r.exc && r.message) {
+							me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
+						}
+					});
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
.then((r) => {
if (!r.exc) {
me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
}
});
if (this.frm.doc.pos_profile) {
frappe.db
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
.then((r) => {
if (!r.exc && r.message) {
me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
}
});
}
🤖 Prompt for AI Agents
In erpnext/accounts/doctype/sales_invoice/sales_invoice.js around lines 61 to
67, the code calls frappe.db.get_value with this.frm.doc.pos_profile but does
not guard for a missing pos_profile or a missing r.message; accessing
r.message.set_grand_total_to_default_mop can throw. Add an early return if
this.frm.doc.pos_profile is falsy before calling get_value, and after the
promise resolves check that r and r.message are defined before reading
set_grand_total_to_default_mop; only assign me.frm.set_default_payment when the
property exists (otherwise leave it unchanged or set a safe default).

Comment on lines +240 to +291
def get_asset_value_adjustment_map_by_category(filters):
asset_value_adjustments = frappe.db.sql(
"""
SELECT
a.asset_category AS asset_category,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date

FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.asset_category
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)

category_value_adjustment_map = {}

for r in asset_value_adjustments:
category_value_adjustment_map[r["asset_category"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}

return category_value_adjustment_map

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix disposal-date guard and add finance_book gating for value adjustments.

Two issues in the SQL:

  • value_adjustment_till_to_date excludes all adjustments for assets disposed before to_date, even those posted before disposal. This understates adjustment_during_period for disposed assets.
  • finance_book filtering is missing, while other queries in this report honor it. This can produce inconsistent results when a finance_book filter is applied.

Proposed fix:

  • Bound “till” by disposal_date when present: include entries up to LEAST(to_date, disposal_date).
  • Honor finance_book when provided and gate assets similarly to other queries.

Apply this diff:

 def get_asset_value_adjustment_map_by_category(filters):
-	asset_value_adjustments = frappe.db.sql(
-		"""
+	finance_book_filter = ""
+	exists_condition = ""
+	if filters.get("finance_book"):
+		finance_book_filter = " and ifnull(gle.finance_book, '')=%(finance_book)s"
+		exists_condition = (
+			" and exists (select 1 from `tabAsset Depreciation Schedule` ads "
+			"where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
+		)
+
+	asset_value_adjustments = frappe.db.sql(
+		f"""
 		SELECT
 			a.asset_category AS asset_category,
 			IFNULL(
 				SUM(
 					CASE
 						WHEN gle.posting_date < %(from_date)s
 								AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
 						THEN gle.debit - gle.credit
 						ELSE 0
 					END
 				),
 			0) AS value_adjustment_before_from_date,
 			IFNULL(
 				SUM(
 					CASE
-						WHEN gle.posting_date <= %(to_date)s
-								AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
+						WHEN gle.posting_date <= %(to_date)s
+								AND (a.disposal_date IS NULL OR gle.posting_date <= a.disposal_date)
 						THEN gle.debit - gle.credit
 						ELSE 0
 					END
 				),
 			0) AS value_adjustment_till_to_date
 
 		FROM `tabGL Entry` gle
 		JOIN `tabAsset` a ON gle.against_voucher = a.name
 		JOIN `tabAsset Category Account` aca
 			ON aca.parent = a.asset_category
 			AND aca.company_name = %(company)s
 		WHERE gle.is_cancelled = 0
 			AND a.docstatus = 1
 			AND a.company = %(company)s
 			AND a.purchase_date <= %(to_date)s
 			AND gle.account = aca.fixed_asset_account
-		GROUP BY a.asset_category
-	""",
-		{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
+			{exists_condition}
+			{finance_book_filter}
+		GROUP BY a.asset_category
+	""",
+		{
+			"from_date": filters.from_date,
+			"to_date": filters.to_date,
+			"company": filters.company,
+			"finance_book": filters.get("finance_book"),
+		},
 		as_dict=1,
 	)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_asset_value_adjustment_map_by_category(filters):
asset_value_adjustments = frappe.db.sql(
"""
SELECT
a.asset_category AS asset_category,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.asset_category
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)
category_value_adjustment_map = {}
for r in asset_value_adjustments:
category_value_adjustment_map[r["asset_category"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return category_value_adjustment_map
def get_asset_value_adjustment_map_by_category(filters):
# Gate by finance_book if provided
finance_book_filter = ""
exists_condition = ""
if filters.get("finance_book"):
finance_book_filter = " and ifnull(gle.finance_book, '')=%(finance_book)s"
exists_condition = (
" and exists (select 1 from `tabAsset Depreciation Schedule` ads "
"where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
)
asset_value_adjustments = frappe.db.sql(
f"""
SELECT
a.asset_category AS asset_category,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR gle.posting_date <= a.disposal_date)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
{exists_condition}
{finance_book_filter}
GROUP BY a.asset_category
""",
{
"from_date": filters.from_date,
"to_date": filters.to_date,
"company": filters.company,
"finance_book": filters.get("finance_book"),
},
as_dict=1,
)
category_value_adjustment_map = {}
for r in asset_value_adjustments:
category_value_adjustment_map[r["asset_category"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return category_value_adjustment_map

Comment on lines +510 to +561
def get_asset_value_adjustment_map(filters):
asset_with_value_adjustments = frappe.db.sql(
"""
SELECT
a.name AS asset,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date

FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.name
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)

asset_value_adjustment_map = {}

for r in asset_with_value_adjustments:
asset_value_adjustment_map[r["asset"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}

return asset_value_adjustment_map

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Mirror the disposal-date and finance_book fixes for per-asset adjustments.

Same concerns as the category-level function:

  • Include adjustments up to disposal_date (if any) rather than excluding disposed assets from the “till” bucket.
  • Honor finance_book consistently.

Apply this diff:

 def get_asset_value_adjustment_map(filters):
-	asset_with_value_adjustments = frappe.db.sql(
-		"""
+	finance_book_filter = ""
+	exists_condition = ""
+	if filters.get("finance_book"):
+		finance_book_filter = " and ifnull(gle.finance_book, '')=%(finance_book)s"
+		exists_condition = (
+			" and exists (select 1 from `tabAsset Depreciation Schedule` ads "
+			"where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
+		)
+
+	asset_with_value_adjustments = frappe.db.sql(
+		f"""
 		SELECT
 			a.name AS asset,
 			IFNULL(
 				SUM(
 					CASE
 						WHEN gle.posting_date < %(from_date)s
 								AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
 						THEN gle.debit - gle.credit
 						ELSE 0
 					END
 				),
 			0) AS value_adjustment_before_from_date,
 			IFNULL(
 				SUM(
 					CASE
-						WHEN gle.posting_date <= %(to_date)s
-								AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
+						WHEN gle.posting_date <= %(to_date)s
+								AND (a.disposal_date IS NULL OR gle.posting_date <= a.disposal_date)
 						THEN gle.debit - gle.credit
 						ELSE 0
 					END
 				),
 			0) AS value_adjustment_till_to_date
 
 		FROM `tabGL Entry` gle
 		JOIN `tabAsset` a ON gle.against_voucher = a.name
 		JOIN `tabAsset Category Account` aca
 			ON aca.parent = a.asset_category
 			AND aca.company_name = %(company)s
 		WHERE gle.is_cancelled = 0
 			AND a.docstatus = 1
 			AND a.company = %(company)s
 			AND a.purchase_date <= %(to_date)s
 			AND gle.account = aca.fixed_asset_account
-		GROUP BY a.name
-	""",
-		{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
+			{exists_condition}
+			{finance_book_filter}
+		GROUP BY a.name
+	""",
+		{
+			"from_date": filters.from_date,
+			"to_date": filters.to_date,
+			"company": filters.company,
+			"finance_book": filters.get("finance_book"),
+		},
 		as_dict=1,
 	)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_asset_value_adjustment_map(filters):
asset_with_value_adjustments = frappe.db.sql(
"""
SELECT
a.name AS asset,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(to_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
GROUP BY a.name
""",
{"from_date": filters.from_date, "to_date": filters.to_date, "company": filters.company},
as_dict=1,
)
asset_value_adjustment_map = {}
for r in asset_with_value_adjustments:
asset_value_adjustment_map[r["asset"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return asset_value_adjustment_map
def get_asset_value_adjustment_map(filters):
finance_book_filter = ""
exists_condition = ""
if filters.get("finance_book"):
finance_book_filter = " and ifnull(gle.finance_book, '')=%(finance_book)s"
exists_condition = (
" and exists (select 1 from `tabAsset Depreciation Schedule` ads "
"where ads.asset = a.name and ads.finance_book = %(finance_book)s)"
)
asset_with_value_adjustments = frappe.db.sql(
f"""
SELECT
a.name AS asset,
IFNULL(
SUM(
CASE
WHEN gle.posting_date < %(from_date)s
AND (a.disposal_date IS NULL OR a.disposal_date >= %(from_date)s)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_before_from_date,
IFNULL(
SUM(
CASE
WHEN gle.posting_date <= %(to_date)s
AND (a.disposal_date IS NULL OR gle.posting_date <= a.disposal_date)
THEN gle.debit - gle.credit
ELSE 0
END
),
0) AS value_adjustment_till_to_date
FROM `tabGL Entry` gle
JOIN `tabAsset` a ON gle.against_voucher = a.name
JOIN `tabAsset Category Account` aca
ON aca.parent = a.asset_category
AND aca.company_name = %(company)s
WHERE gle.is_cancelled = 0
AND a.docstatus = 1
AND a.company = %(company)s
AND a.purchase_date <= %(to_date)s
AND gle.account = aca.fixed_asset_account
{exists_condition}
{finance_book_filter}
GROUP BY a.name
""",
{
"from_date": filters.from_date,
"to_date": filters.to_date,
"company": filters.company,
"finance_book": filters.get("finance_book"),
},
as_dict=1,
)
asset_value_adjustment_map = {}
for r in asset_with_value_adjustments:
asset_value_adjustment_map[r["asset"]] = {
"adjustment_before_from_date": flt(r.get("value_adjustment_before_from_date", 0)),
"adjustment_till_to_date": flt(r.get("value_adjustment_till_to_date", 0)),
}
return asset_value_adjustment_map
🤖 Prompt for AI Agents
In
erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
around lines 510 to 561, the per-asset SQL currently excludes disposed assets
from the "till" bucket and ignores the finance_book filter; update the query to
honor finance_book and to include value adjustments up to an asset's
disposal_date by (1) joining tabAsset Finance Book (alias afb) ON afb.parent =
a.name AND afb.finance_book = %(finance_book)s and adding %(finance_book)s to
the query params, and (2) change the CASE conditions so that the "till"
aggregation requires gle.posting_date <= %(to_date)s AND gle.posting_date <=
COALESCE(a.disposal_date, %(to_date)s) (similarly ensure the "before"
aggregation only counts entries with gle.posting_date < %(from_date)s AND
gle.posting_date <= COALESCE(a.disposal_date, %(from_date)s)); return the same
map structure but now filtered by finance_book and respecting disposal_date when
computing the two buckets.

Comment on lines +93 to 95
row = d.as_dict(no_private_properties=True)
row.pop("name")
return row

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Good sanitization shift; make pop safe and confirm framework support for the flag

Switching to as_dict(no_private_properties=True) is a sensible improvement. Two follow-ups:

  • Use a safe pop to avoid surprises if name isn’t present.
  • Verify the installed Frappe version supports the no_private_properties kwarg to prevent a runtime TypeError.

Apply this diff for the safe pop:

-			row = d.as_dict(no_private_properties=True)
-			row.pop("name")
+			row = d.as_dict(no_private_properties=True)
+			row.pop("name", None)

To confirm framework support for the flag:


🌐 Web query:

Does the Frappe Document.as_dict method support the `no_private_properties` argument (and from which version)?

💡 Result:

Short answer: No — there is no documented / repo-backed argument named no_private_properties on the core frappe.model.Document.as_dict. It is not part of the public API in the docs, and a code search of the Frappe repo does not show that name.

Details and sources

  • The official Document API/docs show as_dict used without that argument (doc.as_dict()) and do not document a no_private_properties parameter. (docs.frappe.io)
  • In the Frappe codebase some DocTypes override as_dict (for example Email Queue uses as_dict(include_recipients=False)), which shows custom kwargs are possible on per-doctype implementations — but that is not the same as a core Document.as_dict argument named no_private_properties. (github.com)

If you want, I can:

  • search a particular Frappe tag/branch (for example v13, v14, v15 or develop) and show the exact as_dict signature / implementation and any PR that introduced a parameter, or
  • give a short snippet you can use to remove private keys (keys starting with "_") from doc.as_dict() output.

Which would you prefer?

Citations:


Remove private properties manually and use safe pop

The no_private_properties argument isn’t supported by the core Document.as_dict. Instead:

  • Call as_dict() without extra kwargs.
  • Strip out keys beginning with _ yourself.
  • Use pop("name", None) to avoid KeyError if "name" is missing.

Apply this diff:

-			row = d.as_dict(no_private_properties=True)
-			row.pop("name")
+			# get full dict, then remove private fields
+			row = d.as_dict()
+			row = {k: v for k, v in row.items() if not k.startswith("_")}
+			row.pop("name", None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
row = d.as_dict(no_private_properties=True)
row.pop("name")
return row
- row = d.as_dict(no_private_properties=True)
# get full dict, then remove private fields
row = d.as_dict()
row = {k: v for k, v in row.items() if not k.startswith("_")}
row.pop("name", None)
return row
🤖 Prompt for AI Agents
In erpnext/setup/doctype/item_group/item_group.py around lines 93 to 95, the
code calls d.as_dict(no_private_properties=True) and then row.pop("name") which
is invalid because Document.as_dict doesn't accept that kwarg and pop may raise
KeyError; change to call d.as_dict() with no extra kwargs, remove any keys that
start with "_" from the returned dict (e.g., iterate over list(row.keys()) and
delete keys beginning with "_"), and use row.pop("name", None) to safely remove
the name key without raising an error.

Comment on lines 669 to 673
customer = create_internal_customer(
"_Test Internal Customer 3",
"_Test Internal Customer 2",
company,
company,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Change to use “_Test Internal Customer 2” looks good; aligns with common fixtures.

This should reduce fixture divergence and improve test idempotency where “2” is the established internal customer in other tests.

Run this quick scan to ensure no remaining references to “_Test Internal Customer 3” linger elsewhere:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C2 -F '_Test Internal Customer 3' || true

Length of output: 2054


Inconsistent Internal Customer Fixture References Detected
The quick scan reveals lingering uses of “_Test Internal Customer 3” in the sales invoice tests. To maintain consistency and idempotency, please update all of these to “_Test Internal Customer 2”:

• erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py:78
• …/test_sales_invoice.py:2743
• …/test_sales_invoice.py:4948
• …/test_sales_invoice.py:4974

Once updated, rerun your scan to confirm no “_Test Internal Customer 3” references remain.

🤖 Prompt for AI Agents
In erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py around
lines 669 to 673, the internal customer fixture is created as "_Test Internal
Customer 2" but other sales invoice tests still reference "_Test Internal
Customer 3"; update all lingering references in the listed files
(erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py at lines near 78,
2743, 4948, 4974) to "_Test Internal Customer 2" so fixtures are consistent and
idempotent, then run the test/scan suite to verify no "_Test Internal Customer
3" occurrences remain.

Comment on lines +634 to 666
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")

query = (
frappe.qb.from_(mr)
.from_(mr_item)
.select(mr.name)
.distinct()
.select(mr.transaction_date, mr.company)
.where(
(mr.name == mr_item.parent)
& (mr_item.item_code.isin(supplier_items))
& (mr.material_request_type == "Purchase")
& (mr.per_ordered < 99.99)
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.company == filters.get("company"))
)
.orderby(mr_item.item_code, order=Order.asc)
.limit(cint(page_len))
.offset(cint(start))
)

if txt:
query = query.where(mr.name.like(f"%%{txt}%%"))

if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
query = query.where(mr.transaction_date[date[0] : date[1]])

material_requests = query.run(as_dict=True)

return material_requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Fix incorrect date-range filter and prefer an explicit JOIN

Two issues:

  • The date filter uses slicing syntax mr.transaction_date[date[0] : date[1]], which is not a valid Frappe QB predicate. Use between(from_date, to_date).
  • Prefer an explicit JOIN over a cross-join + where filter for clarity and query planning.
-	mr = frappe.qb.DocType("Material Request")
-	mr_item = frappe.qb.DocType("Material Request Item")
+	mr = frappe.qb.DocType("Material Request")
+	mr_item = frappe.qb.DocType("Material Request Item")

-	query = (
-		frappe.qb.from_(mr)
-		.from_(mr_item)
+	query = (
+		frappe.qb.from_(mr)
+		.join(mr_item)
+		.on(mr.name == mr_item.parent)
 		.select(mr.name)
 		.distinct()
 		.select(mr.transaction_date, mr.company)
-		.where(
-			(mr.name == mr_item.parent)
-			& (mr_item.item_code.isin(supplier_items))
+		.where(
+			(mr_item.item_code.isin(supplier_items))
 			& (mr.material_request_type == "Purchase")
 			& (mr.per_ordered < 99.99)
 			& (mr.docstatus == 1)
 			& (mr.status != "Stopped")
 			& (mr.company == filters.get("company"))
 		)
 		.orderby(mr_item.item_code, order=Order.asc)
 		.limit(cint(page_len))
 		.offset(cint(start))
 	)

 	if txt:
 		query = query.where(mr.name.like(f"%%{txt}%%"))

-	if filters.get("transaction_date"):
-		date = filters.get("transaction_date")[1]
-		query = query.where(mr.transaction_date[date[0] : date[1]])
+	if filters.get("transaction_date"):
+		# Expecting filters["transaction_date"] like ["between", [from_date, to_date]]
+		_, date_range = filters.get("transaction_date")
+		from_date, to_date = date_range
+		query = query.where(mr.transaction_date.between(from_date, to_date))

 	material_requests = query.run(as_dict=True)

To ensure no other instances use the invalid slice syntax on QB fields, you can scan the codebase:


🏁 Script executed:

#!/bin/bash
rg -nP --type=py -C2 '\.\w+\[[^\]]+:[^\]]+\]' | sed -n '1,200p'

Length of output: 8533


Enforce valid date‐range filtering and use explicit JOINs in all QB queries

The Python slice syntax (e.g. field[from:to]) is not a supported Frappe QB predicate and must be replaced with the .between(start, end) method. Likewise, prefer .join(…) over cross-joins for better readability and query planning. Besides the Material Request snippet, the scan uncovered other occurrences:

• erpnext/stock/doctype/material_request/material_request.py (Lines 634–666)
– use
diff - query = ( - frappe.qb.from_(mr) - .from_(mr_item) + query = ( + frappe.qb.from_(mr) + .join(mr_item).on(mr.name == mr_item.parent) … - if filters.get("transaction_date"): - date = filters.get("transaction_date")[1] - query = query.where(mr.transaction_date[date[0] : date[1]]) + if filters.get("transaction_date"): + _, (from_date, to_date) = filters["transaction_date"] + query = query.where(mr.transaction_date.between(from_date, to_date))

• erpnext/stock/report/stock_ledger/stock_ledger.py (Line 396)
Replace

- .where(... & (sle.posting_datetime[from_date:to_date]))
+ .where(... & sle.posting_datetime.between(from_date, to_date))

• erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py (Line 212)
Replace

- & (so.transaction_date[conditions.start_date : conditions.end_date])
+ & so.transaction_date.between(conditions.start_date, conditions.end_date)

• erpnext/controllers/budget_controller.py (Lines 215, 246, 279)
Replace each

- po.transaction_date[self.fy_start_date : self.fy_end_date]
+ po.transaction_date.between(self.fy_start_date, self.fy_end_date)

(and similarly for mr.transaction_date and gl.posting_date)

• erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py (Lines 505, 674)
Replace

- ple.posting_date[from_date:to_date]
+ ple.posting_date.between(from_date, to_date)

• erpnext/accounts/report/invalid_ledger_entries/invalid_ledger_entries.py (Lines 113, 128)
Replace

- gle.posting_date[filters.from_date : filters.to_date]
+ gle.posting_date.between(filters.from_date, filters.to_date)

(and the identical slice on ple.posting_date)

Please apply these refactors throughout the codebase to ensure all QB date‐range filters and joins are valid and explicit.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(mr)
.from_(mr_item)
.select(mr.name)
.distinct()
.select(mr.transaction_date, mr.company)
.where(
(mr.name == mr_item.parent)
& (mr_item.item_code.isin(supplier_items))
& (mr.material_request_type == "Purchase")
& (mr.per_ordered < 99.99)
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.company == filters.get("company"))
)
.orderby(mr_item.item_code, order=Order.asc)
.limit(cint(page_len))
.offset(cint(start))
)
if txt:
query = query.where(mr.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
date = filters.get("transaction_date")[1]
query = query.where(mr.transaction_date[date[0] : date[1]])
material_requests = query.run(as_dict=True)
return material_requests
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
query = (
frappe.qb.from_(mr)
.join(mr_item).on(mr.name == mr_item.parent)
.select(mr.name)
.distinct()
.select(mr.transaction_date, mr.company)
.where(
(mr.name == mr_item.parent)
& (mr_item.item_code.isin(supplier_items))
& (mr.material_request_type == "Purchase")
& (mr.per_ordered < 99.99)
& (mr.docstatus == 1)
& (mr.status != "Stopped")
& (mr.company == filters.get("company"))
)
.orderby(mr_item.item_code, order=Order.asc)
.limit(cint(page_len))
.offset(cint(start))
)
if txt:
query = query.where(mr.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
_, (from_date, to_date) = filters["transaction_date"]
query = query.where(mr.transaction_date.between(from_date, to_date))
material_requests = query.run(as_dict=True)
return material_requests

Comment on lines +1422 to 1426
"filter": lambda d: (
get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0
)
and select_item(d),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: pending-qty filter is inverted; normal flow should include items with pending_qty > 0

For non-return PRs, items with pending_qty > 0 should be mapped. The current <= 0 condition likely excludes the intended items. For returns, include negative pending_qty.

Apply this diff:

-				"filter": lambda d: (
-					get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0
-				)
-				and select_item(d),
+				"filter": lambda d: select_item(d)
+				and (
+					get_pending_qty(d)[0] > 0
+					if not doc.get("is_return")
+					else get_pending_qty(d)[0] < 0
+				),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"filter": lambda d: (
get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0
)
and select_item(d),
},
"filter": lambda d: select_item(d)
and (
get_pending_qty(d)[0] > 0
if not doc.get("is_return")
else get_pending_qty(d)[0] < 0
),
🤖 Prompt for AI Agents
In erpnext/stock/doctype/purchase_receipt/purchase_receipt.py around lines 1422
to 1426, the lambda filter for pending_qty is inverted: for non-return PRs it
currently uses <= 0 and for returns it uses > 0. Change the non-return branch to
require get_pending_qty(d)[0] > 0 and change the return branch to require
get_pending_qty(d)[0] < 0, preserving the trailing "and select_item(d)" so that
normal PRs include items with positive pending_qty and returns include negative
pending_qty.

Comment on lines +1006 to +1012
this.barcode_scanner = new erpnext.utils.BarcodeScanner({
frm: this.frm,
warehouse_field: (doc) => {
return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse";
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Fix warehouse selection for Material Receipt scans (and guard against duplicate scanner init).

Routing all non-Transfer scans to s_warehouse will break Material Receipt (and other inbound) flows where only t_warehouse is relevant. Also, setup can be re-run in some edge navigations; guard scanner re-instantiation.

Apply this diff:

-		this.barcode_scanner = new erpnext.utils.BarcodeScanner({
-			frm: this.frm,
-			warehouse_field: (doc) => {
-				return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse";
-			},
-		});
+		if (!this.barcode_scanner) {
+			this.barcode_scanner = new erpnext.utils.BarcodeScanner({
+				frm: this.frm,
+				warehouse_field: (doc) => {
+					// Inbound purposes should use target warehouse; outbound use source
+					const inboundPurposes = [
+						"Material Receipt",
+						"Manufacture",
+						"Repack",
+						"Send to Subcontractor",
+						"Disassemble",
+					];
+					if (doc.purpose === "Material Transfer") return "t_warehouse";
+					return inboundPurposes.includes(doc.purpose) ? "t_warehouse" : "s_warehouse";
+				},
+			});
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.barcode_scanner = new erpnext.utils.BarcodeScanner({
frm: this.frm,
warehouse_field: (doc) => {
return doc.purpose === "Material Transfer" ? "t_warehouse" : "s_warehouse";
},
});
// Initialize barcode scanner once, with correct warehouse logic for inbound vs outbound
if (!this.barcode_scanner) {
this.barcode_scanner = new erpnext.utils.BarcodeScanner({
frm: this.frm,
warehouse_field: (doc) => {
// Inbound purposes should use target warehouse; outbound use source
const inboundPurposes = [
"Material Receipt",
"Manufacture",
"Repack",
"Send to Subcontractor",
"Disassemble",
];
if (doc.purpose === "Material Transfer") return "t_warehouse";
return inboundPurposes.includes(doc.purpose) ? "t_warehouse" : "s_warehouse";
},
});
}
🤖 Prompt for AI Agents
In erpnext/stock/doctype/stock_entry/stock_entry.js around lines 1006-1012, the
barcode scanner is always routing non-Transfer scans to s_warehouse and is
re-instantiated on repeated setup; change the warehouse_field logic to return
t_warehouse for inbound purposes (e.g., "Material Receipt") as well as for
"Material Transfer", otherwise use s_warehouse, and add a guard so you do not
recreate this.barcode_scanner if it already exists (e.g., if
this.barcode_scanner is truthy, skip creation or reuse the existing instance).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
erpnext/controllers/accounts_controller.py (2)

2586-2606: Ensure due_date alignment to invoice posting_date and set base_outstanding

Two gaps in the auto-fetch path:

  • Computed due_date isn’t aligned to the invoice posting_date (as done elsewhere), which can yield a due date earlier than the invoice’s posting date when bill_date < posting_date.
  • base_outstanding isn’t initialized; elsewhere it’s set to base_payment_amount for consistency.

Apply this diff to address both:

@@
-               if schedule.due_date_based_on:
-                   payment_schedule["due_date"] = get_due_date(schedule, posting_date)
+               if schedule.due_date_based_on:
+                   payment_schedule["due_date"] = get_due_date(schedule, posting_date)
+                   # Align computed due_date to invoice posting_date if it falls earlier
+                   if self.get("posting_date") and getdate(payment_schedule["due_date"]) < getdate(self.get("posting_date")):
+                       payment_schedule["due_date"] = self.posting_date
@@
-               payment_schedule["outstanding"] = payment_schedule["payment_amount"]
+               payment_schedule["outstanding"] = payment_schedule["payment_amount"]
+               payment_schedule["base_outstanding"] = payment_schedule["base_payment_amount"]

3429-3431: Guard against None when aligning due_date to posting_date
The comparison in erpnext/controllers/accounts_controller.py:3429 can fail or misbehave if either posting_date or term_details.due_date is None (e.g. when callers supply only bill_date). Add a simple guard to skip the check unless both dates are present:

• File: erpnext/controllers/accounts_controller.py
Lines: 3429–3431

-	if getdate(term_details.due_date) < getdate(posting_date):
-		term_details.due_date = posting_date
+	if posting_date and term_details.due_date and getdate(term_details.due_date) < getdate(posting_date):
+		term_details.due_date = posting_date
♻️ Duplicate comments (1)
erpnext/controllers/accounts_controller.py (1)

2588-2591: Confirmed: fixed the due_date_based_on field assignment bug

The field now correctly copies schedule.due_date_based_on (instead of schedule.credit_days) as flagged earlier. Thanks for addressing.

🧹 Nitpick comments (2)
erpnext/controllers/accounts_controller.py (2)

3399-3416: Avoid duplicate assignment of payment_term; centralize fields to copy

You already set payment_term (Line 3398) and again include it in fields_to_copy. It’s harmless but redundant. Drop it from fields_to_copy for clarity.

 fields_to_copy = [
   "description",
   "invoice_portion",
-  "payment_term",
   "discount_type",
   "discount",
   "mode_of_payment",
   "due_date_based_on",
   "credit_days",
   "credit_months",
   "discount_validity_based_on",
   "discount_validity",
 ]

3435-3445: Safer fallback when due_date_based_on is unset or unrecognized

Minor hardening: default to the base date when due_date_based_on is unset/unknown so callers always get a usable date (instead of None) unless explicitly overridden by default_date.

-def get_due_date(term, posting_date=None, bill_date=None, default_date=None):
-    due_date = default_date
-    date = bill_date or posting_date
+def get_due_date(term, posting_date=None, bill_date=None, default_date=None):
+    date = bill_date or posting_date
+    due_date = default_date or date
     if term.due_date_based_on == "Day(s) after invoice date":
         due_date = add_days(date, term.credit_days)
     elif term.due_date_based_on == "Day(s) after the end of the invoice month":
         due_date = add_days(get_last_day(date), term.credit_days)
     elif term.due_date_based_on == "Month(s) after the end of the invoice month":
         due_date = get_last_day(add_months(date, term.credit_months))
     return due_date
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b04750c and ba5c85d.

📒 Files selected for processing (2)
  • erpnext/controllers/accounts_controller.py (4 hunks)
  • erpnext/stock/doctype/delivery_note/test_delivery_note.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • erpnext/stock/doctype/delivery_note/test_delivery_note.py
🧰 Additional context used
🧬 Code Graph Analysis (1)
erpnext/controllers/accounts_controller.py (1)
erpnext/accounts/party.py (1)
  • get_due_date (625-646)
🪛 GitHub Actions: Linters
erpnext/controllers/accounts_controller.py

[warning] 1-1: Semgrep timeout error while running rule frappe-semgrep-rules.rules.frappe-monkey-patching-not-allowed. Current version has 159 findings.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Patch Test
🔇 Additional comments (1)
erpnext/controllers/accounts_controller.py (1)

2573-2573: LGTM: Correct basis date selection for term computations

Using bill_date → posting_date → transaction_date as the base is aligned with the PR intent and prevents stale due dates from orders.

@ljain112 ljain112 force-pushed the fix-due-date-orders branch from ba5c85d to cac3dbf Compare August 20, 2025 10:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
erpnext/controllers/accounts_controller.py (2)

2586-2606: Set base_outstanding when auto-fetching terms to avoid inconsistent outstanding values.

When automatically_fetch_payment_terms is enabled and allocation is based on terms (the else-branch in set_payment_schedule re-fetches and does not normalize fields), base_outstanding may remain unset on the appended rows. Set it alongside base_payment_amount here.

Apply this diff:

 				payment_schedule["payment_amount"] = flt(
 					grand_total * flt(payment_schedule["invoice_portion"]) / 100,
 					schedule.precision("payment_amount"),
 				)
 				payment_schedule["base_payment_amount"] = flt(
 					base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
 					schedule.precision("base_payment_amount"),
 				)
+				payment_schedule["base_outstanding"] = payment_schedule["base_payment_amount"]
 				payment_schedule["outstanding"] = payment_schedule["payment_amount"]

3422-3431: Guard against unintended due-date bump when only bill_date is provided.

Line 3429 compares due_date against posting_date unconditionally. If posting_date is None, getdate(posting_date) resolves to today, which can incorrectly advance due dates computed from bill_date.

Add a guard so this alignment only happens when posting_date is actually provided.

Proposed adjustment (outside the changed lines) for clarity:

# Only align when posting_date is provided
if posting_date and term_details.due_date and getdate(term_details.due_date) < getdate(posting_date):
    term_details.due_date = posting_date
♻️ Duplicate comments (1)
erpnext/controllers/accounts_controller.py (1)

2588-2588: Good fix: Correctly copying due_date_based_on.

Assignment now uses schedule.due_date_based_on (was credit_days earlier). This aligns with the PR objective and unblocks correct due-date computation.

🧹 Nitpick comments (3)
erpnext/controllers/accounts_controller.py (3)

2573-2573: Use a neutral variable name for the computed reference date.

This local variable can be a bill date, posting date, or transaction date. Naming it reference_date would better reflect its role and reduce confusion when passed into due/discount-date calculators.


3400-3416: Minor duplication: payment_term is assigned twice.

It’s set in the else: branch above and also included in fields_to_copy. Keeping just one source reduces redundancy. Either remove the else: assignment or drop payment_term from fields_to_copy (but not both).


3435-3444: get_due_date: Provide a safe fallback when rules are missing.

If term.due_date_based_on is empty/unrecognized, the function currently returns default_date (often None). Fallback to the computed date to avoid returning None and silently breaking downstream validations.

Apply this diff:

 def get_due_date(term, posting_date=None, bill_date=None, default_date=None):
 	due_date = default_date
 	date = bill_date or posting_date
 	if term.due_date_based_on == "Day(s) after invoice date":
 		due_date = add_days(date, term.credit_days)
 	elif term.due_date_based_on == "Day(s) after the end of the invoice month":
 		due_date = add_days(get_last_day(date), term.credit_days)
 	elif term.due_date_based_on == "Month(s) after the end of the invoice month":
 		due_date = get_last_day(add_months(date, term.credit_months))
+	if not due_date:
+		due_date = date
 	return due_date
📜 Review details

Configuration used: .coderabbit.yml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ba5c85d and cac3dbf.

📒 Files selected for processing (2)
  • erpnext/controllers/accounts_controller.py (4 hunks)
  • erpnext/stock/doctype/delivery_note/test_delivery_note.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • erpnext/stock/doctype/delivery_note/test_delivery_note.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Patch Test
🔇 Additional comments (1)
erpnext/controllers/accounts_controller.py (1)

3400-3416: LGTM: Propagating term metadata to payment schedule is correct.

Copying due_date_based_on, credit_days, credit_months, discount_validity_based_on, and discount_validity makes the invoice self-descriptive and supports recomputation on mapping.

@ljain112 ljain112 force-pushed the fix-due-date-orders branch from 76768cf to 938819e Compare August 20, 2025 11:08
@ljain112 ljain112 closed this Aug 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.