Commit Graph

58968 Commits

Author SHA1 Message Date
Mihir Kandoi
b2ee8cb1b9 ci(postgres): fix semgrep + two review findings in the checker
- semgrep: annotate the source-reading open() with # nosemgrep for the
  frappe-security-file-traversal rule (dev-only lint tool; path comes from pre-commit,
  not user input).
- bool-scan: only inspect the field *value* arg (db_set args[1]/dict args[0];
  set_value args[3]/dict args[2]) so a positional update_modified=False
  (e.g. db_set('f', 0, False)) no longer false-positives.
- # pg-ok: also honour the annotation on a multi-line call's closing paren line
  (scan one line past the node's end).
2026-06-21 17:10:34 +05:30
Mihir Kandoi
16e45c41f5 ci(postgres): drop the checker's unit test
Remove erpnext/tests/test_postgres_compat.py (and its pre-commit exclude); a unit
test for the dev-tooling lint helper isn't needed in the app test suite.
2026-06-21 17:03:51 +05:30
Mihir Kandoi
549a24f7b9 ci(postgres): add a static pre-commit check for MySQL-only SQL
The Postgres test job is label-gated, so it does not run on every PR. This adds an
always-on pre-commit hook that statically flags the *mechanical* breaks: MySQL-only
functions (timestamp(date,time), timediff, str_to_date, date_format/add/sub,
group_concat, period_diff, SQL IF()), SHOW INDEX/TABLES/COLUMNS, single-quoted
aliases, UPDATE..JOIN, interpolated/f-string SQL carrying MySQL-isms,
set_value/db_set(<Check>, bool), and MySQL SHOW INDEX result keys.

It deliberately does NOT flag the framework auto-translations (ifnull->coalesce,
backtick/locate/REGEXP, .like()->ILIKE) nor the *semantic* divergences (loose GROUP
BY, case-sensitive ==/IN, NULL ordering, tiebreakers) — those need the test suite,
which remains the backstop. AST + structure-gated regex keep false positives near
zero (docstrings and prose skipped); '# pg-ok' exempts intentional MariaDB-only
branches. Scoped to erpnext/ excluding patches/. Includes a unit test of the checker.
2026-06-21 16:53:53 +05:30
Mihir Kandoi
0beb29321e Merge pull request #56251 from mihir-kandoi/pg-ci-remaining-failures
fix(postgres): resolve remaining Postgres test failures on develop
2026-06-21 16:19:28 +05:30
Mihir Kandoi
f595b3c0eb Merge pull request #56252 from mihir-kandoi/pg-savepoint-guards
fix(postgres): savepoint-guard swallow-and-continue insert paths
2026-06-21 16:16:08 +05:30
Mihir Kandoi
3cd2a36117 test(stock): tolerate timezone slack in test_heatmap_data on Postgres
get_timeline_data uses UnixTimestamp(posting_date); on Postgres that is the date's
midnight epoch in the DB session timezone, which can sit up to a day ahead of the
Python time.time() instant when the app timezone is ahead of UTC. The strict
'<= now' upper bound is therefore flaky on Postgres. Allow a day of slack on the
upper bound; MariaDB's UNIX_TIMESTAMP stays <= now so its pass/fail is unchanged.
2026-06-21 15:59:04 +05:30
Mihir Kandoi
bac4f1de52 fix(postgres): savepoint bank-account creation during company setup
create_bank_account() inserts a bank Account and swallows DuplicateEntryError
('bank account same as a CoA entry'). On Postgres the failed insert aborts the
transaction, so the rest of company setup ran against a poisoned transaction.
Take a savepoint and roll back to it in the handler. No-op on MariaDB.
2026-06-21 15:48:37 +05:30
Mihir Kandoi
0e25a77a62 fix(postgres): savepoint Plaid bank-account creation loop
add_bank_accounts() inserts a Bank Account per Plaid account in a loop. On a
duplicate the bare insert raises UniqueValidationError, which on Postgres aborts
the whole transaction; the handler only msgprint'd and continued, so the next
iteration's insert died with InFailedSqlTransaction. Wrap each iteration in a
savepoint and roll back to it in the handlers (the pattern frappe#40075 prescribes
after dropping the blanket per-insert savepoint). No-op on MariaDB.
2026-06-21 15:48:32 +05:30
Mihir Kandoi
f1a7b14e25 test(perf): Postgres-valid index introspection in test_ensure_indexes
SHOW INDEX is MySQL-only and errored on Postgres. Add a db-aware helper that reads
the leading index column from pg_index on Postgres and keeps SHOW INDEX on
MariaDB; both assert the field is the first column of some index.
2026-06-21 15:38:06 +05:30
Mihir Kandoi
b760b9d935 test(stock): savepoint around expected duplicate barcode save (Postgres)
The deliberate UniqueValidationError when re-adding a barcode aborts the
transaction on Postgres, so the next frappe.get_doc() failed with
InFailedSqlTransaction. Wrap the expected-failure save in a savepoint and roll
back to it. No-op on MariaDB.
2026-06-21 15:29:36 +05:30
Mihir Kandoi
72046d3688 test(stock): savepoint around expected duplicate Bin insert (Postgres)
The deliberate UniqueValidationError from the second Bin insert aborts the
transaction on Postgres, so the following _create_bin() (which takes its own
savepoint) failed with InFailedSqlTransaction. Wrap the expected-failure insert in
a savepoint and roll back to it, mirroring _create_bin's 'preserve transaction in
postgres' pattern. No-op on MariaDB.
2026-06-21 15:29:30 +05:30
Mihir Kandoi
b97a0c9a13 test(accounts): set Check field with int, not bool (Postgres)
set_value(Company, ..., "book_advance_payments_in_separate_party_account", True)
errored on Postgres (smallint column, boolean expression). Use 1; MariaDB unchanged.
2026-06-21 15:29:25 +05:30
Mihir Kandoi
e076a78003 fix(accounts): set Check field 'reconciled' with int, not bool (Postgres)
frappe.db.set_value(..., "reconciled", True) renders SET reconciled=true; the
column is smallint, which Postgres rejects (DatatypeMismatch). MariaDB coerces the
boolean to 1. Pass 1 so both engines store the same value.
2026-06-21 15:29:19 +05:30
Mihir Kandoi
2e5310f8a0 fix(manufacturing): case-sensitive variant BOM lookup on Postgres
_bom_contains_item() lowercased the item name and then reused that lowercased
value as a doc name in frappe.db.get_value("Item", item, "variant_of"). Doc
names are case-sensitive on Postgres, so the lowercased name matched no row,
variant_of came back NULL, and a Work Order for a variant item built from the
template's BOM was wrongly rejected with 'BOM ... does not belong to Item ...'.
Keep the original case for the Item lookup; the comparisons stay case-insensitive.
MariaDB is unchanged (its name lookup was case-insensitive either way).
2026-06-21 15:29:16 +05:30
Mihir Kandoi
07aa0fe6c1 Merge pull request #56250 from mihir-kandoi/pg-56249-review-followup
fix(stock): make get_incoming_value_for_serial_nos a staticmethod
2026-06-21 15:17:59 +05:30
Mihir Kandoi
81a0709dbd fix(stock): make get_incoming_value_for_serial_nos a staticmethod
It never references `self`. The deterministic-serial-value test added in #56249
called it as `get_incoming_value_for_serial_nos(None, sle, serial_nos)` — passing
None for self, which is fragile: a future `self.*` access would fail with an opaque
AttributeError. Declaring it @staticmethod makes the call honest
(`get_incoming_value_for_serial_nos(sle, serial_nos)`) and is backward compatible —
the method has no in-repo callers besides that test, and any `self.`-style call still
binds correctly to a staticmethod.

Addresses Greptile review feedback on #56249.
2026-06-21 14:56:29 +05:30
Mihir Kandoi
ed1261ef8d Merge pull request #56249 from mihir-kandoi/pg-test-helpers-parity
test(postgres): make test-helper SQL Postgres-valid across the suite
2026-06-21 14:26:39 +05:30
Mihir Kandoi
8a5f659681 test(postgres): make test-helper SQL Postgres-valid across the suite
The repo-wide query audit fixed runtime/source queries, but test files carry their
own raw SQL helpers that were never swept and only fail when the suite runs on
Postgres. Port the staging branch's already-green fixes for them:

- timestamp(posting_date, posting_time) (raw + qb Timestamp) -> posting_datetime /
  CombineDatetime (test_stock_ledger_entry, test_stock_balance, test_utils)
- HAVING <select-alias> -> qb .having(<expr>) (test_asset_capitalization, test_purchase_order)
- capital-cased identifiers ("Status", "Name") -> lowercase (test_delivery_note,
  test_purchase_order, test_employee)
- raw GL/SLE select helpers -> frappe.get_all / qb, with order-independent
  comparisons where account ordering is collation-dependent across engines
  (test_purchase_invoice, test_sales_invoice, test_payment_entry, test_asset,
  test_purchase_receipt, test_payment_request, test_repost_accounting_ledger,
  test_journal_entry)

All changes are test-only and behaviour-identical on MariaDB (lowercase column names
resolve the same; posting_datetime == timestamp(posting_date, posting_time); HAVING on
the expression is the same computation). Verified: the heavy modules pass on both
MariaDB and Postgres, and MariaDB output is unchanged.
2026-06-21 14:05:49 +05:30
Mihir Kandoi
362126a627 Merge pull request #56239 from mihir-kandoi/pg-parity-case-insensitive
fix: case-insensitive matching match MariaDB on Postgres
2026-06-21 13:11:21 +05:30
rohitwaghchaure
cc354c4e94 Merge pull request #56235 from mihir-kandoi/pg-remove-dead-get-batches
refactor(stock): remove dead get_batches() in batch.py
2026-06-21 12:46:22 +05:30
Mihir Kandoi
442ba48341 fix(manufacturing): case-insensitive batch_no filter in Cost of Poor Quality report
The report's batch_no filter used an exact `==`, which is case-sensitive on Postgres -- a
differently-cased batch_no missed Job Cards that MariaDB (case-insensitive collation)
matches. Add a dedicated batch_no branch wrapping both sides in Lower() (keeping the exact
match, not a substring like serial_no): MariaDB result is unchanged, Postgres now matches.
2026-06-21 11:59:57 +05:30
Nabin Hait
97acd4b33b Merge pull request #56141 from frappe/refactor/journal-entry-client-script
refactor: simplify Journal Entry client script
2026-06-21 11:54:38 +05:30
Nabin Hait
1de903143a Merge pull request #56150 from nabinhait/refactor-si-intercompany-fixedassets
refactor(sales_invoice): simplify fixed-asset and inter-company validations
2026-06-21 11:48:20 +05:30
Mihir Kandoi
db3f70c0e7 fix(stock): case-insensitive serial-no match in get_stock_ledgers_for_serial_nos
The serial-no filter used serial_batch_entry.serial_no.isin(serial_nos), which is
case-sensitive on Postgres -- a differently-cased serial no missed Serial and Batch
Entry rows that MariaDB (case-insensitive collation) matches (the OR'd regexp branch
only covers the legacy Stock Ledger Entry.serial_no text, empty for bundle-tracked
serials). Lower() both sides: MariaDB result unchanged, Postgres now matches too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:44:06 +05:30
Mihir Kandoi
9389ce6d9a fix(website): case-insensitive Item Variant attribute match on Postgres
get_item_codes_by_attributes compared Item Variant Attribute `attribute`/`attribute_value`
with raw equality/IN, which is case-sensitive on Postgres -- a differently-cased website
filter value missed variants that MariaDB (case-insensitive collation) matches. Lower()
both sides: MariaDB result is unchanged (already case-insensitive), Postgres now matches too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:30:29 +05:30
Mihir Kandoi
c4bbf22c62 Merge pull request #56236 from mihir-kandoi/pg-test-stock-entry-timestamp
test(stock): order get_sle by posting_datetime, not MySQL timestamp()
2026-06-21 11:20:00 +05:30
Mihir Kandoi
1772ccc61a test(stock): order get_sle by posting_datetime, not MySQL timestamp()
test_stock_entry.get_sle ordered by `timestamp(posting_date, posting_time)`, a
MySQL-only two-arg function that errors on Postgres ("function timestamp(date, time)
does not exist"), so every test using get_sle (test_fifo, test_stock_entry_qty, ...)
failed to run on Postgres. Order by the precomputed `posting_datetime` column instead
(identical value on MariaDB, valid on both engines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 11:00:50 +05:30
Mihir Kandoi
bc9503bdbf Merge pull request #56234 from mihir-kandoi/pg-stock-index-tests
test(stock): make Bin/Item index tests Postgres-valid (SHOW INDEX → db-agnostic helpers)
2026-06-21 10:53:48 +05:30
Mihir Kandoi
851e70b0f3 Merge pull request #56233 from mihir-kandoi/pg-timesheet-coalesce
fix(projects): use Coalesce for timesheet portal sales_invoice (not bitwise OR)
2026-06-21 10:51:25 +05:30
Mihir Kandoi
345cbc97e1 refactor(stock): remove dead get_batches() in batch.py
batch.get_batches(item_code, warehouse, ...) was added by #55647 and has no callers
anywhere in erpnext, frappe, or payments (not whitelisted, not referenced from JS/hooks).
It is also obsolete: it joins Stock Ledger Entry on `batch_no`, which the Serial and
Batch Bundle system no longer populates, so it returns nothing even on MariaDB. Its
query was additionally Postgres-invalid (GROUP BY batch_id with ORDER BY expiry_date/
creation -> GroupingError, since batch_id is not the primary key).

Remove the dead function (and its now-unused CurDate/Sum import) rather than fix a query
that nothing can reach. Live batch-quantity lookups go through get_batch_qty() /
get_auto_batch_nos(), which use the bundle model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:36:09 +05:30
Mihir Kandoi
dd2f3d42a5 Merge pull request #56231 from mihir-kandoi/pg-rfq-transaction-list
fix(controllers): fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT)
2026-06-21 10:35:33 +05:30
Mihir Kandoi
a927397eac Merge pull request #56230 from mihir-kandoi/pg-lost-quotations-intdiv
fix(selling): keep Lost Quotations % fractional on Postgres (integer division)
2026-06-21 10:29:28 +05:30
Mihir Kandoi
5a0e7f57a3 fix(projects): use Coalesce for timesheet portal sales_invoice (not bitwise OR)
get_timesheets_list selected `timesheet.sales_invoice | detail.sales_invoice`,
intending COALESCE (pick the parent timesheet's invoice, else the detail's) -- the
original raw SQL was COALESCE(ts.sales_invoice, tsd.sales_invoice). pypika's `|` is a
bitwise OR, not a coalesce:

- Postgres: `varchar | varchar` -> "operator does not exist" (hard error).
- MariaDB: bitwise OR coerces the operands to integers; with a NULL detail invoice the
  result is NULL, so the portal showed no invoice even when the timesheet was billed.

Replace with Coalesce(table.sales_invoice, child_table.sales_invoice).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:27:14 +05:30
Mihir Kandoi
9fe2202f39 test(stock): use db-agnostic index introspection in Item index test
test_index_creation used `frappe.db.sql("show index from tabItem")` and the MySQL-only
result key "Column_name". "SHOW INDEX" errors on Postgres, so the test could not run
there. Use the db-agnostic frappe.db.get_column_index("tabItem", column) (checking both
unique and non-unique single-column indexes) instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:25:32 +05:30
Mihir Kandoi
f66ef869fc test(stock): use db-agnostic index introspection in Bin index test
test_index_exists used `frappe.db.sql("show index from tabBin ...")`. "SHOW INDEX"
is MySQL-only syntax and errors on Postgres (syntax error at "from"), so the test
could not run there. Use the db-agnostic frappe.db.has_index("tabBin",
"unique_item_warehouse") instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:25:30 +05:30
Mihir Kandoi
42fffc4857 Merge pull request #56227 from mihir-kandoi/pg-general-ledger-alias
fix(accounts): General Ledger remarks alias Postgres-valid
2026-06-21 10:19:25 +05:30
Mihir Kandoi
de70333f8f Merge pull request #56229 from mihir-kandoi/pg-lcv-having
fix(stock): make Landed Cost Voucher vendor-invoice query Postgres-valid (HAVING→WHERE)
2026-06-21 10:18:54 +05:30
Mihir Kandoi
404d4413b5 Merge pull request #56228 from nishkagosalia/st-71960
fix: disarding stock entry fix
2026-06-21 10:16:42 +05:30
Mihir Kandoi
a7d9078bf4 fix(controllers): fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT)
rfq_transaction_list had two defects introduced when it was converted to the query
builder:

1. `party.supplier == party[0]` compared supplier to a column literally named "0"
   (a stray index on the DocType, not the intended `parties[0]` value). This renders
   as `supplier = \`0\`` / `supplier = "0"` and errors on BOTH engines
   (MariaDB: Unknown column '0'; Postgres: column "0" does not exist), so the
   supplier portal RFQ list was completely broken.
2. SELECT DISTINCT ordered by `creation`, which is not in the select list. Postgres
   rejects this ("for SELECT DISTINCT, ORDER BY expressions must appear in select list").

Compare against `parties[0]` and add `creation` to the select list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:14:42 +05:30
Mihir Kandoi
0e88f59196 Merge pull request #56226 from mihir-kandoi/pg-item-variant-update-join
fix(controllers): make update_variant_attribute_values Postgres-valid (drop UPDATE..JOIN)
2026-06-21 10:13:50 +05:30
Mihir Kandoi
ed6a682779 fix(selling): keep Lost Quotations % fractional on Postgres (integer division)
The "Lost Quotations %" column computed Count(distinct) / total_quotations * 100,
where both operands are integers. Postgres does integer division on int/int, so any
group that is a strict minority of the total truncated to 0 (e.g. 1 of 4 -> 0%);
MariaDB always divides as decimal. Multiply by 100.0 before dividing so the division
is done in floating point on both engines.

The "Lost Value %" column already divided Sum(Currency)/Sum(Currency) (numeric), so it
was unaffected; left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:09:45 +05:30
Mihir Kandoi
016097da2e Merge pull request #56225 from mihir-kandoi/pg-projects-pg-validity
fix(projects): Project timeline GROUP BY Postgres-valid
2026-06-21 10:04:08 +05:30
Mihir Kandoi
acbf453def fix(accounts): make General Ledger remarks alias Postgres-valid
When Accounts Settings -> general_ledger_remarks_length is set, the GL report
adds `substr(remarks, 1, n) as 'remarks'` to its raw SQL. Postgres treats a
single-quoted column alias as a string literal and raises a syntax error, so
the General Ledger report is broken on Postgres whenever that setting is on.

Use a bare alias (`as remarks`). substr() itself is portable.

Adds a test that sets general_ledger_remarks_length and runs the report,
asserting it executes (and returns rows) on both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:58:06 +05:30
Mihir Kandoi
7f8fa5b5a2 fix(stock): make Landed Cost Voucher vendor-invoice query Postgres-valid
get_vendor_invoice_query filtered unclaimed invoices with
.having(unclaimed_amount > 0), but the query has no GROUP BY/aggregate and
unclaimed_amount is a SELECT alias. Postgres rejects HAVING on a SELECT alias
(and HAVING without GROUP BY on a non-aggregated column); MariaDB allowed it.
Move the threshold into WHERE on the underlying expression.

Behaviour is identical on MariaDB (same rows); fixes a hard error on Postgres.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:55:51 +05:30
Mihir Kandoi
3d9b704730 Merge pull request #56224 from mihir-kandoi/pg-production-planning-report
fix(manufacturing): Production Planning report GROUP BY Postgres-valid
2026-06-21 09:54:49 +05:30
nishkagosalia
debe1855c6 fix: disarding stock entry fix 2026-06-21 09:54:20 +05:30
Mihir Kandoi
598864f0be fix(controllers): make update_variant_attribute_values Postgres-valid (drop UPDATE..JOIN)
update_variant_attribute_values (propagates renamed Item Attribute Values to
variant items) used a qb UPDATE ... JOIN. Postgres has no UPDATE..JOIN syntax,
so renaming an Item Attribute Value errored on Postgres.

Rewrite as a correlated UPDATE that restricts to variant items via a subquery
on the parent (item_variant_table.parent.isin(variant Items)) instead of
joining the Item table. MariaDB behaviour is unchanged.

Covered by the existing test_item.test_rename_attribute_value_updates_variants
and test_swapped_attribute_value_renames_update_variants, which errored on
Postgres before and now pass on both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:46:43 +05:30
Mihir Kandoi
a483177690 fix(projects): make Project timeline GROUP BY Postgres-valid
get_timeline_data grouped Timesheet Detail by Date(from_time) but selected
UnixTimestamp(from_time) (the full timestamp, ungrouped). MariaDB
arbitrary-picks a row's timestamp; Postgres rejects it ("must appear in the
GROUP BY clause"), so the Project timeline (calendar heatmap) is broken on PG.

Select UnixTimestamp(Date(from_time)) — the day's epoch — which is the
timeline key and matches the GROUP BY. CurDate() - Interval(years=1) is
portable and kept as-is.

Adds a test (no coverage existed) that records a timesheet against a project
and asserts get_timeline_data returns day-bucketed counts, on both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:44:48 +05:30
Mihir Kandoi
469d58d1f4 fix(manufacturing): make Production Planning report GROUP BY Postgres-valid
get_purchase_details grouped Purchase Order Item by (item_code, warehouse)
while selecting `qty` ungrouped/unaggregated. MariaDB arbitrary-picks one
row's qty; Postgres rejects the query ("must appear in the GROUP BY clause"),
so the report is broken on Postgres.

Sum the qty per item+warehouse ({"SUM": "qty"}). The column is the "Arrival
Qty" (quantity on order arriving) display figure; summing the open PO lines is
the meaningful planning number, and is deterministic vs MariaDB's arbitrary
single-line pick (which only differed when an item+warehouse had multiple open
PO lines).

Adds a test (no test file existed) that creates a Work Order plus two PO lines
for a BOM raw material and asserts the report runs and reports arrival_qty = 7
(3 + 4), on both engines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 09:35:56 +05:30
Mihir Kandoi
f8359c91b2 Merge pull request #56221 from mihir-kandoi/pg-stock-entry
fix(stock): convert get_used_alternative_items to query builder (Postgres)
2026-06-21 08:09:27 +05:30