make_payment_request(**args) is whitelisted and the client passes
`schedules` as a list, so json.loads(args.get("schedules")) raised
TypeError under JSON body mode. Use frappe.parse_json.
Replace json.loads(object_hook=...) with frappe.parse_json and wrap each
row in frappe._dict, fixing attribute access when args arrive as native
JSON (list of dicts) instead of a JSON string.
Adds a `pcv_job_timeout` Int field (default 3600s) to Accounts Settings
so admins can tune the enqueue timeout for PCV background jobs without
a code change. All three `frappe.enqueue` calls in
`process_period_closing_voucher.py` now read this value at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Financial Report Template calculation_formula filters are user-authored and
only validated for field existence + operator membership, not that a
like/ilike operator targets a text field. A filter such as
["is_group", "like", "1"] builds `is_group ILIKE '%1%'`; PostgreSQL has no
LIKE/ILIKE operator for a smallint/int/numeric column
(`operator does not exist: smallint ~~* unknown`) and aborts the report, while
MariaDB implicitly casts the numeric column to text and matches.
For like-family operators, cast a numeric/Check Account field to varchar
(`Cast_(field, "varchar")`), reproducing MariaDB's implicit numeric->text
coercion on both engines. Text-field filters (the normal account_name/
account_number case) are left untouched, so MariaDB output is unchanged.
* fix(accounts): wrap loose finance_book in max() in Trial Balance (Simple) (Postgres)
The Trial Balance (Simple) Query Report selects `finance_book` but groups only
by `fiscal_year, company, posting_date, account`. PostgreSQL rejects the
non-grouped, non-aggregated column:
column "tabGL Entry.finance_book" must appear in the GROUP BY clause or be
used in an aggregate function
MariaDB tolerates it and returns an arbitrary finance_book per group.
Wrapping it in `max(finance_book)` keeps the row count identical (the GROUP BY
is unchanged) and makes PostgreSQL valid. Adding finance_book to GROUP BY would
split each group into N rows and change the MariaDB row count, so it is not an
option. This replaces MariaDB's previously arbitrary finance_book value with a
deterministic one (the only sanctioned MariaDB-output change); the row count is
preserved.
* fix(accounts): make Sales Partners Commission valid on Postgres (ORDER BY + div-by-zero)
The Sales Partners Commission Query Report had two PostgreSQL problems:
1. It ended with `ORDER BY "Total Commission:Currency:120"`, but the alias the
query produces is `"Total Commission:Currency:170"` (width 170, not 120), so
the ORDER BY never referenced a real output column. On MariaDB a double-quoted
token is a string literal — a no-op sort that never errored. On PostgreSQL a
double-quoted token is an identifier, so it errors with
`column "Total Commission:Currency:120" does not exist` (and single-quoting it
instead trips `non-integer constant in ORDER BY`). Since the clause was always
a no-op on MariaDB, it is removed — MariaDB's group-order output is unchanged
and the report runs on PostgreSQL.
2. `sum(total_commission)*100 / sum(amount_eligible_for_commission)` has an
unguarded divisor: the inner query filters `total_commission`/`base_net_total`
but not `amount_eligible_for_commission`, so a partner whose rows sum to 0
there makes MariaDB return NULL but PostgreSQL raise `division by zero`. Wrap
it in `NULLIF(sum(amount_eligible_for_commission), 0)` — NULL on both engines.
Both verified live on MariaDB and PostgreSQL.
calculate_exchange_rate_using_last_gle divides (debit - credit) by
(debit_in_account_currency - credit_in_account_currency). The GL row is
re-selected by (voucher_type, voucher_no, account) ordered by posting_date
WITHOUT the "(debit_in_account_currency > 0) | (credit_in_account_currency > 0)"
filter the first query used, so the chosen row can have equal/zero account-
currency amounts, making the divisor 0.
MariaDB returns NULL for x/0 (the caller maps it via `or 0.0`); PostgreSQL
raises `division by zero` and aborts. Wrapping the divisor in NullIf(divisor, 0)
yields NULL on both engines, so MariaDB output is unchanged and PostgreSQL no
longer errors.
The Asset Depreciations and Balances report tested disposal status with
IfNull(asset.disposal_date, 0) != 0 / == 0 — coalescing the DATE column
disposal_date with the integer 0. frappe.qb renders this as
COALESCE("disposal_date", 0); PostgreSQL requires COALESCE arguments to
share a type and raises:
psycopg2.errors.DatatypeMismatch: COALESCE types date and integer
cannot be matched
The predicate is in the WHERE/CASE of every query the report runs (both
group_by=Asset Category and group_by=Asset), so the whole report errored
on PostgreSQL. MariaDB's IFNULL(date, 0) is permissive and worked.
Replace each comparison with the null-test form already used elsewhere in
this same file: IfNull(disposal_date, 0) != 0 -> disposal_date.isnotnull(),
== 0 -> disposal_date.isnull(). Semantically identical (a stored date is
never 0), valid on both engines, MariaDB output unchanged.
The Postgres-portability change moved the POS item-group filters to the query builder
(item.item_group.isin(...)) and frappe.get_all(["name","in",...]), which escape values
once. get_item_groups() still pre-escaped each name with frappe.db.escape(), so the
names were escaped TWICE -> `item_group IN ('''Products''')`, matching nothing. Any POS
Profile that restricts item groups returned ZERO items, on both MariaDB and Postgres.
Return raw names; the parameterized callers escape them correctly. (get_parent_item_group
also returned the quoted literal before this fix.) Add a regression test: a POS Profile
restricted to an item group must still surface that group's items — it returns 0 before
the fix and passes after, on both engines.
* feat: capitalize full actual charge on stock items only for Purchase Invoice
Extends #56102 (Purchase Receipt) to the Purchase Invoice GL: an actual
valuation charge (e.g. Freight) flagged 'Allocate Full Amount to Stock Items'
is fully capitalized onto stock/asset items only; when unchecked, only the
stock items' share of a spread-across-all-items charge is capitalized.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test: aggregate GL rows per account in PI freight test
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Across Sales Order, Delivery Note, Sales Invoice and POS Invoice, the
Commission section's commission_rate, total_commission and
amount_eligible_for_commission are sales-partner commission fields:
- depends_on eval:doc.sales_partner so they only show when a Sales Partner
is set;
- no_copy so a duplicated/amended document does not carry a stale commission
rate or computed commission amount (the sales partner itself still copies).
depends_on is client-only, so the server-side commission calculation is
unchanged. Add a Sales Order test for the no_copy behaviour.
Drop the dead 'if coupon:' guard (get_doc would have thrown) and collapse the
duplicate increment branches into a single exhausted-check plus increment.
No behaviour change.
Add tests for the previously-untested branches of validate_coupon_code
(not-yet-valid, expired, maximum-use exhausted) and update_coupon_code_count
(releasing a use on cancel, and rejecting use beyond the maximum). Both
functions are now fully covered.