Conservative cleanup of frappe.throw/msgprint messages per the message
style guide; meaning, severity, and .format() arguments are unchanged:
- index bare {} placeholders as {0}/{1}/... so translators can reorder
- move f-strings / .format() / concatenation out of _() (they break
gettext extraction and never translate)
- wrap translatable dynamic values (DocType/Select labels) in _()
- fix grammar and colloquialisms ("doesn't belongs" -> "does not belong",
"till" -> "until", "Rules exists" -> "Rules exist", exclusive "one of
X and Y" -> "one of X, Y, or Z")
- drop no-op _() wrapping runtime-built HTML strings
Part of #53976.
_get_pricing_rules orders by priority desc; get_pricing_rules then reads
pricing_rules[0].has_priority. priority is a Select (varchar): unset is '' on
MariaDB but NULL on Postgres, which sorts to the top under DESC and flips the
selection. Order by coalesce(priority, '') desc so the unset value sorts last
('' is the text minimum) on both backends.
Replaces the previous over-engineered stub with 7 short functions.
Account data, Account Closing Balance, and all metadata come from
frappe.db as normal; only tabGL Entry is read from the duckdb_conn.
Reuses get_opening_balance() for Account Closing Balance unchanged,
reuses all downstream compute helpers (calculate_values, prepare_data,
etc.) unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the placeholder stub with 8 focused functions that mirror the
normal execute() flow using parameterized DuckDB SQL queries: account
fetch, period GL entries, opening balances (with Period Closing Voucher
path), and all filters (cost center, project, finance book, accounting
dimensions). Reuses existing pure-Python processing functions unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.