make_default_records inserted Scorecard Variable/Standing rows in a loop and swallowed DuplicateEntryError (frappe.NameError). On Postgres the failed insert poisons the txn so the next iteration's insert raises InFailedSqlTransaction. insert(ignore_if_duplicate=True) emits ON CONFLICT DO NOTHING, never poisoning the txn. No-op on MariaDB.
Same shape: the rows loop submits a Repost Item Valuation; a caught DuplicateEntryError poisons the Postgres txn so the next iteration's submit raises InFailedSqlTransaction. Savepoint + rollback(save_point=) before continue. No-op on MariaDB.
The item/warehouse loop submits a Repost Item Valuation; a DuplicateEntryError poisons the Postgres transaction, so the next iteration's .submit() raises InFailedSqlTransaction. MariaDB continues. Savepoint per iteration + rollback(save_point=) on the caught duplicate (mirrors repost_item_valuation:782). No-op on MariaDB.
get_exchange_rate orders Currency Exchange by 'date desc' LIMIT 1 with no unique tiebreaker. Currency Exchange autoname {date}-{from}-{to}-{purpose} allows multiple same-date rows (different purpose) for one currency pair; on the no-purpose-filter path all match, so MariaDB and Postgres can return a different exchange_rate for the same inputs. Add 'name desc' so both engines pick the same row. MariaDB row count unchanged.
get_rate_for_return builds Abs(stock_value_difference / actual_qty) for Sales/Delivery returns and passes it to get_value with no actual_qty filter. A matched Stock Ledger Entry with actual_qty=0 (a zero-qty repost / serial-batch row) makes Postgres raise 'division by zero' while MariaDB returns NULL. Wrap the divisor in NullIf(actual_qty, 0) so both engines return NULL. MariaDB output unchanged. Sibling of the already-fixed /actual_qty sites in stock_ledger.py and incorrect_serial_no_valuation.py.
create_purchase_invoice caught its own failure and then ran frappe.db.set_value + log_error in the SAME transaction. On Postgres a failed insert/save aborts the whole transaction, so the error-marking died with InFailedSqlTransaction and the failure cascaded through prepare_data_for_import's per-file loop, killing the entire import; MariaDB recovers per-statement and continues.
Let create_purchase_invoice raise, and wrap each call in prepare_data_for_import in frappe.db.savepoint + rollback(save_point=...). On failure the savepoint rollback un-poisons the transaction, the error is logged, and the per-file status is set to Error and committed (self.db_set(commit=True), matching the existing process_file_data status commit) so an interrupted import durably reflects Error instead of staying at the already-committed Processing File Data; the loop then continues to the next file. The savepoint is taken after create_supplier/create_address so those are preserved exactly as before.
Behaviour change (MariaDB): a failed invoice's partially-created draft Purchase Invoice is now rolled back on BOTH engines instead of being left as an orphan draft on MariaDB. Deliberate and more correct - a failed import should not leave a partial invoice; release-note worthy.
Greptile flagged that `gh release download` with `github.token` could be
rejected for fork pull requests (token scoped to the fork, asset in
frappe/erpnext). The release is public and published, so the asset is
downloadable anonymously from objects.githubusercontent.com — drop the token
and curl the public URL directly. Removes the cross-repo token dependency and
keeps fork PRs working. Cloudflare is still bypassed since GitHub serves the
asset, not frappe.io.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Stop DB and stage datadir" step swallowed a failed `pg_ctl -m fast -w
stop` with `|| true`, then moved and tarred the PGDATA regardless. A stop
that times out or errors would bake a still-running, crash-inconsistent
cluster into the artifact every test shard consumes — and with
full_page_writes off, crash recovery can't repair torn pages. Drop the
`|| true` so a failed stop fails the job, mirroring the MariaDB sister's
"don't bake a dirty datadir" guard.
Also drop the redundant `ALTER SYSTEM SET fsync/synchronous_commit/
full_page_writes = off` block from install.sh. Its comment claimed the
postgres workflow "runs a service-container DB and never calls start-db.sh",
but it does call start-db.sh, which already applies those flags via `-o` on
every postgres start (setup job and each shard). The block was a no-op and
its justification was factually wrong.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Patch Test job intermittently failed on the "Download erpnext v14 backup"
step with HTTP 403 Forbidden: frappe.io sits behind Cloudflare, and wget's
default User-Agent gets flagged by bot protection on cache misses. This caused
random failures across PRs that only a re-run would clear.
Pull the fixed baseline from the v14-baseline GitHub release using the built-in
token instead. Release assets are served from GitHub's CDN and authenticated
from the runner, so no rate-limit roulette.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
create_routing inserts a Routing and, on DuplicateEntryError, re-fetches and updates. On Postgres the failed insert aborts the transaction so the get_doc/save in the except raises InFailedSqlTransaction; MariaDB recovers. Savepoint + rollback before the fallback path.
make_reposting_for_accounting_ledgers submits a new Repost Item Valuation per voucher in a loop under except Exception. On Postgres a failed submit aborts the transaction so the next iteration's DB work dies with InFailedSqlTransaction; MariaDB continues. Savepoint per iteration, roll back on failure. No-op on MariaDB.
Same submit()/add_comment-in-except shape as the PO->SCO mapper: on Postgres a failed submit aborts the transaction so the follow-on Comment insert raises InFailedSqlTransaction; MariaDB continues. Savepoint + rollback before add_comment. No-op on MariaDB.
target_doc.submit() is wrapped in except Exception whose handler calls add_comment (a Comment insert). On Postgres a failed submit poisons the transaction so the add_comment insert raises InFailedSqlTransaction; MariaDB logs the comment. Savepoint + rollback before add_comment. No-op on MariaDB.
Purchase Receipt stock and asset GL lines used the item row's cost center
but always fell back to the document-level project, unlike Purchase Invoice
which uses the item-level project. add_gl_entry accepted a project argument
but never wrote it to the GL dict, so the inward, Stock Received But Not
Billed, landed cost, divisional loss, sub-contracting and exchange rate
lines dropped the row's project.
Write project into the GL dict and pass project=item.project on the entries
that were missing it, so project behaves like cost center and matches
Purchase Invoice.
Ticket: 72523
get_last_sales_amt ordered by the sales date DESC only; same-date documents made the reported Last Order Amount engine-dependent. Add name DESC tiebreaker.
get_last_purchase_rate ordered by posting_date DESC only; same-date Purchase Invoices yielded an undefined last_purchase_rate that diverged between MariaDB and Postgres. Add purchase_invoice.name DESC tiebreaker.
get_latest_location_and_custodian ordered by transaction_date DESC only; equal-dated movements left the current location/custodian engine-dependent. Add asm.name DESC tiebreaker so both engines pick the same movement.
calculate_exchange_rate_using_last_gle ordered the latest-GLE lookups by posting_date DESC only. With multiple GL Entries on the latest posting_date the picked row was undefined, so MariaDB and Postgres could choose different vouchers and return a different last_exchange_rate (and revaluation gain/loss). Add gl.name DESC as a tiebreaker so both engines pick the same row; MariaDB row count unchanged.
The lead/address cleanup pre-escaped each address name before passing the
list into a query-builder .isin() filter, which escapes again. The
double-escaping produced `name IN ('''Addr''')`, matching nothing, so
Lead-linked Addresses were never deleted on either MariaDB or Postgres.
Pass the raw list straight into .isin() so the builder escapes once.