Commit Graph

58986 Commits

Author SHA1 Message Date
Mihir Kandoi
b8bbcda047 Merge pull request #56265 from mihir-kandoi/pg-trends-rowcount
fix(controllers): keep Sales/Purchase Trends one row per based-on key (MariaDB parity)
2026-06-22 01:25:01 +05:30
Mihir Kandoi
8f1c703871 fix(controllers): keep Supplier trends one row per supplier + add regression tests
The earlier parity fix aggregated the non-key descriptive columns for the Item
and Customer based-on paths but left Supplier grouping by all three selected
columns (supplier, supplier_name, supplier_group). supplier_name is a stored
per-transaction field, so historical purchase docs holding a divergent value for
the same supplier would split one supplier into multiple rows — diverging from
the original MariaDB output, which grouped by t1.supplier only.

Aggregate supplier_name with Max() and keep only supplier + the FD master column
supplier_group in GROUP BY, restoring one row per supplier on both engines.

Add regression tests for the Supplier (purchase) and Customer (sales) paths that
assert a single row per key even when stored descriptive fields diverge; both
fail on the pre-fix multi-column GROUP BY and pass after the fix, on MariaDB and
Postgres.
2026-06-22 00:58:56 +05:30
Mihir Kandoi
7f6004bfd9 Merge pull request #56128 from Henil666/fix/mt940-label-typo
fix: correct typo in Bank Statement Import MT940 label
2026-06-22 00:34:07 +05:30
Mihir Kandoi
dbd1388b40 fix(controllers): keep Sales/Purchase Trends one row per based-on key (MariaDB parity)
#56192 made the trends queries Postgres-strict-GROUP-BY-valid by widening based_on_group_by
to include the selected descriptive columns. For Item it added t2.item_name, for Customer
t1.territory (and customer_name) — but item_name is an editable per-line field and territory an
editable per-document field, not functionally dependent on the item_code/customer key. On MariaDB
(ONLY_FULL_GROUP_BY off) this SPLITS the single row per key into one row per distinct
(key, item_name)/(key, territory), so a customer transacting across two territories (or an item
with an edited item_name) now shows duplicate rows with fractured per-period subtotals.

Group by the KEY only and aggregate the non-key descriptive columns with Max(): one row per
based-on key (identical to the pre-#56192 MariaDB output) and still Postgres-valid. Supplier
columns are master-joined / fetch-locked (functionally dependent) so they stay unchanged.
2026-06-21 22:38:30 +05:30
Mihir Kandoi
6395d968ad Merge pull request #56260 from mihir-kandoi/pg-wo-stock-report-rowcount
fix(manufacturing): keep Work Order Stock report one row per item (MariaDB parity)
2026-06-21 22:21:21 +05:30
Sudharsanan Ashok
130c2594e1 fix(stock): update voucher valuaion rate in sle (#55960) 2026-06-21 21:50:19 +05:30
Mihir Kandoi
2221f2c6f1 fix(manufacturing): keep Work Order Stock report one row per item (MariaDB parity)
#56196's Postgres GROUP BY fix added bom.quantity, bom_item.stock_qty and
bin.actual_qty to the GROUP BY. bom.quantity and bin.actual_qty are pinned to a
single value by the WHERE/join, but a BOM may list the same item_code on multiple
lines with different stock_qty (validate_materials does not dedupe), so grouping by
stock_qty SPLITS the row and changes req_items/instock on MariaDB for such BOMs.

Aggregate build_qty with Max() and group by item_code only: one row per item_code
(identical to the pre-#56196 single-line result; deterministic for duplicate lines),
and Postgres-valid. MariaDB output is unchanged for the common single-line case and
its row count is restored for the duplicate-line case.
2026-06-21 19:58:49 +05:30
MochaMind
0ff0343588 chore: update POT file (#56253) 2026-06-21 14:26:56 +02:00
Mihir Kandoi
d9d94da9f5 Merge pull request #56256 from mihir-kandoi/pg-precommit-lint
ci(postgres): static pre-commit check for MySQL-only SQL
2026-06-21 17:29:15 +05:30
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
0e0575f27b Merge pull request #56243 from frappe/pg-ci-required
ci: upgrade the PostgreSQL server test workflow (opt-in via 'postgres' label)
2026-06-21 17:00:49 +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
f95e91323e ci(postgres): install payments app on the test site
The Postgres CI site only listed erpnext in install_apps, so the payments app
(fetched and built by install.sh via 'bench get-app payments') was never
installed on the site — leaving 'tabPayment Gateway' absent. test_payment_request
(and other payment-gateway-dependent tests) then errored on Postgres with
'relation "tabPayment Gateway" does not exist', while MariaDB passed because its
site_config already lists ["payments", "erpnext"]. Match that ordering for parity.
2026-06-21 16:19:52 +05:30
Mihir Kandoi
a46a6bf921 ci: speed up Postgres CI by disabling DB durability for the disposable test DB
Postgres fsyncs on every commit by default, which dominates a commit-heavy test suite.
Turn off synchronous_commit/fsync/full_page_writes on the throwaway CI database (reload-
time settings, no restart). MariaDB CI is unaffected (DB != postgres).
2026-06-21 16:19:52 +05:30
Mihir Kandoi
c820591089 ci: name the Postgres job distinctly so it is not a required check
The MariaDB job is named 'Python Unit Tests', and 'Python Unit Tests (1..4)' are the
required status checks on develop. Naming the Postgres matrix job the same made its
checks report under those required contexts, effectively gating every (labelled) PR on
Postgres. Rename it to 'Postgres Unit Tests' so its contexts are distinct and the
workflow stays non-required until we deliberately add it to branch protection.
2026-06-21 16:19:52 +05:30
Mihir Kandoi
57d0cebfb8 ci: make Postgres coverage upload glob explicit (codecov files) 2026-06-21 16:19:52 +05:30
Mihir Kandoi
d7eb54b153 ci: upgrade the PostgreSQL server test workflow (kept opt-in via 'postgres' label)
Bring the Server (Postgres) workflow in line with Server (MariaDB) internals while
keeping it opt-in for now: pull_request runs still require the 'postgres' label, but the
job now uses the full 4-container matrix (was 1), adds the nightly schedule /
workflow_dispatch / repository_dispatch triggers (which always run), and uploads
coverage. Builds ERPNext against frappe `develop` (PostgreSQL query-builder/ORM support
is merged there), so no fork override is needed.

The ERPNext server suite now passes on PostgreSQL and MariaDB from a single codebase;
flipping this to run on every PR / become a required check is a later, separate step.
2026-06-21 16:19:52 +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