Compare commits

..

135 Commits

Author SHA1 Message Date
MochaMind
060cd9f320 fix: Bosnian translations 2026-06-18 23:55:57 +05:30
MochaMind
eb6530208b fix: Croatian translations 2026-06-18 23:55:52 +05:30
MochaMind
526f91f6b5 fix: Bosnian translations 2026-06-17 23:22:48 +05:30
MochaMind
4465ebaeb5 fix: Persian translations 2026-06-17 23:22:44 +05:30
Mihir Kandoi
65d9f78409 Merge pull request #56057 from mihir-kandoi/pg-accounts-payments 2026-06-17 18:59:43 +05:30
Mihir Kandoi
acda04a4bd refactor(postgres): port Payment Request doctype queries to the query builder
3-way merged onto develop (preserving the get_party_bank_account import move).
get_subscription_details passes order_by="" so get_all does not inject the
doctype default sort the raw query never had.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:38:02 +05:30
Mihir Kandoi
d1e167815f refactor(postgres): port Payment Entry doctype queries to the query builder
3-way merged onto develop, preserving develop's set_exchange_rate(ref_doc=doc) change.
One portable raw query is intentionally kept (as on the source branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:38:01 +05:30
Mihir Kandoi
588dfac4cd refactor(postgres): port Mode of Payment doctype queries to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:38:00 +05:30
Mihir Kandoi
ac26c01e52 refactor(postgres): port Cashier Closing doctype queries to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:38:00 +05:30
Mihir Kandoi
c0d2bd7bce refactor(postgres): port Invoice Discounting doctype queries to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:37:59 +05:30
Mihir Kandoi
4d03e915f7 refactor(postgres): port Bank Transaction doctype queries to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:37:58 +05:30
Mihir Kandoi
09beed9cc3 refactor(postgres): port Payment Order doctype queries to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:37:57 +05:30
Mihir Kandoi
0737a4cfbb Merge pull request #56051 from mihir-kandoi/pg-accounts-statements
refactor(postgres): port Accounts statement & ledger report queries to the query builder
2026-06-17 17:55:47 +05:30
Mihir Kandoi
1332ad7583 refactor(postgres): port Share Ledger report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:36:01 +05:30
Mihir Kandoi
058be399c3 refactor(postgres): port Asset Depreciation Ledger report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:36:01 +05:30
Mihir Kandoi
e482c846c8 refactor(postgres): port General Ledger report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:36:00 +05:30
Mihir Kandoi
6a60f072a8 refactor(postgres): port Dimension-wise Account Balance report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:59 +05:30
Mihir Kandoi
18c1f0f04d refactor(postgres): port Trial Balance report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:58 +05:30
Mihir Kandoi
217c107549 refactor(postgres): port Consolidated Trial Balance report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:57 +05:30
Mihir Kandoi
44ca5878b8 refactor(postgres): port Consolidated Financial Statement report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:56 +05:30
Mihir Kandoi
66e82c56b1 refactor(postgres): port Gross Profit report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:55 +05:30
Mihir Kandoi
65c0d35f2e refactor(postgres): port Budget Variance report query to the query builder
The raw get_fiscal_years query had no ORDER BY (de-facto oldest-first); the
get_all port adds explicit order_by="name asc" so the Fiscal Year doctype
default (name DESC) does not reverse the report column order / cumulative values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:55 +05:30
Mihir Kandoi
e9eca10927 refactor(postgres): port Cash Flow report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:54 +05:30
Mihir Kandoi
6849d292f8 refactor(postgres): port financial statements helper queries to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:35:52 +05:30
Mihir Kandoi
261b7fe7aa Merge pull request #56047 from mihir-kandoi/pg-accounts-registers
refactor(postgres): port Accounts register report queries to the query builder
2026-06-17 17:09:03 +05:30
Mihir Kandoi
30568d36d0 refactor(postgres): port Accounts Receivable report query to the query builder
Most queries are straight raw-SQL -> query-builder ports. One rider:
get_future_payments_from_journal_entry sums future amounts with no GROUP BY,
so its non-aggregated identity columns (invoice_no/party/future_date/future_ref)
are wrapped in Max() to satisfy postgres strict GROUP BY. The summed amount is
unchanged; the attributed invoice/party label stays within MariaDB's existing
arbitrary-row indeterminacy for that already-aggregated single-row query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:49:48 +05:30
Mihir Kandoi
8527e78820 refactor(postgres): port POS Register report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:46:32 +05:30
Mihir Kandoi
ae4a5e82b0 refactor(postgres): port Item-wise Purchase Register report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:46:31 +05:30
Mihir Kandoi
196fce9792 refactor(postgres): port Purchase Register report query to the query builder
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:46:30 +05:30
Mihir Kandoi
449004d29a refactor(postgres): port Sales Register report query to the query builder
Also sort the distinct income / unrealized P&L account lists in python:
frame drops ORDER BY for distinct queries on postgres, so the generated
account-column order must be pinned in python to stay deterministic on
both backends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:46:29 +05:30
Mihir Kandoi
bae3668bd0 Merge pull request #56045 from mihir-kandoi/pg-final-fixes
fix(postgres): final fix-class changes (asset GROUP BY + accounts-controller boolean)
2026-06-17 16:32:13 +05:30
Mihir Kandoi
8ce0e5386a Merge pull request #56042 from mihir-kandoi/codex/fix-stock-ageing-reco-ageing
fix: preserve stock ageing on non-serial reconciliation
2026-06-17 16:15:26 +05:30
Mihir Kandoi
4ba042c7c7 fix(postgres): compare Check fields with == 1 in accounts controller CASE/condition
disable_rounded_total and is_pos are smallint Check fields; postgres rejects
using them as bare boolean conditions in CASE WHEN / bitwise-AND, so compare
explicitly against 1. No-op on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:09:00 +05:30
Mihir Kandoi
59ad76c21e fix(postgres): satisfy strict GROUP BY in asset depreciation query
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:08:24 +05:30
Mihir Kandoi
1efe0be379 Merge pull request #56041 from mihir-kandoi/pg-engine-semantics
fix(postgres): assorted engine-semantics fixes (casing, null, boolean)
2026-06-17 16:07:07 +05:30
Mihir Kandoi
6bb7fa6d68 fix: preserve stock ageing on non-serial reconciliation 2026-06-17 15:55:22 +05:30
Mihir Kandoi
ef5feb613a fix(postgres): default empty balance_serial_no to "" in Available Serial No report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:42:04 +05:30
Mihir Kandoi
2fa9d7cee6 fix(postgres): match stored "Exchange Gain Or Loss" voucher_type casing
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:42:02 +05:30
Mihir Kandoi
431dc208b3 Merge pull request #56040 from mihir-kandoi/pg-fn-swap
fix(postgres): replace MySQL-only SQL functions with portable equivalents
2026-06-17 15:40:43 +05:30
Mihir Kandoi
0b795a628f fix(postgres): use portable GroupConcat in Lost Opportunity report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:19:16 +05:30
Mihir Kandoi
595a4c8517 fix(postgres): replace MySQL IF() with Case in Cheques and Deposits report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:50 +05:30
Mihir Kandoi
9ec224c3fd fix(postgres): use CombineDatetime for work order stock-entry timestamps
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:49 +05:30
Mihir Kandoi
68415c341b fix(postgres): use portable DateDiff in supplier scorecard variables
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:48 +05:30
Mihir Kandoi
a43df3278f fix(postgres): use portable DateDiff/CurDate in Inactive Sales Items report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:18:47 +05:30
Mihir Kandoi
4d06b01abf Merge pull request #56037 from mihir-kandoi/pg-groupby-doctypes
fix(postgres): satisfy strict GROUP BY in doctype & core queries
2026-06-17 15:00:55 +05:30
ruthra kumar
a04d54b2fb Merge pull request #56034 from ruthra-kumar/remove_custom_utility_for_company_creation
refactor(test): remove custom utility for company creation
2026-06-17 14:13:22 +05:30
rohitwaghchaure
0476f318e4 Merge pull request #55807 from aerele/fix/support-#70713
fix(stock): allow partial raw material pick/transfer from work order
2026-06-17 14:10:29 +05:30
Mihir Kandoi
7fbfa35f95 fix(postgres): satisfy strict GROUP BY in serial and batch bundle
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:56 +05:30
Mihir Kandoi
bbf506e848 fix(postgres): satisfy strict GROUP BY in work order required items
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:55 +05:30
Mihir Kandoi
725fd8ca97 fix(postgres): satisfy strict GROUP BY in production plan sub-assembly queries
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:54 +05:30
Mihir Kandoi
85191d1cac fix(postgres): satisfy strict GROUP BY in production plan bom explosion
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:53 +05:30
Mihir Kandoi
93021a9d45 fix(postgres): satisfy strict GROUP BY in accounts advances service
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:52 +05:30
Mihir Kandoi
0afc6dd363 fix(postgres): satisfy strict GROUP BY in unreconcile payment
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:51 +05:30
Mihir Kandoi
6dc2e43dd6 fix(postgres): satisfy strict GROUP BY in process period closing voucher
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:50 +05:30
Mihir Kandoi
d34e4b8783 fix(postgres): satisfy strict GROUP BY in exchange rate revaluation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:49 +05:30
Mihir Kandoi
501acd0414 fix(postgres): satisfy strict GROUP BY in bank reconciliation tool
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:05:48 +05:30
Mihir Kandoi
113943f851 Merge pull request #56036 from mihir-kandoi/pg-groupby-reports
fix(postgres): satisfy strict GROUP BY in 12 reports
2026-06-17 13:39:35 +05:30
Mihir Kandoi
c124e90a89 fix(postgres): satisfy strict GROUP BY in Total Stock Summary report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:08 +05:30
Mihir Kandoi
9096b4a9df fix(postgres): satisfy strict GROUP BY in Stock And Account Value Comparison report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:07 +05:30
Mihir Kandoi
e69eaa5102 fix(postgres): satisfy strict GROUP BY in Product Bundle Balance report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:06 +05:30
Mihir Kandoi
e77b27ae99 fix(postgres): satisfy strict GROUP BY in Batch Wise Balance History report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:04 +05:30
Mihir Kandoi
60235f4b2b fix(postgres): satisfy strict GROUP BY in Payment Terms Status For Sales Order report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:03 +05:30
Mihir Kandoi
463103ebf1 fix(postgres): satisfy strict GROUP BY in Inactive Customers report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:02 +05:30
Mihir Kandoi
a3ec98a57c fix(postgres): satisfy strict GROUP BY in Process Loss Report report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:02 +05:30
Mihir Kandoi
9ab8803fed fix(postgres): satisfy strict GROUP BY in Bom Stock Analysis report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:01 +05:30
Mihir Kandoi
8a566e6ba5 fix(postgres): satisfy strict GROUP BY in Sales Pipeline Analytics report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:19:00 +05:30
Mihir Kandoi
812a06cf44 fix(postgres): satisfy strict GROUP BY in Requested Items To Order And Receive report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:18:59 +05:30
Mihir Kandoi
23778c3875 fix(postgres): satisfy strict GROUP BY in Voucher Wise Balance report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:18:57 +05:30
Mihir Kandoi
24a66d10e7 fix(postgres): satisfy strict GROUP BY in Deferred Revenue And Expense report
Wrap the non-aggregated, functionally-dependent column(s) in Max()/Min() (or add
them to GROUP BY) so the report's grouped query is valid under PostgreSQL's strict
GROUP BY. No behaviour change on MariaDB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:18:56 +05:30
Mihir Kandoi
3faaa87645 Merge pull request #56033 from mihir-kandoi/pg-zero-date
fix(postgres): db-aware zero-date (0000-00-00) item end-of-life checks
2026-06-17 12:57:22 +05:30
ruthra kumar
afeaba5142 refactor(test): update assertion for new test records 2026-06-17 12:39:29 +05:30
Mihir Kandoi
1a016cbcd6 refactor(postgres): consistent zero-date pattern (address review)
Use the same explicit db-aware conditional-add as work_order/mapper.py for the
end_of_life check (shared _item_is_alive helper in reorder_item.py; inline in
stock_projected_qty.py) instead of the inline ternary that became == None on
postgres. Identical SQL, no behaviour change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:35:29 +05:30
Mihir Kandoi
7532ec9f9a fix(postgres): db-aware zero-date (0000-00-00) item end-of-life checks
The "item is not discontinued" checks treat an item as alive when its
`end_of_life` is unset, in the future, or the MariaDB zero-date `'0000-00-00'`.
`'0000-00-00'` is an invalid date literal on PostgreSQL (it errors), and a
"not set" end_of_life is `NULL` there anyway — already covered by the existing
`end_of_life IS NULL` term. So the zero-date comparison is applied on MariaDB
only; PostgreSQL keeps the `IS NULL` / future-date terms. No behaviour change on
MariaDB.

Sites: work order item-master selection (`mapper.py`), reorder-level item
selection (`reorder_item.py`), and the Stock Projected Qty report.

Part of the staged MariaDB<->PostgreSQL parity rollout (one problem class).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:23:15 +05:30
Mihir Kandoi
48e66d04e6 Merge pull request #56025 from mihir-kandoi/pg-row-locking-cursor
fix(postgres): db-aware row-locking, savepoints & cursors
2026-06-17 12:19:55 +05:30
ruthra kumar
59a69fc497 refactor(test): broken test case in accounts controller 2026-06-17 10:22:00 +05:30
Mihir Kandoi
a899183087 fix(postgres): keep row locks via lock-then-read (address review)
Acquire the same row locks on postgres that MariaDB takes, via a separate plain
SELECT <pk> ... FOR UPDATE before each grouped/aggregate read (FOR UPDATE is
invalid with GROUP BY on postgres). Applied to all 6 aggregate lock sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:50:33 +05:30
ruthra kumar
3d109571ee refactor(test): remove even more dead code 2026-06-17 08:31:38 +05:30
Mihir Kandoi
d079677500 fix(postgres): db-aware row-locking, savepoints & cursors
PostgreSQL rejects `SELECT ... FOR UPDATE` when combined with `GROUP BY`,
aggregates or `DISTINCT`, has no concept of MySQL's locking semantics for those
shapes, and its server-side (unbuffered) cursors can't run nested queries
mid-iteration. This makes the row-locking / cursor paths db-aware so they keep
the exact MariaDB behaviour there and use the valid PostgreSQL form on Postgres.

One problem class, applied across the codebase:

- **`FOR UPDATE` + GROUP BY/aggregate** — keep `.for_update()` on MariaDB; on
  Postgres acquire the lock in a separate plain `SELECT ... FOR UPDATE` pass (or
  skip where the grouped read isn't a lock point). Deprecated serial/batch,
  serial-batch-bundle, pick list, stock reservation entry.
- **Unbuffered/server-side cursor** — Stock Ageing streamed via an unbuffered
  cursor and then ran nested queries; on Postgres that invalidates the cursor, so
  process the buffered result directly there.
- **Transaction savepoints** — Opening Invoice Creation Tool rolled back the whole
  transaction per failed invoice (which on Postgres also discards sibling rows and
  earlier error logs); scope each invoice to a savepoint instead.

No behaviour change on MariaDB (the locking/cursor path is unchanged there).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 08:20:23 +05:30
ruthra kumar
6e62750c2f refactor(tests): reuse persistent master data instead of creating company per test
Replace per-test company creation in setUp() with persistent master data
from BootStrapTestData. Add Test PCV Company to test_records.json so it
becomes a persistent fixture rather than a throwaway created per test run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:12:54 +05:30
Diptanil Saha
faadc1620b fix(company): replaced "this company" with company name on delete transactions dialog (#56021) 2026-06-17 02:11:08 +05:30
MochaMind
f249d57b30 fix: sync translations from crowdin (#56018) 2026-06-17 00:54:16 +05:30
Mihir Kandoi
3f66541b99 Merge pull request #56020 from frappe/revert-56008-pg-manufacturing-projects
Revert "refactor(manufacturing, projects): make raw SQL portable to PostgreSQL (parity rollout 2/9)"
2026-06-16 23:50:48 +05:30
Mihir Kandoi
e7c2f8ee11 Merge pull request #56019 from frappe/revert-55994-pg-selling-buying
Revert "refactor(selling, buying): make raw SQL portable to PostgreSQL (parity rollout 1/9)"
2026-06-16 23:48:48 +05:30
Mihir Kandoi
8dacf62da0 Revert "refactor(manufacturing, projects): make raw SQL portable to PostgreSQ…"
This reverts commit 2d24eedab2.
2026-06-16 23:29:28 +05:30
Mihir Kandoi
935746e752 Revert "refactor(selling, buying): make raw SQL portable to PostgreSQL (parity rollout 1/9)" 2026-06-16 23:28:53 +05:30
Nikhil Kothari
4e2a10e496 fix: check for bank account permission when updating balance (#56016)
* fix: check for bank account permission when updating balance

* fix: add company to bank balance doctype
2026-06-16 17:23:55 +00:00
ruthra kumar
a32c784084 Merge pull request #55988 from ruthra-kumar/dropping_accountstestmixin
refactor(test): remove dependency on accounts test mixin
2026-06-16 22:02:21 +05:30
Mihir Kandoi
2d24eedab2 refactor(manufacturing, projects): make raw SQL portable to PostgreSQL (parity rollout 2/9) (#56008)
refactor(manufacturing, projects): make raw SQL portable to PostgreSQL

Convert the MariaDB-only raw `frappe.db.sql` in the Manufacturing and Projects
modules to the cross-database query builder / ORM, and fix the non-portable
constructs that remain. Every change is a no-op on MariaDB (identical rendered
SQL / identical results) and only brings PostgreSQL — standards-strict where
MySQL is lax — in line.

Areas: BOM (cost/where-used/explosion), Work Order (operations, required items,
mapper, stock report), Workstation, Production Plan sub-assembly/explosion
queries, BOM Stock Analysis / Process Loss / Work Order Stock reports; Projects
(project, task, timesheet, activity cost, project update), Daily Timesheet
Summary and Project-wise Stock Tracking reports.

Part of the staged MariaDB<->PostgreSQL parity rollout (module 2 of 9).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:20:36 +00:00
dependabot[bot]
ef1fbb7899 chore(deps): bump vite from 8.0.11 to 8.0.16 in /banking (#56007) 2026-06-16 16:00:43 +00:00
rohitwaghchaure
b5ecc9e6bd Merge pull request #55983 from rohitwaghchaure/fixed-recalculate-valuation-rate-in-pr
fix: provision to recalculate valuation rate during reposting
2026-06-16 21:21:59 +05:30
Mihir Kandoi
f195044fd1 Merge pull request #55994 from mihir-kandoi/pg-selling-buying
refactor(selling, buying): make raw SQL portable to PostgreSQL (parity rollout 1/9)
2026-06-16 21:18:57 +05:30
ruthra kumar
004087097c refactor(test): remove redundant clear method and minor fixes 2026-06-16 20:40:03 +05:30
Mihir Kandoi
992015424b Merge pull request #55978 from aerele/driver-permission
fix: update system manager permissions
2026-06-16 20:36:01 +05:30
Mihir Kandoi
a86b169d8b Merge pull request #55974 from aerele/fix/support-70978
fix(stock): update transfer status for mixed transfer flows
2026-06-16 20:34:14 +05:30
Rohit Waghchaure
e183e32619 fix: test case 2026-06-16 20:09:44 +05:30
Rohit Waghchaure
28992eb2f4 test: reset dont_execute_stock_reposts flag in tearDown
The test_prevention_of_cancelled_transaction_riv test sets
frappe.flags.dont_execute_stock_reposts = True without resetting it,
which leaked into the recalculate_valuation_rate tests and made
repost() a no-op (incoming rate stayed unchanged). Reset the flag
in tearDown so reposts run for subsequent tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 19:53:27 +05:30
Rohit Waghchaure
0ae61c4921 fix: provision to recalculate valuation rate during reposting 2026-06-16 19:47:48 +05:30
rohitwaghchaure
8190696d36 Merge pull request #55971 from rohitwaghchaure/fixed-actual-tax-amount
fix: exclude non-stock item's tax value from stock valuation
2026-06-16 19:10:50 +05:30
rohitwaghchaure
b12032485b Merge pull request #55986 from rohitwaghchaure/removed-redundant-validation
chore: removed redundant validation in SCR
2026-06-16 19:08:00 +05:30
Mihir Kandoi
be0f571d62 refactor(selling, buying): make raw SQL portable to PostgreSQL
Convert the MariaDB-only raw `frappe.db.sql` in the Selling and Buying
modules to the cross-database query builder / ORM, and fix the few
non-portable constructs that remain. Every change is a no-op on MariaDB
(identical rendered SQL / identical results) and only brings PostgreSQL —
which is standards-strict where MySQL is lax — in line.

Patterns addressed in these modules:

- Strict GROUP BY — PostgreSQL rejects SELECTing a non-aggregated column
  that isn't functionally dependent on the grouped key. Sales Order
  Analysis, Sales Analytics, Purchase Order Analysis and Procurement
  Tracker now group by the PK (1:1 with the existing key, so no behaviour
  change) or aggregate genuinely-independent columns.
- App clock vs DB clock — Sales Order Analysis computed delay against the
  database CURRENT_DATE, which differs from the app's today by a day when
  the DB runs a different timezone; switched to `nowdate()` (deterministic,
  identical on both DBs).
- Portable date math / functions — DATEDIFF and friends via the db-aware
  query-builder functions.
- Raw SQL → query builder for the remaining self-contained selling/buying
  reads (POS item search, customer naming suffix, packing-items
  availability, customer credit/acquisition reports).

Part of the staged MariaDB↔PostgreSQL parity rollout (module 1 of 9).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:06:19 +05:30
Mihir Kandoi
3a1e4d14f3 Merge pull request #55985 from aerele/fix/batch-item-link-filters
fix(stock): show only batched items in batch item selector
2026-06-16 18:29:44 +05:30
ruthra kumar
1fda0dfb9b refactor(tests): replace AccountsTestMixin master data setup with direct attribute assignments
All test classes inheriting AccountsTestMixin that called create_company(),
create_item(), create_customer(), create_supplier(), create_usd_receivable_account(),
and create_usd_payable_account() in setUp() now set instance attributes directly
using master data pre-created by BootStrapTestData, eliminating redundant DB
inserts on every test run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 17:55:42 +05:30
Rohit Waghchaure
1b4487450c chore: removed redundant validation in SCR 2026-06-16 17:51:54 +05:30
Mihir Kandoi
9564f677e4 Merge pull request #55982 from frappe/codex/bom-secondary-item-modified-timestamp
fix: bump BOM secondary item modified timestamp
2026-06-16 17:39:52 +05:30
Dharanidharan2813
62f6d18143 fix(stock): show only batched items in batch item selector 2026-06-16 17:35:41 +05:30
pandiyan
1dbdf85ddc test(stock): validate completed status for mixed transfer methods 2026-06-16 17:06:26 +05:30
Mihir Kandoi
026ec8a6d9 fix: bump BOM secondary item modified timestamp 2026-06-16 16:52:16 +05:30
Mihir Kandoi
a9207f1e12 Merge pull request #55851 from frappe/feat-stock-balance-alt-uom-columns
feat: add alternate UOM balance columns to Stock Balance report
2026-06-16 16:43:03 +05:30
Nabin Hait
2a5ba9050e Merge pull request #55943 from aerele/ignore_cancelled_GLE
refactor: ignore cancelled GLE's while looking for account
2026-06-16 16:36:18 +05:30
pandiyan
02d41b1dac fix(stock): update transfer status for mixed transfer flows 2026-06-16 16:28:32 +05:30
nareshkannasln
501c8087cb fix: update system manager permissions 2026-06-16 16:08:52 +05:30
Rohit Waghchaure
13e1f84eb1 fix: exclude non-stock item's tax value from stock valuation 2026-06-16 15:35:18 +05:30
Nabin Hait
75394baa28 Merge pull request #55947 from nabinhait/fix/clearance-date-on-amend-54909
fix(accounts): clear clearance date when amending reconciled voucher
2026-06-16 15:28:22 +05:30
Nabin Hait
fb59f825ee Merge pull request #55785 from aerele/fix-payment-entry-transaction-currency
fix: set transaction currency on payment entry gl entries
2026-06-16 15:16:04 +05:30
Nabin Hait
34f78f7261 Merge pull request #55896 from nabinhait/fix/fixed-asset-turnover-ratio
fix: use net fixed assets for Fixed Asset Turnover Ratio
2026-06-16 15:13:03 +05:30
Dipen Gala
987f606b4d fix: resolve pre-commit formatting and missing UOM in stock balance test
- Reformat generator expression in add_alt_uom_columns to satisfy ruff
  line-length rule (pre-commit was auto-fixing this and failing CI)
- Create "Carton" UOM before use in test_alt_uom_balance_uses_first_alternate_uom
  to avoid LinkValidationError when "Carton" doesn't exist in test DB

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:10:00 +05:30
Mihir Kandoi
b1b510c824 Merge pull request #55830 from aerele/fix/qi-stock-entry-inspection
fix(stock): enable quality inspection for all Stock Entry purposes
2026-06-16 15:07:47 +05:30
Mihir Kandoi
066158174e Merge pull request #55852 from umairsy/fix/allow-zero-qty-bom-secondary-items
fix(bom): allow zero qty for secondary items in BOM
2026-06-16 15:05:31 +05:30
Diptanil Saha
07d073da0d fix(accounts): removed whitelist on get_balance_on (#55956) 2026-06-16 14:34:52 +05:30
Sudharsanan11
ba1b1ee20d test(stock): add test to validate the partial transfer of raw material 2026-06-16 14:20:15 +05:30
Sudharsanan11
213adc9ebe fix(stock): allow partial raw material picking/transfer from work order
When creating a pick list for a work order with partially available stock,
the resulting Material Transfer for Manufacture stock entry was setting
fg_completed_qty = for_qty (= wo.qty), causing material_transferred_for_manufacturing
to reach wo.qty after just one partial transfer and blocking further pick lists.

Fix:
- Set fg_completed_qty = 0 on stock entries created from pick lists so the
  old SUM(fg_completed_qty) path never fires prematurely
- Recompute material_transferred_for_manufacturing after each transfer:
  use SUM(fg_completed_qty) when > 0 (direct entries / excess transfer),
  otherwise use min(transferred/required) × wo.qty (pick list flow)
- Add _validate_no_excess_transfer for pick list entries (fg_completed_qty=0)
  to prevent transferring more than pending qty; skip for return entries
  and when backflush is based on Material Transferred for Manufacture
- Remove the zero-qty prompt in pick list work_order trigger; skip the
  qty dialog in work_order.js when max transferable qty is already 0
- Hide fg_completed_qty field in Stock Entry for Material Transfer for
  Manufacture purpose since it is unused in that flow

Fixes: #70713, #63846
2026-06-16 14:20:15 +05:30
Nabin Hait
0e7d45b1af fix: minor fix
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-16 14:08:15 +05:30
Sudharsanan11
609ccc3cb1 test(stock): add test to validate the quality inspection for stock entry 2026-06-16 13:11:06 +05:30
Sudharsanan11
dceb9a3c6c fix(stock): enable quality inspection for all Stock Entry purposes
- Remove `depends_on` restriction from `inspection_required` field so it
  is visible for all Stock Entry purposes, not just Manufacture
- Fix `check_item_quality_inspection` to return items for Stock Entry
  (was returning [] for unknown doctypes, blocking QI creation flow)
- Fix `inspection_type` in transaction.js to be purpose-aware: Manufacture
  and Material Receipt → "Incoming"; all other purposes → "Outgoing"
2026-06-16 12:34:55 +05:30
Nabin Hait
1a8d73cbbe fix(accounts): clear clearance date when amending reconciled voucher
The framework ignores `no_copy` while amending, so a reconciled voucher
carried a stale clearance date into its amendment even though the linked
bank transaction gets unreconciled on cancellation. Reset it via a shared
`before_insert` hook on AccountsController.

Fixes #54909
2026-06-16 00:03:41 +05:30
ervishnucs
40942401df refactor: ignore cancelled GLE's while looking for account 2026-06-15 18:22:30 +05:30
Mohammad Umair Sayed
c1bef53f92 refactor(bom): drop redundant secondary item qty None check
Float fields default to 0, so qty is never None. Per review feedback,
remove the validation entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:06:24 +05:30
Nabin Hait
986af3852c fix: use net fixed assets for Fixed Asset Turnover Ratio
The Fixed Asset Turnover Ratio in the Financial Ratios report divided
Net Sales by Total Assets (the root-level Asset group), which actually
computes the Total Asset Turnover Ratio.

Populate a `fixed_asset` balance from the asset account carrying the
`Fixed Asset` account_type (mirroring how `current_asset` is derived for
`Current Asset`) and use it as the denominator, so the ratio reflects
Net Sales / Net Fixed Assets per the standard definition.

Fixes #54529
2026-06-14 20:06:32 +05:30
Dipen Gala
021b807057 refactor(stock-balance): reduce alt UOM to single column, fix i18n, add tests
- Reduce from 2 alternate UOM columns to 1 (first alt UOM by idx)
- Fix broken translation strings: replace _(f"...{slot}") with
  _("...") — f-strings inside _() are never extracted by bench
  get-untranslated, breaking non-English installations
- Simplify fieldnames: alt_uom_1/alt_uom_1_bal_qty → alt_uom/alt_uom_bal_qty
- Add 4 test cases covering: single alt UOM, no alt UOM, disabled filter,
  and multiple alt UOMs (first-wins behaviour)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:34:28 +05:30
Umair Sayed
bc7c0de208 refactor(bom): remove qty=0 alert from BOM Secondary Item JS
The informational toast is not required for the feature to work.
The core fix (reqd removed from JSON, validation relaxed in bom.py)
is sufficient to allow zero qty on BOM secondary items.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 16:10:56 +05:30
Umair Sayed
de3df6bcef fix(manufacture): preserve user-entered rate for secondary items with zero cost allocation
When a BOM secondary item has cost_allocation_per = 0 (the default), the
previous code unconditionally computed `0 / transfer_qty = 0`, wiping any
rate the user had entered for the item. Now the allocation formula only runs
when cost_allocation_per > 0, allowing the valuation-rate fallback (or a
manually entered rate) to apply instead.

Additionally, secondary items with transfer_qty = 0 now short-circuit the
entire rate pipeline: they get rate = 0 and amount = 0 immediately, avoiding
a ZeroDivisionError and the spurious "enter basic rate" prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 16:04:12 +05:30
Umair Sayed
6771daf6a1 fix(bom): allow zero qty for secondary items (Co-Product, By-Product, Scrap, Additional Finished Good)
Secondary output items in a BOM do not always guarantee output during
manufacture. The actual qty is only known when manufacturing completes,
so setting zero in the BOM is a valid way to express "output is
non-deterministic".

Changes:
- Remove `reqd: 1` from the qty field in BOM Secondary Item so that 0
  is accepted as an explicit value (non_negative constraint is kept, so
  negative values are still rejected).
- Relax validate_secondary_items() in bom.py to only reject qty that is
  None/missing, not qty that is explicitly 0.
- Add a qty event handler in bom.js that shows a blue informational
  alert when the user sets qty to 0, explaining that the actual output
  will be recorded at manufacture time.

Fixes https://github.com/frappe/erpnext/issues/55401

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 14:45:02 +05:30
Dipen Gala
2d93c5835a feat: add alternate UOM balance columns to Stock Balance report
Closes #52953

The Stock Balance report previously showed balance qty only in the item's
stock UOM. To view balance in an alternate UOM, users had to set the
"Include UOM" filter — which applies a single UOM to all items. This breaks
down when different items use different alternate UOMs (e.g., Pens in Box,
Ink in Milliliters).

This change adds a new "Show Alternate UOM Balance" checkbox filter. When
enabled, up to two alternate UOM columns are injected right after the
Balance Qty column:

  Balance Qty | Alt UOM 1 | Balance Qty (Alt UOM 1) | Alt UOM 2 | Balance Qty (Alt UOM 2)

Each row resolves its own alternate UOMs from `tabUOM Conversion Detail`
(ordered by idx, excluding the item's stock UOM). The converted balance
qty is computed as: stock qty / conversion_factor.

Items with fewer than 2 alternate UOMs leave the extra columns blank.
The existing "Include UOM" filter behaviour is unchanged.
2026-06-12 14:11:13 +05:30
ravibharathi656
288f36bbd7 test: assert transaction currency and rate on payment entry gl entries 2026-06-10 16:41:39 +05:30
ravibharathi656
a3c9072812 fix: set transaction currency on payment entry gl entries 2026-06-10 16:41:26 +05:30
144 changed files with 3024 additions and 1888 deletions

View File

@@ -48,7 +48,7 @@
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.11"
"vite": "^8.0.16"
},
"devDependencies": {
"@eslint/js": "^9.39.1",

View File

@@ -250,7 +250,7 @@ const ClosingBalanceForm = ({ defaultBalance, date, bankAccount, onClose }: { de
{_("Enter the closing balance you see in your bank statement for {0} as of the {1}", [bankAccount?.account_name ?? bankAccount?.name ?? '', formatDate(date, 'Do MMM YYYY')])}
</DialogDescription>
</DialogHeader>
{error && <ErrorBanner error={error} />}
{error && <div className="py-2"><ErrorBanner error={error} /></div>}
<div className="py-4">
<CurrencyFormField
name="balance"

View File

@@ -33,6 +33,16 @@ export const getErrorMessages = (error?: FrappeError | null): ParsedErrorMessage
}
})
// @ts-expect-error - some errors have _error_message
if (error?._error_message) {
eMessages.push({
// @ts-expect-error - some errors have _error_message
message: error?._error_message,
title: "Error",
indicator: "red"
})
}
if (eMessages.length === 0) {
// Get the message from the exception by removing the exc_type
const indexOfFirstColon = error?.exception?.indexOf(':')

View File

@@ -358,10 +358,10 @@
dependencies:
"@tybys/wasm-util" "^0.10.1"
"@oxc-project/types@=0.128.0":
version "0.128.0"
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.128.0.tgz#efc7524f948ff9e8ab1404ecad1823849c6fe149"
integrity sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==
"@oxc-project/types@=0.133.0":
version "0.133.0"
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.133.0.tgz#2e282ef9e1d26e06b68ccd14b73f310a3b2cf7f8"
integrity sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==
"@radix-ui/number@1.1.1":
version "1.1.1"
@@ -1042,95 +1042,95 @@
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.1.tgz#78244efe12930c56fd255d7923865857c41ac8cb"
integrity sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==
"@rolldown/binding-android-arm64@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz#3af8b2242086125934a85c1915b76e0a6a2054c1"
integrity sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==
"@rolldown/binding-android-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz#54ce8f8382213f4a314a0c2f7ba83f81ffeae592"
integrity sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==
"@rolldown/binding-darwin-arm64@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz#ae0b4467d24ecd6c6589f03d4d4699616ee9649c"
integrity sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==
"@rolldown/binding-darwin-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz#388fca1566c14c00c4b446fc3928630e7f0d95fc"
integrity sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==
"@rolldown/binding-darwin-x64@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz#23cf24b0a7b96c8990bbdd8a91e7fd3ba82b00e7"
integrity sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==
"@rolldown/binding-darwin-x64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz#53f57de1f599ecf1db13823cfc88c18fb80954ad"
integrity sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==
"@rolldown/binding-freebsd-x64@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz#a047a770f94dc451c062b729e5d1cf82e5c6f9c4"
integrity sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==
"@rolldown/binding-freebsd-x64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz#6f3fdda1b7aeaac9d268a526804b4fb96e4e35f1"
integrity sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==
"@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz#c0b7f346cbf50301cea669a4632bc63aabe6a72c"
integrity sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==
"@rolldown/binding-linux-arm-gnueabihf@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz#d87a454bf585cc9676849377e91d6e375297326f"
integrity sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==
"@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz#af56373c7996ebe6379207cd699c9f7f705e235d"
integrity sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==
"@rolldown/binding-linux-arm64-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz#419fd6bf612cf348f10528cbcd94ebab9607d8d1"
integrity sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==
"@rolldown/binding-linux-arm64-musl@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz#a8f5acd21fcffc8991aa84710e3ae603c4240ea4"
integrity sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==
"@rolldown/binding-linux-arm64-musl@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz#fcc6918696bb76844877e1e4930a18fd0d374069"
integrity sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==
"@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz#1d4a89e040ff82141fc46e717cfab80b05f7c13f"
integrity sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==
"@rolldown/binding-linux-ppc64-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz#32aecb7c8dae5d4f2a8cde57a058ec86991542f8"
integrity sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==
"@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz#97c21feeb2ed87d07820f0b2dcc5dd663e7a7f3b"
integrity sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==
"@rolldown/binding-linux-s390x-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz#bed9346ea81e6bb8b93cf11f5d88b77db890b763"
integrity sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==
"@rolldown/binding-linux-x64-gnu@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz#06310d40fe139ccc3c433b361120d337c66ebec2"
integrity sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==
"@rolldown/binding-linux-x64-gnu@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz#64c2d26f75dffd9b5a1f97557a00ae77250c8cb7"
integrity sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==
"@rolldown/binding-linux-x64-musl@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz#6a711258841f42609b238050cfcd5db13ac136d0"
integrity sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==
"@rolldown/binding-linux-x64-musl@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz#5a45132e8a47659eeaaf3b540c2954a97c860ff3"
integrity sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==
"@rolldown/binding-openharmony-arm64@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz#15cb644beeafdbec930d79ed45c2a7c2573eac70"
integrity sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==
"@rolldown/binding-openharmony-arm64@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz#290513068c55e849dc8457a32afee1d7b0acb309"
integrity sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==
"@rolldown/binding-wasm32-wasi@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz#ca3a56d11dfd533d743711141b3bb4c1ec10110e"
integrity sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==
"@rolldown/binding-wasm32-wasi@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz#3d9972dbf1a953d3c7afaa4a0f20ef2b2e39f31b"
integrity sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==
dependencies:
"@emnapi/core" "1.10.0"
"@emnapi/runtime" "1.10.0"
"@napi-rs/wasm-runtime" "^1.1.4"
"@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz#8c2117d68331d7de59d24631146d538fc203d27c"
integrity sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==
"@rolldown/binding-win32-arm64-msvc@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz#a004ab607a16d6f03bcb555728ff888af75773ad"
integrity sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==
"@rolldown/binding-win32-x64-msvc@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz#bb5c28df3095046778cc1b020ef52fc5ee7b7e70"
integrity sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==
"@rolldown/pluginutils@1.0.0-rc.18":
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz#51cf2589596a179ebe8cbf313f1358c7b51a2fdc"
integrity sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==
"@rolldown/binding-win32-x64-msvc@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz#e2a25b34691a1cc8a1209d7de709063026dd0cdb"
integrity sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==
"@rolldown/pluginutils@1.0.0-rc.7":
version "1.0.0-rc.7"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz#0414869467f0e471a6515d4f506c85fde867e022"
integrity sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==
"@rolldown/pluginutils@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz#e3fcee093fbb5ce765e1ad088ff4de2889f6f9be"
integrity sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==
"@socket.io/component-emitter@~3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2"
@@ -3031,10 +3031,10 @@ ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
nanoid@^3.3.12:
version "3.3.12"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05"
integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==
natural-compare@^1.4.0:
version "1.4.0"
@@ -3119,22 +3119,17 @@ picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
picomatch@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
postcss@^8.5.14:
version "8.5.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
postcss@^8.5.15:
version "8.5.15"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c"
integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==
dependencies:
nanoid "^3.3.11"
nanoid "^3.3.12"
picocolors "^1.1.1"
source-map-js "^1.2.1"
@@ -3394,29 +3389,29 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
rolldown@1.0.0-rc.18:
version "1.0.0-rc.18"
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.0-rc.18.tgz#c597f89a4ce12e6fc918fa91e4f892b340aa92f0"
integrity sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==
rolldown@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.0.3.tgz#db88a3008fb0e28230a00423727ce75ba32121ac"
integrity sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==
dependencies:
"@oxc-project/types" "=0.128.0"
"@rolldown/pluginutils" "1.0.0-rc.18"
"@oxc-project/types" "=0.133.0"
"@rolldown/pluginutils" "^1.0.0"
optionalDependencies:
"@rolldown/binding-android-arm64" "1.0.0-rc.18"
"@rolldown/binding-darwin-arm64" "1.0.0-rc.18"
"@rolldown/binding-darwin-x64" "1.0.0-rc.18"
"@rolldown/binding-freebsd-x64" "1.0.0-rc.18"
"@rolldown/binding-linux-arm-gnueabihf" "1.0.0-rc.18"
"@rolldown/binding-linux-arm64-gnu" "1.0.0-rc.18"
"@rolldown/binding-linux-arm64-musl" "1.0.0-rc.18"
"@rolldown/binding-linux-ppc64-gnu" "1.0.0-rc.18"
"@rolldown/binding-linux-s390x-gnu" "1.0.0-rc.18"
"@rolldown/binding-linux-x64-gnu" "1.0.0-rc.18"
"@rolldown/binding-linux-x64-musl" "1.0.0-rc.18"
"@rolldown/binding-openharmony-arm64" "1.0.0-rc.18"
"@rolldown/binding-wasm32-wasi" "1.0.0-rc.18"
"@rolldown/binding-win32-arm64-msvc" "1.0.0-rc.18"
"@rolldown/binding-win32-x64-msvc" "1.0.0-rc.18"
"@rolldown/binding-android-arm64" "1.0.3"
"@rolldown/binding-darwin-arm64" "1.0.3"
"@rolldown/binding-darwin-x64" "1.0.3"
"@rolldown/binding-freebsd-x64" "1.0.3"
"@rolldown/binding-linux-arm-gnueabihf" "1.0.3"
"@rolldown/binding-linux-arm64-gnu" "1.0.3"
"@rolldown/binding-linux-arm64-musl" "1.0.3"
"@rolldown/binding-linux-ppc64-gnu" "1.0.3"
"@rolldown/binding-linux-s390x-gnu" "1.0.3"
"@rolldown/binding-linux-x64-gnu" "1.0.3"
"@rolldown/binding-linux-x64-musl" "1.0.3"
"@rolldown/binding-openharmony-arm64" "1.0.3"
"@rolldown/binding-wasm32-wasi" "1.0.3"
"@rolldown/binding-win32-arm64-msvc" "1.0.3"
"@rolldown/binding-win32-x64-msvc" "1.0.3"
scheduler@^0.27.0:
version "0.27.0"
@@ -3540,18 +3535,10 @@ tapable@^2.3.3:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
tinyglobby@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.16.tgz#1c3b7eb953fce42b226bc5a1ee06428281aff3d6"
integrity sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==
tinyglobby@^0.2.15, tinyglobby@^0.2.17:
version "0.2.17"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.4"
@@ -3725,16 +3712,16 @@ vfile@^6.0.0:
"@types/unist" "^3.0.0"
vfile-message "^4.0.0"
vite@^8.0.11:
version "8.0.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.11.tgz#d128fe82a0dd24da5127d20560735f1cd7ade0a6"
integrity sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==
vite@^8.0.16:
version "8.0.16"
resolved "https://registry.yarnpkg.com/vite/-/vite-8.0.16.tgz#ae073866c06563d6634a90169a496e11bd84f1a6"
integrity sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==
dependencies:
lightningcss "^1.32.0"
picomatch "^4.0.4"
postcss "^8.5.14"
rolldown "1.0.0-rc.18"
tinyglobby "^0.2.16"
postcss "^8.5.15"
rolldown "1.0.3"
tinyglobby "^0.2.17"
optionalDependencies:
fsevents "~2.3.3"

View File

@@ -22,11 +22,13 @@ class TestAdvancePaymentLedgerEntry(ERPNextTestSuite, AccountsTestMixin):
"""
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_usd_payable_account()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
self.cash = "Cash - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
self.creditors_usd = "_Test Payable USD - _TC"
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
"""

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"allow_rename": 1,
"creation": "2026-04-11 19:48:13.622253",
"doctype": "DocType",
@@ -7,7 +8,8 @@
"field_order": [
"bank_account",
"date",
"balance"
"balance",
"company"
],
"fields": [
{
@@ -31,12 +33,20 @@
"in_list_view": 1,
"label": "Balance",
"reqd": 1
},
{
"fetch_from": "bank_account.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-11 19:49:45.374695",
"modified": "2026-06-16 22:17:48.007982",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account Balance",

View File

@@ -16,6 +16,7 @@ class BankAccountBalance(Document):
balance: DF.Currency
bank_account: DF.Link
company: DF.Link | None
date: DF.Date
# end: auto-generated types

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Max, Sum
from frappe.utils import cint, create_batch, flt
from erpnext import get_default_cost_center
@@ -1410,12 +1410,14 @@ def get_je_matching_query(
Sum(getattr(jea, amount_field)).as_("paid_amount"),
ConstantColumn("Journal Entry").as_("doctype"),
je.name,
je.cheque_no.as_("reference_no"),
je.cheque_date.as_("reference_date"),
je.pay_to_recd_from.as_("party"),
jea.party_type,
je.posting_date,
jea.account_currency.as_("currency"),
# non-grouped columns are constant per grouped JE name (party_type/currency come from the
# single bank-account line) -> Max() keeps the GROUP BY valid on postgres with the same value
Max(je.cheque_no).as_("reference_no"),
Max(je.cheque_date).as_("reference_date"),
Max(je.pay_to_recd_from).as_("party"),
Max(jea.party_type).as_("party_type"),
Max(je.posting_date).as_("posting_date"),
Max(jea.account_currency).as_("currency"),
)
.where(je.docstatus == 1)
.where(je.voucher_type != "Opening Entry")
@@ -1423,7 +1425,7 @@ def get_je_matching_query(
.where(jea.account == common_filters.bank_account)
.where(filter_by_date)
.groupby(je.name)
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
.orderby(Max(je.cheque_date) if cint(filter_by_reference_date) else Max(je.posting_date))
)
if frappe.flags.auto_reconcile_vouchers is True:

View File

@@ -17,9 +17,10 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.bank = "HDFC - _TC"
self.debit_to = "Debtors - _TC"
bank_dt = qb.DocType("Bank")
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account()

View File

@@ -26,9 +26,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBankStatementImportLog(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.bank = "HDFC - _TC"
bank_dt = qb.DocType("Bank")
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account()

View File

@@ -5,6 +5,8 @@ import frappe
from frappe import _
from frappe.model.docstatus import DocStatus
from frappe.model.document import Document
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Abs, Max, Sum
from frappe.utils import flt, getdate
@@ -478,30 +480,28 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
def get_related_bank_gl_entries(docs):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
if not docs:
return {}
result = frappe.db.sql(
"""
SELECT
gle.voucher_type AS doctype,
gle.voucher_no AS docname,
gle.account AS gl_account,
SUM(ABS(gle.credit_in_account_currency - gle.debit_in_account_currency)) AS amount
FROM
`tabGL Entry` gle
LEFT JOIN
`tabAccount` ac ON ac.name = gle.account
WHERE
ac.account_type = 'Bank'
AND (gle.voucher_type, gle.voucher_no) IN %(docs)s
AND gle.is_cancelled = 0
GROUP BY
gle.voucher_type, gle.voucher_no, gle.account
""",
{"docs": docs},
as_dict=True,
gle = frappe.qb.DocType("GL Entry")
ac = frappe.qb.DocType("Account")
result = (
frappe.qb.from_(gle)
.left_join(ac)
.on(ac.name == gle.account)
.select(
gle.voucher_type.as_("doctype"),
gle.voucher_no.as_("docname"),
gle.account.as_("gl_account"),
Sum(Abs(gle.credit_in_account_currency - gle.debit_in_account_currency)).as_("amount"),
)
.where(
(ac.account_type == "Bank")
& Tuple(gle.voucher_type, gle.voucher_no).isin([Tuple(vt, vn) for vt, vn in docs])
& (gle.is_cancelled == 0)
)
.groupby(gle.voucher_type, gle.voucher_no, gle.account)
.run(as_dict=True)
)
entries = {}
@@ -523,31 +523,32 @@ def get_total_allocated_amount(docs):
if not docs:
return {}
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT total, latest_date, gl_account, payment_document, payment_entry FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account,
btp.payment_document,
btp.payment_entry
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
(btp.payment_document, btp.payment_entry) IN %(docs)s
AND bt.docstatus = 1
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
) temp
WHERE
rownum = 1
""",
dict(docs=docs),
as_dict=True,
# The original window query (ROW_NUMBER/FIRST_VALUE + rownum = 1) just collapses to one
# row per (account, payment_document, payment_entry) with the partition's allocation total
# and most recent transaction date — i.e. a plain GROUP BY with SUM and MAX.
btp = frappe.qb.DocType("Bank Transaction Payments")
bt = frappe.qb.DocType("Bank Transaction")
ba = frappe.qb.DocType("Bank Account")
result = (
frappe.qb.from_(btp)
.left_join(bt)
.on(bt.name == btp.parent)
.left_join(ba)
.on(ba.name == bt.bank_account)
.select(
Sum(btp.allocated_amount).as_("total"),
Max(bt.date).as_("latest_date"),
ba.account.as_("gl_account"),
btp.payment_document,
btp.payment_entry,
)
.where(
Tuple(btp.payment_document, btp.payment_entry).isin([Tuple(pd, pe) for pd, pe in docs])
& (bt.docstatus == 1)
)
.groupby(ba.account, btp.payment_document, btp.payment_entry)
.run(as_dict=True)
)
payment_allocation_details = {}

View File

@@ -104,6 +104,36 @@ class TestBankTransaction(ERPNextTestSuite):
self.assertEqual(bank_transaction.unallocated_amount, 1700)
self.assertEqual(bank_transaction.payment_entries, [])
# Amending a reconciled payment entry must not carry over its clearance date
def test_clearance_date_cleared_on_amend(self):
bank_transaction = frappe.get_doc(
"Bank Transaction",
dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
)
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment.name,
"amount": bank_transaction.unallocated_amount,
}
]
)
reconcile_vouchers(bank_transaction.name, vouchers)
self.assertTrue(frappe.db.get_value("Payment Entry", payment.name, "clearance_date"))
payment.reload()
payment.cancel()
amended = frappe.copy_doc(payment)
amended.amended_from = payment.name
amended.docstatus = 0
amended.insert()
self.assertFalse(amended.clearance_date)
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc(

View File

@@ -11,9 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.bank = "HDFC - _TC"
self.debit_to = "Debtors - _TC"
self.cash = "Cash - _TC"
bank_dt = qb.DocType("Bank")
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
self.create_bank_account()

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt
@@ -43,13 +44,17 @@ class CashierClosing(Document):
self.make_calculations()
def get_outstanding(self):
values = frappe.db.sql(
"""
select sum(outstanding_amount)
from `tabSales Invoice`
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
""",
(self.date, self.from_time, self.time, self.user),
si = frappe.qb.DocType("Sales Invoice")
values = (
frappe.qb.from_(si)
.select(Sum(si.outstanding_amount))
.where(
(si.posting_date == self.date)
& (si.posting_time >= self.from_time)
& (si.posting_time <= self.time)
& (si.owner == self.user)
)
.run()
)
self.outstanding_amount = flt(values[0][0] if values else 0)

View File

@@ -8,7 +8,7 @@ from frappe import _, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import NullIf, Sum
from frappe.query_builder.functions import Max, NullIf, Sum
from frappe.utils import flt, get_link_to_form
import erpnext
@@ -188,12 +188,18 @@ class ExchangeRateRevaluation(Document):
accounts = [x[0] for x in res]
if accounts:
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
)
gle = qb.DocType("GL Entry")
# balance expressions reused in both SELECT and HAVING; postgres can't reference a
# SELECT alias inside HAVING, so the aggregate expression must be repeated there.
balance = Sum(gle.debit) - Sum(gle.credit)
balance_in_account_currency = Sum(gle.debit_in_account_currency) - Sum(
gle.credit_in_account_currency
)
having_clause = (balance != balance_in_account_currency) & (
(balance_in_account_currency != 0) | (balance != 0)
)
# conditions
conditions = []
conditions.append(gle.account.isin(accounts))
@@ -209,17 +215,15 @@ class ExchangeRateRevaluation(Document):
qb.from_(gle)
.select(
gle.account,
gle.party_type,
gle.party,
gle.account_currency,
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
"balance_in_account_currency"
),
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
(Sum(gle.debit) - Sum(gle.credit) == 0)
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
"zero_balance"
),
# grouped by NullIf(party_type/party, ""); the bare columns + account_currency are
# constant per group -> Max() keeps the GROUP BY valid on postgres with the same value.
Max(gle.party_type).as_("party_type"),
Max(gle.party).as_("party"),
Max(gle.account_currency).as_("account_currency"),
balance_in_account_currency.as_("balance_in_account_currency"),
balance.as_("balance"),
# zero_balance is recomputed in Python below (after rounding), so the SQL value is
# unused -- dropped (it used MySQL's XOR operator, which postgres lacks).
)
.where(Criterion.all(conditions))
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))

View File

@@ -15,11 +15,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_item()
self.create_customer()
self.clear_old_entries()
self.company = "_Test Company"
self.item = "_Test Item"
self.customer = "_Test Customer"
self.cost_center = "Main - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
self.set_system_and_company_settings()
def set_system_and_company_settings(self):

View File

@@ -319,56 +319,48 @@ class InvoiceDiscounting(AccountsController):
@frappe.whitelist()
def get_invoices(filters: str):
filters = frappe._dict(json.loads(filters))
cond = []
if filters.customer:
cond.append("customer=%(customer)s")
if filters.from_date:
cond.append("posting_date >= %(from_date)s")
if filters.to_date:
cond.append("posting_date <= %(to_date)s")
if filters.min_amount:
cond.append("base_grand_total >= %(min_amount)s")
if filters.max_amount:
cond.append("base_grand_total <= %(max_amount)s")
si = frappe.qb.DocType("Sales Invoice")
di = frappe.qb.DocType("Discounted Invoice")
where_condition = ""
if cond:
where_condition += " and " + " and ".join(cond)
discounted = frappe.qb.from_(di).select(di.sales_invoice).where(di.docstatus == 1)
return frappe.db.sql(
"""
select
name as sales_invoice,
customer,
posting_date,
outstanding_amount,
debit_to
from `tabSales Invoice` si
where
docstatus = 1
and outstanding_amount > 0
%s
and not exists(select di.name from `tabDiscounted Invoice` di
where di.docstatus=1 and di.sales_invoice=si.name)
"""
% where_condition,
filters,
as_dict=1,
query = (
frappe.qb.from_(si)
.select(
si.name.as_("sales_invoice"),
si.customer,
si.posting_date,
si.outstanding_amount,
si.debit_to,
)
.where((si.docstatus == 1) & (si.outstanding_amount > 0) & si.name.notin(discounted))
)
if filters.customer:
query = query.where(si.customer == filters.customer)
if filters.from_date:
query = query.where(si.posting_date >= filters.from_date)
if filters.to_date:
query = query.where(si.posting_date <= filters.to_date)
if filters.min_amount:
query = query.where(si.base_grand_total >= filters.min_amount)
if filters.max_amount:
query = query.where(si.base_grand_total <= filters.max_amount)
return query.run(as_dict=1)
def get_party_account_based_on_invoice_discounting(sales_invoice):
party_account = None
invoice_discounting = frappe.db.sql(
"""
select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status
from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch
where par.name=ch.parent
and par.docstatus=1
and ch.sales_invoice = %s
""",
(sales_invoice),
as_dict=1,
par = frappe.qb.DocType("Invoice Discounting")
ch = frappe.qb.DocType("Discounted Invoice")
invoice_discounting = (
frappe.qb.from_(par)
.inner_join(ch)
.on(par.name == ch.parent)
.select(par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status)
.where((par.docstatus == 1) & (ch.sales_invoice == sales_invoice))
.run(as_dict=1)
)
if invoice_discounting:
if invoice_discounting[0].status == "Disbursed":

View File

@@ -484,12 +484,6 @@ class JournalEntry(AccountsController):
d.idx, d.account, d.party_type
)
)
elif d.party_type or d.party:
frappe.throw(
_(
"Row {0}: Party Type or Party can only be set for Receivable / Payable account, but account {1} is of type {2}"
).format(d.idx, d.account, account_type or _("None"))
)
def check_credit_limit(self):
customers = list(

View File

@@ -662,13 +662,6 @@ class TestJournalEntry(ERPNextTestSuite):
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def test_party_not_allowed_for_non_receivable_payable_account(self):
customer = make_customer("_Test New Customer")
jv = make_journal_entry(account1="_Test Cash - _TC", account2="_Test Bank - _TC", amount=100, save=False)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = customer
self.assertRaises(frappe.ValidationError, jv.save)
def test_validate_reference_doc_debit_against_sales_order_throws(self):
"""Characterize: a debit entry linked to a Sales Order is rejected."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

View File

@@ -12,10 +12,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestLedgerHealth(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.debit_to = "Debtors - _TC"
self.income_account = "Sales - _TC"
self.configure_monitoring_tool()
self.clear_old_entries()
def configure_monitoring_tool(self):
monitor_settings = frappe.get_doc("Ledger Health Monitor")

View File

@@ -52,12 +52,11 @@ class ModeofPayment(Document):
def validate_pos_mode_of_payment(self):
if not self.enabled:
pos_profiles = frappe.db.sql(
"""SELECT sip.parent FROM `tabSales Invoice Payment` sip
WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""",
(self.name),
pos_profiles = frappe.get_all(
"Sales Invoice Payment",
filters={"parenttype": "POS Profile", "mode_of_payment": self.name},
pluck="parent",
)
pos_profiles = list(map(lambda x: x[0], pos_profiles))
if pos_profiles:
message = _(

View File

@@ -270,6 +270,13 @@ def start_import(invoices):
errors = 0
names = []
for idx, d in enumerate(invoices):
# Scope each invoice to a savepoint so a failure only undoes that invoice.
# A plain rollback() would discard the whole transaction — including invoices
# imported earlier in this batch and the error logs of earlier failures (the
# latter only survive on mariadb because the Error Log table is MyISAM; on
# postgres they would be lost). Rolling back to a savepoint keeps both.
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
frappe.db.savepoint(savepoint)
try:
invoice_number = None
if d.invoice_number:
@@ -284,7 +291,7 @@ def start_import(invoices):
names.append(doc.name)
except Exception:
errors += 1
frappe.db.rollback()
frappe.db.rollback(save_point=savepoint)
doc.log_error("Opening invoice creation failed")
if errors:
frappe.msgprint(

View File

@@ -9,8 +9,8 @@ import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Count
from frappe.query_builder import Case, Tuple
from frappe.query_builder.functions import Abs, Count, Max
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
from pypika.functions import Coalesce, Sum
@@ -766,13 +766,19 @@ class PaymentEntry(AccountsController):
def validate_journal_entry(self):
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype == "Journal Entry":
je_accounts = frappe.db.sql(
"""select debit, credit from `tabJournal Entry Account`
where account = %s and party=%s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
""",
(self.party_account, self.party, d.reference_name),
as_dict=True,
je_accounts = frappe.get_all(
"Journal Entry Account",
filters={
"account": self.party_account,
"party": self.party,
"docstatus": 1,
"parent": d.reference_name,
},
or_filters=[
["reference_type", "is", "not set"],
["reference_type", "in", ["Sales Order", "Purchase Order"]],
],
fields=["debit", "credit"],
)
if not je_accounts:
@@ -857,27 +863,17 @@ class PaymentEntry(AccountsController):
)
base_outstanding = flt(allocated_amount * conversion_rate, base_outstanding_precision)
ps = frappe.qb.DocType("Payment Schedule")
if cancel:
frappe.db.sql(
"""
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` - %s,
base_paid_amount = `base_paid_amount` - %s,
discounted_amount = `discounted_amount` - %s,
outstanding = `outstanding` + %s,
base_outstanding = `base_outstanding` - %s
WHERE parent = %s and payment_term = %s""",
(
allocated_amount - discounted_amt,
base_paid_amount,
discounted_amt,
allocated_amount,
base_outstanding,
key[1],
key[0],
),
)
(
frappe.qb.update(ps)
.set(ps.paid_amount, ps.paid_amount - (allocated_amount - discounted_amt))
.set(ps.base_paid_amount, ps.base_paid_amount - base_paid_amount)
.set(ps.discounted_amount, ps.discounted_amount - discounted_amt)
.set(ps.outstanding, ps.outstanding + allocated_amount)
.set(ps.base_outstanding, ps.base_outstanding - base_outstanding)
.where((ps.parent == key[1]) & (ps.payment_term == key[0]))
).run()
else:
if allocated_amount > outstanding:
frappe.throw(
@@ -887,26 +883,15 @@ class PaymentEntry(AccountsController):
)
if allocated_amount and outstanding:
frappe.db.sql(
"""
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` + %s,
base_paid_amount = `base_paid_amount` + %s,
discounted_amount = `discounted_amount` + %s,
outstanding = `outstanding` - %s,
base_outstanding = `base_outstanding` - %s
WHERE parent = %s and payment_term = %s""",
(
allocated_amount - discounted_amt,
base_paid_amount,
discounted_amt,
allocated_amount,
base_outstanding,
key[1],
key[0],
),
)
(
frappe.qb.update(ps)
.set(ps.paid_amount, ps.paid_amount + (allocated_amount - discounted_amt))
.set(ps.base_paid_amount, ps.base_paid_amount + base_paid_amount)
.set(ps.discounted_amount, ps.discounted_amount + discounted_amt)
.set(ps.outstanding, ps.outstanding - allocated_amount)
.set(ps.base_outstanding, ps.base_outstanding - base_outstanding)
.where((ps.parent == key[1]) & (ps.payment_term == key[0]))
).run()
def get_allocated_amount_in_transaction_currency(
self, allocated_amount, reference_doctype, reference_docname
@@ -1216,11 +1201,7 @@ class PaymentEntry(AccountsController):
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
def clear_unallocated_reference_document_rows(self):
self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]}))
frappe.db.sql(
"""delete from `tabPayment Entry Reference`
where parent = %s and allocated_amount = 0""",
self.name,
)
frappe.db.delete("Payment Entry Reference", {"parent": self.name, "allocated_amount": 0})
def set_title(self):
if frappe.flags.in_import and self.title:
@@ -1876,7 +1857,7 @@ def get_matched_payment_request_of_references(references=None):
PR.reference_doctype,
PR.reference_name,
PR.outstanding_amount.as_("allocated_amount"),
PR.name.as_("payment_request"),
Max(PR.name).as_("payment_request"), # count == 1 below ⇒ one row per group; postgres-safe
Count("*").as_("count"),
)
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
@@ -2315,12 +2296,7 @@ def get_orders_to_be_billed(
if not voucher_type:
return []
# dynamic dimension filters
condition = ""
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
@@ -2329,38 +2305,38 @@ def get_orders_to_be_billed(
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
orders = frappe.db.sql(
"""
select
name as voucher_no,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
(if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount,
transaction_date as posting_date
from
`tab{voucher_type}`
where
{party_type} = %s
and docstatus = 1
and company = %s
and status != "Closed"
and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
and abs(100 - per_billed) > 0.01
{condition}
order by
transaction_date, name
""".format(
**{
"rounded_total_field": rounded_total_field,
"grand_total_field": grand_total_field,
"voucher_type": voucher_type,
"party_type": scrub(party_type),
"condition": condition,
}
),
(party, company),
as_dict=True,
voucher = frappe.qb.DocType(voucher_type)
invoice_amount = (
Case()
.when(voucher[rounded_total_field] != 0, voucher[rounded_total_field])
.else_(voucher[grand_total_field])
)
query = (
frappe.qb.from_(voucher)
.select(
voucher.name.as_("voucher_no"),
invoice_amount.as_("invoice_amount"),
(invoice_amount - voucher.advance_paid).as_("outstanding_amount"),
voucher.transaction_date.as_("posting_date"),
)
.where(
(voucher[scrub(party_type)] == party)
& (voucher.docstatus == 1)
& (voucher.company == company)
& (voucher.status != "Closed")
& (invoice_amount > voucher.advance_paid)
& (Abs(100 - voucher.per_billed) > 0.01)
)
)
# dynamic dimension filters
for dim in active_dimensions:
if filters.get(dim.fieldname):
query = query.where(voucher[dim.fieldname] == filters.get(dim.fieldname))
orders = query.orderby(voucher.transaction_date).orderby(voucher.name).run(as_dict=True)
order_list = []
for d in orders:
if (
@@ -2409,8 +2385,8 @@ def get_negative_outstanding_invoices(
return frappe.db.sql(
"""
select
"{voucher_type}" as voucher_type, name as voucher_no, {account} as account,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
'{voucher_type}' as voucher_type, name as voucher_no, {account} as account,
coalesce(nullif({rounded_total_field}, 0), {grand_total_field}) as invoice_amount,
outstanding_amount, posting_date,
due_date, conversion_rate as exchange_rate
from
@@ -3272,27 +3248,28 @@ def get_reference_as_per_payment_terms(
def get_paid_amount(dt, dn, party_type, party, account, due_date):
gle = frappe.qb.DocType("GL Entry")
if party_type == "Customer":
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
dr_or_cr = gle.credit_in_account_currency - gle.debit_in_account_currency
else:
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
dr_or_cr = gle.debit_in_account_currency - gle.credit_in_account_currency
paid_amount = frappe.db.sql(
f"""
select ifnull(sum({dr_or_cr}), 0) as paid_amount
from `tabGL Entry`
where against_voucher_type = %s
and against_voucher = %s
and party_type = %s
and party = %s
and account = %s
and due_date = %s
and {dr_or_cr} > 0
""",
(dt, dn, party_type, party, account, due_date),
paid_amount = (
frappe.qb.from_(gle)
.select(Sum(dr_or_cr))
.where(
(gle.against_voucher_type == dt)
& (gle.against_voucher == dn)
& (gle.party_type == party_type)
& (gle.party == party)
& (gle.account == account)
& (gle.due_date == due_date)
& (dr_or_cr > 0)
)
.run()
)
return paid_amount[0][0] if paid_amount else 0
return (paid_amount[0][0] or 0) if paid_amount else 0
@frappe.whitelist()

View File

@@ -34,8 +34,14 @@ class PaymentEntryGLComposer(BaseGLComposer):
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
add_regional_gl_entries(gl_entries, doc)
self.set_transaction_currency_and_rate_in_gl_map(gl_entries, doc)
return gl_entries
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries, doc):
for gle in gl_entries:
gle.setdefault("transaction_currency", doc.transaction_currency)
gle.setdefault("transaction_exchange_rate", doc.transaction_exchange_rate)
def add_party_gl_entries(self, gl_entries):
doc = self.doc
if not doc.party_account:

View File

@@ -1037,14 +1037,17 @@ class TestPaymentEntry(ERPNextTestSuite):
gle.credit_in_account_currency,
gle.debit_in_transaction_currency,
gle.credit_in_transaction_currency,
gle.transaction_currency,
gle.transaction_exchange_rate,
)
.orderby(gle.account)
.where(gle.voucher_no == payment_entry.name)
.run()
)
# transaction currency/rate come from the paid-from USD account (company currency is INR)
expected_gl_entries = (
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0, "USD", 84.4),
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0, "USD", 84.4),
)
self.assertEqual(gl_entries, expected_gl_entries)

View File

@@ -10,76 +10,22 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_ent
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentLedgerEntry(ERPNextTestSuite):
def setUp(self):
self.ple = qb.DocType("Payment Ledger Entry")
self.create_company()
self.create_item()
self.create_customer()
self.clear_old_entries()
def create_company(self):
company_name = "_Test Payment Ledger"
company = None
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PL"
self.income_account = "Sales - _PL"
self.expense_account = "Cost of Goods Sold - _PL"
self.debit_to = "Debtors - _PL"
self.creditors = "Creditors - _PL"
# create bank account
if frappe.db.exists("Account", "HDFC - _PL"):
self.bank = "HDFC - _PL"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PL",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item_name = "_Test PL Item"
item = create_item(
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
name = "_Test PL Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.creditors = "Creditors - _TC"
self.bank = "Cash - _TC"
self.item = "_Test Item"
self.customer = "_Test Customer"
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -152,18 +98,6 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
)
return so
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
je = frappe.new_doc("Journal Entry")
je.posting_date = posting_date or nowdate()

View File

@@ -60,23 +60,32 @@ class PaymentOrder(Document):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
return frappe.get_all(
"Payment Order Reference",
filters={"parent": filters.get("parent"), "mode_of_payment": ["like", f"%{txt}%"]},
fields=["mode_of_payment"],
limit_start=start,
limit_page_length=page_len,
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
as_list=True,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
return frappe.db.sql(
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
return frappe.get_all(
"Payment Order Reference",
filters={
"parent": filters.get("parent"),
"supplier": ["like", f"%{txt}%"],
"payment_reference": ["is", "not set"],
},
fields=["supplier"],
limit_start=start,
limit_page_length=page_len,
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
as_list=True,
)

View File

@@ -629,11 +629,9 @@ class PaymentRequest(Document):
def check_if_payment_entry_exists(self):
if self.status == "Paid":
if frappe.get_all(
if frappe.db.exists(
"Payment Entry Reference",
filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
fields=["parent"],
limit=1,
{"reference_name": self.reference_name, "docstatus": ["<", 2]},
):
frappe.throw(_("Payment Entry already exists"), title=_("Error"))
@@ -1212,10 +1210,11 @@ def get_dummy_message(doc):
@frappe.whitelist()
def get_subscription_details(reference_doctype: str, reference_name: str):
if reference_doctype == "Sales Invoice":
subscriptions = frappe.db.sql(
"""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",
reference_name,
as_dict=1,
subscriptions = frappe.get_all(
"Subscription Invoice",
filters={"invoice": reference_name},
fields=["parent as sub_name"],
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
)
subscription_plans = []
for subscription in subscriptions:

View File

@@ -18,7 +18,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry(
@@ -27,10 +26,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv1.company = company
jv1.company = "Test PCV Company"
jv1.save()
jv1.submit()
@@ -40,10 +39,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cost of Goods Sold - TPC",
account2="Cash - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv2.company = company
jv2.company = "Test PCV Company"
jv2.save()
jv2.submit()
@@ -67,14 +66,13 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
company = create_company()
surplus_account = create_account()
cost_center1 = create_cost_center("Main")
cost_center2 = create_cost_center("Western Branch")
create_sales_invoice(
company=company,
company="Test PCV Company",
cost_center=cost_center1,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -85,7 +83,7 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
posting_date="2021-03-15",
)
create_sales_invoice(
company=company,
company="Test PCV Company",
cost_center=cost_center2,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -130,12 +128,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
)
def test_period_closing_with_finance_book_entries(self):
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice(
company=company,
company="Test PCV Company",
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
@@ -152,9 +149,9 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
amount=400,
cost_center=cost_center,
posting_date="2021-03-15",
company=company,
company="Test PCV Company",
)
jv.company = company
jv.company = "Test PCV Company"
jv.finance_book = create_finance_book().name
jv.save()
jv.submit()
@@ -181,7 +178,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertSequenceEqual(pcv_gle, expected_gle)
def test_gl_entries_restrictions(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
self.make_period_closing_voucher(posting_date="2021-03-31")
@@ -192,16 +188,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv1.company = company
jv1.company = "Test PCV Company"
jv1.save()
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
@@ -211,10 +206,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center1,
company=company,
company="Test PCV Company",
save=False,
)
jv1.company = company
jv1.company = "Test PCV Company"
jv1.save()
jv1.submit()
@@ -224,10 +219,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
company=company,
company="Test PCV Company",
save=False,
)
jv2.company = company
jv2.company = "Test PCV Company"
jv2.save()
jv2.submit()
@@ -254,11 +249,11 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
company=company,
company="Test PCV Company",
save=False,
)
jv3.company = company
jv3.company = "Test PCV Company"
jv3.save()
jv3.submit()
@@ -293,12 +288,12 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
warehouse = frappe.db.get_value("Warehouse", {"company": "Test PCV Company"}, "name")
repost_doc = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"company": company,
"company": "Test PCV Company",
"posting_date": "2020-03-15",
"based_on": "Item and Warehouse",
"item_code": "Test Item 1",
@@ -339,7 +334,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv = make_journal_entry(
@@ -348,10 +342,10 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
company="Test PCV Company",
save=False,
)
jv.company = company
jv.company = "Test PCV Company"
jv.save()
jv.submit()
@@ -377,19 +371,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
def create_company():
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "Test PCV Company",
"country": "United States",
"default_currency": "USD",
}
)
company.insert(ignore_if_duplicate=True)
return company.name
def create_account():
account = frappe.get_doc(
{

View File

@@ -553,7 +553,8 @@ def process_individual_date(docname: str, date, report_type, parentfield):
Sum(gle.credit).as_("credit"),
Sum(gle.debit_in_account_currency).as_("debit_in_account_currency"),
Sum(gle.credit_in_account_currency).as_("credit_in_account_currency"),
gle.account_currency,
# account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid
Max(gle.account_currency).as_("account_currency"),
).where(
(gle.company.eq(company))
& (gle.is_cancelled.eq(0))

View File

@@ -25,10 +25,8 @@ class TestProcessStatementOfAccounts(ERPNextTestSuite, AccountsTestMixin):
update_modified=False,
)
self.create_company()
self.create_customer()
self.company = "_Test Company"
self.create_customer(customer_name="Other Customer")
self.clear_old_entries()
self.si = create_sales_invoice()
create_sales_invoice(customer="Other Customer")

View File

@@ -16,12 +16,14 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_supplier()
self.create_usd_receivable_account()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.cash = "Cash - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
def create_sales_invoice(self, do_not_submit=False):
si = create_sales_invoice(
@@ -372,7 +374,6 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(so.advance_paid, 0)
def test_06_unreconcile_advance_from_payment_entry(self):
self.enable_advance_as_liability()
so1 = self.create_sales_order()
so2 = self.create_sales_order()
@@ -423,7 +424,11 @@ class TestUnreconcilePayment(ERPNextTestSuite, AccountsTestMixin):
self.disable_advance_as_liability()
def test_07_adv_from_so_to_invoice(self):
self.enable_advance_as_liability()
frappe.db.set_value("Company", self.company, "book_advance_payments_in_separate_party_account", True)
frappe.db.set_value(
"Company", self.company, "default_advance_received_account", "Advance Received - _TC"
)
so = self.create_sales_order()
pe = self.create_payment_entry()
pe.paid_amount = 1000

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.query_builder.functions import Abs, Max, Sum
from frappe.utils.data import comma_and
from erpnext.accounts.utils import (
@@ -72,7 +72,7 @@ class UnreconcilePayment(Document):
alloc.party,
)
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", 1)
@frappe.whitelist()
@@ -120,18 +120,20 @@ def get_linked_payments_for_doc(
res = (
qb.from_(ple)
.select(
ple.account,
ple.party_type,
ple.party,
ple.company,
ple.voucher_type.as_("reference_doctype"),
Max(ple.account).as_("account"),
Max(ple.party_type).as_("party_type"),
Max(ple.party).as_("party"),
Max(ple.company).as_("company"),
Max(ple.voucher_type).as_("reference_doctype"),
ple.voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
Max(ple.account_currency).as_("account_currency"),
)
.where(Criterion.all(criteria))
.groupby(ple.voucher_no, ple.against_voucher_no)
.having(qb.Field("allocated_amount") > 0)
.having(Abs(Sum(ple.amount_in_account_currency)) > 0)
# deterministic order across backends (postgres GROUP BY does not imply ordering)
.orderby(ple.voucher_no)
.run(as_dict=True)
)
return res
@@ -146,17 +148,19 @@ def get_linked_payments_for_doc(
query = (
qb.from_(ple)
.select(
ple.company,
ple.account,
ple.party_type,
ple.party,
ple.against_voucher_type.as_("reference_doctype"),
Max(ple.company).as_("company"),
Max(ple.account).as_("account"),
Max(ple.party_type).as_("party_type"),
Max(ple.party).as_("party"),
Max(ple.against_voucher_type).as_("reference_doctype"),
ple.against_voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
Max(ple.account_currency).as_("account_currency"),
)
.where(Criterion.all(criteria))
.groupby(ple.against_voucher_no)
# deterministic order across backends (postgres GROUP BY does not imply ordering)
.orderby(ple.against_voucher_no)
)
res = query.run(as_dict=True)
@@ -180,15 +184,18 @@ def get_linked_advances(company, docname):
return (
qb.from_(adv)
.select(
adv.company,
adv.against_voucher_type.as_("reference_doctype"),
# non-grouped columns are constant per against_voucher_no -> Max() is unchanged and postgres-valid
Max(adv.company).as_("company"),
Max(adv.against_voucher_type).as_("reference_doctype"),
adv.against_voucher_no.as_("reference_name"),
Abs(Sum(adv.amount)).as_("allocated_amount"),
adv.currency,
Max(adv.currency).as_("currency"),
)
.where(Criterion.all(criteria))
.having(qb.Field("allocated_amount") > 0)
.having(Abs(Sum(adv.amount)) > 0)
.groupby(adv.against_voucher_no)
# deterministic order across backends (postgres GROUP BY does not imply ordering)
.orderby(adv.against_voucher_no)
.run(as_dict=True)
)

View File

@@ -543,11 +543,19 @@ def get_party_gle_currency(party_type, party, company):
def get_party_gle_account(party_type, party, company):
def generator():
existing_gle_account = frappe.db.sql(
"""select account from `tabGL Entry`
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
limit 1""",
{"company": company, "party_type": party_type, "party": party},
gl = qb.DocType("GL Entry")
existing_gle_account = (
qb.from_(gl)
.select(gl.account)
.where(
(gl.docstatus == 1)
& (gl.company == company)
& (gl.party_type == party_type)
& (gl.party == party)
& (gl.is_cancelled == 0)
)
.limit(1)
.run()
)
return existing_gle_account[0][0] if existing_gle_account else None

View File

@@ -9,11 +9,10 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_supplier(currency="USD", supplier_name="Test Supplier2")
self.create_usd_payable_account()
self.company = "_Test Company"
self.item = "_Test Item"
self.supplier = "_Test Supplier 2"
self.creditors_usd = "_Test Payable USD - _TC"
def test_accounts_payable_for_foreign_currency_supplier(self):
pi = self.create_purchase_invoice(do_not_submit=True)

View File

@@ -7,7 +7,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, query_builder, scrub
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.query_builder.functions import Date, Max, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -427,32 +427,21 @@ class ReceivablePayableReport:
self.delivery_notes = frappe._dict()
# delivery note link inside sales invoice
# nosemgrep
si_against_dn = frappe.db.sql(
"""
select parent, delivery_note
from `tabSales Invoice Item`
where docstatus=1 and parent in (%s)
"""
% (",".join(["%s"] * len(self.invoices))),
tuple(self.invoices),
as_dict=1,
si_against_dn = frappe.get_all(
"Sales Invoice Item",
filters={"docstatus": 1, "parent": ["in", list(self.invoices)]},
fields=["parent", "delivery_note"],
)
for d in si_against_dn:
if d.delivery_note:
self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note)
# nosemgrep
dn_against_si = frappe.db.sql(
"""
select distinct parent, against_sales_invoice
from `tabDelivery Note Item`
where against_sales_invoice in (%s)
"""
% (",".join(["%s"] * len(self.invoices))),
tuple(self.invoices),
as_dict=1,
dn_against_si = frappe.get_all(
"Delivery Note Item",
filters={"against_sales_invoice": ["in", list(self.invoices)]},
fields=["parent", "against_sales_invoice"],
distinct=True,
)
for d in dn_against_si:
@@ -476,14 +465,10 @@ class ReceivablePayableReport:
# Get Sales Team
if self.filters.show_sales_person:
# nosemgrep
sales_team = frappe.db.sql(
"""
select parent, sales_person
from `tabSales Team`
where parenttype = 'Sales Invoice'
""",
as_dict=1,
sales_team = frappe.get_all(
"Sales Team",
filters={"parenttype": "Sales Invoice"},
fields=["parent", "sales_person"],
)
for d in sales_team:
self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append(
@@ -548,22 +533,31 @@ class ReceivablePayableReport:
def get_payment_terms(self, row):
# build payment_terms for row
# nosemgrep
payment_terms_details = frappe.db.sql(
f"""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount,
ps.description, ps.paid_amount, ps.base_paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date
""",
row.voucher_no,
as_dict=1,
si = frappe.qb.DocType(row.voucher_type)
ps = frappe.qb.DocType("Payment Schedule")
payment_terms_details = (
frappe.qb.from_(si)
.inner_join(ps)
.on(si.name == ps.parent)
.select(
si.name,
si.party_account_currency,
si.currency,
si.conversion_rate,
si.total_advance,
ps.due_date,
ps.payment_term,
ps.payment_amount,
ps.base_payment_amount,
ps.description,
ps.paid_amount,
ps.base_paid_amount,
ps.discounted_amount,
)
.where((ps.parenttype == row.voucher_type) & (si.name == row.voucher_no) & (si.is_return == 0))
.orderby(ps.paid_amount, order=frappe.qb.desc)
.orderby(ps.due_date)
.run(as_dict=1)
)
original_row = frappe._dict(row)
@@ -661,7 +655,6 @@ class ReceivablePayableReport:
def get_future_payments_from_payment_entry(self):
pe = frappe.qb.DocType("Payment Entry")
pe_ref = frappe.qb.DocType("Payment Entry Reference")
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
return (
frappe.qb.from_(pe)
@@ -674,11 +667,14 @@ class ReceivablePayableReport:
(pe.posting_date).as_("future_date"),
(pe_ref.allocated_amount).as_("future_amount"),
(pe.reference_no).as_("future_ref"),
ifelse(
# CASE is portable; MySQL's IF() does not exist on postgres
query_builder.Case()
.when(
pe.payment_type == "Receive",
pe.source_exchange_rate * pe_ref.allocated_amount,
pe.target_exchange_rate * pe_ref.allocated_amount,
).as_("future_amount_in_base_currency"),
)
.else_(pe.target_exchange_rate * pe_ref.allocated_amount)
.as_("future_amount_in_base_currency"),
)
.where(
(pe.docstatus < 2)
@@ -695,11 +691,13 @@ class ReceivablePayableReport:
.inner_join(jea)
.on(jea.parent == je.name)
.select(
jea.reference_name.as_("invoice_no"),
jea.party,
jea.party_type,
je.posting_date.as_("future_date"),
je.cheque_no.as_("future_ref"),
# Sum() below makes this an implicit aggregate (no GROUP BY); the non-aggregated columns
# are arbitrary per the single group on MySQL -> Max() keeps it valid on postgres.
Max(jea.reference_name).as_("invoice_no"),
Max(jea.party).as_("party"),
Max(jea.party_type).as_("party_type"),
Max(je.posting_date).as_("future_date"),
Max(je.cheque_no).as_("future_ref"),
)
.where(
(je.docstatus < 2)
@@ -712,30 +710,25 @@ class ReceivablePayableReport:
if self.filters.get("party"):
if self.account_type == "Payable":
query = query.select(
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
future_amount = Sum(jea.debit_in_account_currency - jea.credit_in_account_currency)
future_amount_in_base_currency = Sum(jea.debit - jea.credit)
else:
query = query.select(
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
)
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
future_amount = Sum(jea.credit_in_account_currency - jea.debit_in_account_currency)
future_amount_in_base_currency = Sum(jea.credit - jea.debit)
else:
query = query.select(
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
"future_amount_in_base_currency"
)
)
query = query.select(
Sum(
jea.debit_in_account_currency
if self.account_type == "Payable"
else jea.credit_in_account_currency
).as_("future_amount")
future_amount_in_base_currency = Sum(jea.debit if self.account_type == "Payable" else jea.credit)
future_amount = Sum(
jea.debit_in_account_currency
if self.account_type == "Payable"
else jea.credit_in_account_currency
)
query = query.having(qb.Field("future_amount") > 0)
query = query.select(
future_amount.as_("future_amount"),
future_amount_in_base_currency.as_("future_amount_in_base_currency"),
)
# use the aggregate expression in HAVING; postgres can't reference a SELECT alias there
query = query.having(future_amount > 0)
return query.run(as_dict=True)
def allocate_future_payments(self, row):
@@ -891,16 +884,19 @@ class ReceivablePayableReport:
if self.filters.get("sales_person"):
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
# nosemgrep
records = frappe.db.sql(
"""
select distinct parent, parenttype
from `tabSales Team` steam
where parenttype in ('Customer', 'Sales Invoice')
and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person)
""",
(lft, rgt),
as_dict=1,
steam = frappe.qb.DocType("Sales Team")
sp = frappe.qb.DocType("Sales Person")
records = (
frappe.qb.from_(steam)
.select(steam.parent, steam.parenttype)
.distinct()
.where(
steam.parenttype.isin(["Customer", "Sales Invoice"])
& steam.sales_person.isin(
frappe.qb.from_(sp).select(sp.name).where((sp.lft >= lft) & (sp.rgt <= rgt))
)
)
.run(as_dict=1)
)
self.sales_person_records = frappe._dict()

View File

@@ -12,11 +12,17 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_usd_receivable_account()
self.clear_old_entries()
self.company = "_Test Company"
self.company_abbr = "_TC"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.cash = "Cash - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
frappe.set_user("Administrator")

View File

@@ -11,10 +11,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.maxDiff = None
self.create_company()
self.create_customer()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
def test_01_receivable_summary_output(self):
"""

View File

@@ -15,10 +15,7 @@ def execute(filters=None):
def get_data(filters):
data = []
depreciation_accounts = frappe.db.sql_list(
""" select name from tabAccount
where ifnull(account_type, '') = 'Depreciation' """
)
depreciation_accounts = frappe.get_all("Account", filters={"account_type": "Depreciation"}, pluck="name")
filters_data = [
["company", "=", filters.get("company")],
@@ -33,10 +30,8 @@ def get_data(filters):
filters_data.append(["against_voucher", "=", filters.get("asset")])
if filters.get("asset_category"):
assets = frappe.db.sql_list(
"""select name from tabAsset
where asset_category = %s and docstatus=1""",
filters.get("asset_category"),
assets = frappe.get_all(
"Asset", filters={"asset_category": filters.get("asset_category"), "docstatus": 1}, pluck="name"
)
filters_data.append(["against_voucher", "in", assets])

View File

@@ -0,0 +1,18 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from erpnext.accounts.report.asset_depreciation_ledger.asset_depreciation_ledger import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestAssetDepreciationLedger(ERPNextTestSuite):
def test_report_executes(self):
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
# both MariaDB and postgres.
company = frappe.db.get_value("Company", {}, "name")
columns, *_rest = execute(
frappe._dict({"company": company, "from_date": "2020-01-01", "to_date": "2030-12-31"})
)
self.assertTrue(columns)

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe.query_builder import CustomFunction
from frappe.query_builder.custom import MonthName
from frappe.utils import add_months, flt, formatdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -113,7 +113,6 @@ def build_budget_map(budget_records, filters):
def get_actual_transactions(dimension_name, filters):
budget_against = frappe.scrub(filters.get("budget_against"))
monthname = CustomFunction("MONTHNAME", ["date"])
gle = frappe.qb.DocType("GL Entry")
budget = frappe.qb.DocType("Budget")
@@ -126,7 +125,7 @@ def get_actual_transactions(dimension_name, filters):
gle.debit,
gle.credit,
gle.fiscal_year,
monthname(gle.posting_date).as_("month_name"),
MonthName(gle.posting_date).as_("month_name"),
budget[budget_against].as_("budget_against"),
)
.where(
@@ -137,7 +136,10 @@ def get_actual_transactions(dimension_name, filters):
& (gle.is_cancelled == 0)
& (budget[budget_against] == dimension_name)
)
.groupby(gle.name)
# budget[budget_against] is selected from the Budget table, which is not functionally
# dependent on the grouped GL Entry PK, so postgres requires it in the GROUP BY. The WHERE
# pins it to dimension_name (a constant), so grouping by it does not change the result.
.groupby(gle.name, budget[budget_against])
.orderby(gle.fiscal_year)
)
@@ -157,15 +159,11 @@ def get_actual_transactions(dimension_name, filters):
def get_budget_distributions(budget):
return frappe.db.sql(
"""
SELECT start_date, end_date, amount, percent
FROM `tabBudget Distribution`
WHERE parent = %s
ORDER BY start_date ASC
""",
(budget.name,),
as_dict=True,
return frappe.get_all(
"Budget Distribution",
filters={"parent": budget.name},
fields=["start_date", "end_date", "amount", "percent"],
order_by="start_date asc",
)
@@ -351,20 +349,16 @@ def get_columns(filters):
def get_fiscal_years(filters):
fiscal_year = frappe.db.sql(
"""
select
name
from
`tabFiscal Year`
where
name between %(from_fiscal_year)s and %(to_fiscal_year)s
""",
{"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]},
return frappe.get_all(
"Fiscal Year",
filters={"name": ["between", [filters["from_fiscal_year"], filters["to_fiscal_year"]]]},
fields=["name"],
# the raw query had no ORDER BY (de-facto oldest-first); get_all would otherwise apply the
# Fiscal Year doctype default (name DESC) and reverse column order / cumulative-mode values.
order_by="name asc",
as_list=True,
)
return fiscal_year
def get_cost_center_with_children(cost_centers):
"""Expand each cost center to include itself and all its descendants."""

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from erpnext.accounts.report.budget_variance_report.budget_variance_report import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestBudgetVarianceReport(ERPNextTestSuite):
def test_report_executes(self):
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
# both MariaDB and postgres.
company = frappe.db.get_value("Company", {}, "name")
fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc")
columns, *_rest = execute(
frappe._dict(
{
"company": company,
"from_fiscal_year": fy,
"to_fiscal_year": fy,
"period": "Yearly",
"budget_against": "Cost Center",
}
)
)
self.assertTrue(columns)

View File

@@ -7,6 +7,7 @@ from datetime import timedelta
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import cstr, flt
from pypika import Order
@@ -213,37 +214,43 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
def get_account_type_based_gl_data(company, filters=None):
cond = ""
filters = frappe._dict(filters or {})
gle = frappe.qb.DocType("GL Entry")
account = frappe.qb.DocType("Account")
query = (
frappe.qb.from_(gle)
.select(Sum(gle.credit) - Sum(gle.debit))
.where(
(gle.company == company)
& (gle.posting_date >= filters.start_date)
& (gle.posting_date <= filters.end_date)
& (gle.voucher_type != "Period Closing Voucher")
& gle.account.isin(
frappe.qb.from_(account)
.select(account.name)
.where(account.account_type == filters.account_type)
)
)
)
if filters.include_default_book_entries:
company_fb = frappe.get_cached_value("Company", company, "default_finance_book")
cond = """ AND (finance_book in ({}, {}, '') OR finance_book IS NULL)
""".format(
frappe.db.escape(filters.finance_book),
frappe.db.escape(company_fb),
query = query.where(
gle.finance_book.isin([filters.finance_book, company_fb, ""]) | gle.finance_book.isnull()
)
else:
cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % (
frappe.db.escape(cstr(filters.finance_book))
query = query.where(
gle.finance_book.isin([cstr(filters.finance_book), ""]) | gle.finance_book.isnull()
)
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
cond += " and cost_center in %(cost_center)s"
cost_centers = get_cost_centers_with_children(filters.cost_center)
query = query.where(gle.cost_center.isin(cost_centers))
gl_sum = frappe.db.sql_list(
f"""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
""",
filters,
)
return gl_sum[0] if gl_sum and gl_sum[0] else 0
gl_sum = query.run()
return gl_sum[0][0] if gl_sum and gl_sum[0][0] else 0
def get_start_date(period, accumulated_values, company):
@@ -367,11 +374,10 @@ def get_net_income(company, period_list, filters):
from_date, to_date = get_opening_range_using_fiscal_year(company, period_list)
for root_type in ["Income", "Expense"]:
for root in frappe.db.sql(
"""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""",
root_type,
as_dict=1,
for root in frappe.get_all(
"Account",
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
fields=["lft", "rgt"],
):
set_gl_entries_by_account(
company,

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from erpnext.accounts.report.cash_flow.cash_flow import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestCashFlow(ERPNextTestSuite):
def test_report_executes(self):
# Smoke-guards the raw-SQL -> query-builder port: the report query must compile and run on
# both MariaDB and postgres.
company = frappe.db.get_value("Company", {}, "name")
fy = frappe.db.get_value("Fiscal Year", {}, "name", order_by="year_start_date desc")
columns, *_rest = execute(
frappe._dict(
{
"company": company,
"from_fiscal_year": fy,
"to_fiscal_year": fy,
"filter_based_on": "Fiscal Year",
"periodicity": "Yearly",
}
)
)
self.assertTrue(columns)

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _, qb
from frappe.query_builder import CustomFunction
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
@@ -93,7 +93,6 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
.run(as_dict=1)
)
ifelse = CustomFunction("IF", ["condition", "then", "else"])
pe = qb.DocType("Payment Entry")
doctype_name = ConstantColumn("Payment Entry")
payments = (
@@ -101,7 +100,10 @@ def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filter
.select(
doctype_name.as_("doctype"),
pe.name,
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
Case()
.when(pe.paid_from.eq(filters.account), pe.paid_amount)
.else_(pe.received_amount)
.as_("amount"),
pe.payment_type,
pe.party_type,
pe.posting_date,

View File

@@ -0,0 +1,24 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.utils import nowdate
from erpnext.accounts.report.cheques_and_deposits_incorrectly_cleared.cheques_and_deposits_incorrectly_cleared import (
execute,
)
from erpnext.tests.utils import ERPNextTestSuite
class TestChequesAndDepositsIncorrectlyCleared(ERPNextTestSuite):
def test_report_executes_with_case_amount(self):
# Exercises the Payment Entry branch whose amount column uses a db-aware CASE expression
# (previously a MySQL-only IF()). IF() does not compile on postgres, so running the report
# query guards the portability fix on both databases.
company = frappe.db.get_value("Company", {}, "name")
account = frappe.db.get_value(
"Account", {"account_type": "Bank", "company": company, "is_group": 0}, "name"
)
columns, data = execute(frappe._dict({"account": account, "report_date": nowdate()}))
self.assertTrue(columns)
self.assertIsInstance(data, list)

View File

@@ -347,11 +347,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
filters.end_date = end_date
gl_entries_by_account = {}
for root in frappe.db.sql(
"""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""",
root_type,
as_dict=1,
for root in frappe.get_all(
"Account",
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
fields=["lft", "rgt"],
):
set_gl_entries_by_account(
start_date,
@@ -512,9 +511,11 @@ def get_companies(filters):
def get_subsidiary_companies(company):
lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"])
return frappe.db.sql_list(
f"""select name from `tabCompany`
where lft >= {lft} and rgt <= {rgt} order by lft, rgt"""
return frappe.get_all(
"Company",
filters={"lft": [">=", lft], "rgt": ["<=", rgt]},
pluck="name",
order_by="lft, rgt",
)
@@ -604,14 +605,10 @@ def set_gl_entries_by_account(
company_lft, company_rgt = frappe.get_cached_value("Company", filters.get("company"), ["lft", "rgt"])
companies = frappe.db.sql(
""" select name, default_currency from `tabCompany`
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
{
"company_lft": company_lft,
"company_rgt": company_rgt,
},
as_dict=1,
companies = frappe.get_all(
"Company",
filters={"lft": [">=", company_lft], "rgt": ["<=", company_rgt]},
fields=["name", "default_currency"],
)
currency_info = frappe._dict(

View File

@@ -126,12 +126,22 @@ def get_data(filters) -> list[list]:
def get_company_wise_tb_data(filters, reporting_currency, ignore_reporting_currency):
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, account_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
as_dict=True,
accounts = frappe.get_all(
"Account",
filters={"company": filters.company},
fields=[
"name",
"account_number",
"parent_account",
"account_name",
"root_type",
"report_type",
"account_type",
"is_group",
"lft",
"rgt",
],
order_by="lft",
)
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")

View File

@@ -11,10 +11,12 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestCustomerLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.cash = "Cash - _TC"
def create_sales_invoice(self, do_not_submit=False, **args):
si = create_sales_invoice(

View File

@@ -3,7 +3,8 @@
import frappe
from frappe import _, qb
from frappe.query_builder import Column, functions
from frappe.query_builder import functions
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, getdate, rounded
from erpnext.accounts.report.financial_statements import get_period_list
@@ -300,8 +301,10 @@ class Deferred_Revenue_and_Expense_Report:
Get all sales and purchase invoices which has deferred revenue/expense items
"""
gle = qb.DocType("GL Entry")
# column doesn't have an alias option
posted = Column("posted")
# a literal marker: real GL rows are "posted" (dummy/simulated future entries use "not").
# ConstantColumn renders a single-quoted string literal, valid on both backends -- a plain
# Column rendered as "posted", which MySQL reads as the string but postgres as an identifier.
posted = ConstantColumn("posted").as_("posted")
if self.filters.type == "Revenue":
inv = qb.DocType("Sales Invoice")
@@ -327,13 +330,15 @@ class Deferred_Revenue_and_Expense_Report:
)
.select(
inv.name.as_("doc"),
inv.posting_date,
# non-grouped columns are constant per grouped invoice / invoice item -> Max() keeps the
# GROUP BY valid on postgres while returning the same value MySQL picked.
functions.Max(inv.posting_date).as_("posting_date"),
inv_item.name.as_("item"),
inv_item.item_name,
inv_item.service_start_date,
inv_item.service_end_date,
inv_item.base_net_amount,
deferred_account_field,
functions.Max(inv_item.item_name).as_("item_name"),
functions.Max(inv_item.service_start_date).as_("service_start_date"),
functions.Max(inv_item.service_end_date).as_("service_end_date"),
functions.Max(inv_item.base_net_amount).as_("base_net_amount"),
functions.Max(deferred_account_field).as_(deferred_account_field.name),
gle.posting_date.as_("gle_posting_date"),
functions.Sum(gle.debit).as_("debit"),
functions.Sum(gle.credit).as_("credit"),

View File

@@ -61,11 +61,16 @@ class TestDeferredRevenueAndExpense(ERPNextTestSuite, AccountsTestMixin):
)
def setUp(self):
self.create_company()
self.create_customer("_Test Customer")
self.create_supplier("_Test Furniture Supplier")
self.company = "_Test Company"
self.company_abbr = "_TC"
self.customer = "_Test Customer"
self.supplier = "_Test Supplier"
self.warehouse = "Stores - _TC"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.setup_deferred_accounts_and_items()
self.clear_old_entries()
@ERPNextTestSuite.change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
def test_deferred_revenue(self):

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import cstr, flt
from frappe.utils import flt
import erpnext
from erpnext.accounts.report.financial_statements import (
@@ -31,18 +31,23 @@ def execute(filters=None):
def get_data(filters, dimension_list):
company_currency = erpnext.get_company_currency(filters.company)
acc = frappe.db.sql(
"""
select
name, account_number, parent_account, lft, rgt, root_type,
report_type, account_name, include_in_gross, account_type, is_group
from
`tabAccount`
where
company=%s
order by lft""",
(filters.company),
as_dict=True,
acc = frappe.get_all(
"Account",
filters={"company": filters.company},
fields=[
"name",
"account_number",
"parent_account",
"lft",
"rgt",
"root_type",
"report_type",
"account_name",
"include_in_gross",
"account_type",
"is_group",
],
order_by="lft",
)
if not acc:
@@ -50,16 +55,17 @@ def get_data(filters, dimension_list):
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
min_lft, max_rgt = frappe.db.sql(
"""select min(lft), max(rgt) from `tabAccount`
where company=%s""",
(filters.company),
lft_rgt = frappe.get_all(
"Account",
filters={"company": filters.company},
fields=[{"MIN": "lft", "as": "min_lft"}, {"MAX": "rgt", "as": "max_rgt"}],
)[0]
min_lft, max_rgt = lft_rgt.min_lft, lft_rgt.max_rgt
account = frappe.db.sql_list(
"""select name from `tabAccount`
where lft >= %s and rgt <= %s and company = %s""",
(min_lft, max_rgt, filters.company),
account = frappe.get_all(
"Account",
filters={"lft": [">=", min_lft], "rgt": ["<=", max_rgt], "company": filters.company},
pluck="name",
)
gl_entries_by_account = {}
@@ -75,42 +81,34 @@ def get_data(filters, dimension_list):
def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account):
condition = get_condition(filters.get("dimension"))
if account:
condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account]))
dimension_field = frappe.scrub(filters.get("dimension"))
gl_filters = {
"company": filters.get("company"),
"from_date": filters.get("from_date"),
"to_date": filters.get("to_date"),
"finance_book": cstr(filters.get("finance_book")),
dimension_field: ["in", list(set(dimension_list))],
"posting_date": ["between", [filters.get("from_date"), filters.get("to_date")]],
"is_cancelled": 0,
}
if account:
gl_filters["account"] = ["in", account]
gl_filters["dimensions"] = tuple(set(dimension_list))
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
gl_entries = frappe.db.sql(
"""
select
posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency
from
`tabGL Entry`
where
company=%(company)s
{condition}
and posting_date >= %(from_date)s
and posting_date <= %(to_date)s
and is_cancelled = 0
order by account, posting_date""".format(
dimension=frappe.scrub(filters.get("dimension")), condition=condition
),
gl_filters,
as_dict=True,
) # nosec
gl_entries = frappe.get_all(
"GL Entry",
filters=gl_filters,
fields=[
"posting_date",
"account",
dimension_field,
"debit",
"credit",
"is_opening",
"fiscal_year",
"debit_in_account_currency",
"credit_in_account_currency",
"account_currency",
],
order_by="account, posting_date",
)
for entry in gl_entries:
gl_entries_by_account.setdefault(entry.account, []).append(entry)
@@ -178,14 +176,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0)
def get_condition(dimension):
conditions = []
conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
return " and {}".format(" and ".join(conditions)) if conditions else ""
def get_dimensions(filters):
meta = frappe.get_meta(filters.get("dimension"), cached=False)
query_filters = {}

View File

@@ -71,6 +71,7 @@ def get_ratios_data(filters, period_list, years):
assets, liabilities, income, expense = get_gl_data(filters, period_list, years)
current_asset, total_asset = {}, {}
fixed_asset = {}
current_liability, total_liability = {}, {}
net_sales, total_income = {}, {}
cogs, total_expense = {}, {}
@@ -93,6 +94,7 @@ def get_ratios_data(filters, period_list, years):
quick_asset,
total_quick_asset,
],
[fixed_asset, total_asset, "Fixed Asset", year, assets, "Asset", {}, 0],
[
current_liability,
total_liability,
@@ -112,7 +114,7 @@ def get_ratios_data(filters, period_list, years):
add_solvency_ratios(
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
)
add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense)
add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense)
return data
@@ -193,7 +195,7 @@ def add_solvency_ratios(
data.append(return_on_equity_ratio)
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
def add_turnover_ratios(data, years, period_list, filters, fixed_asset, net_sales, cogs, direct_expense):
precision = frappe.db.get_single_value("System Settings", "float_precision")
data.append({"ratio": _("Turnover Ratios")})
@@ -208,7 +210,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
)
ratio_data = [
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
[_("Fixed Asset Turnover Ratio"), net_sales, fixed_asset],
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
[_("Inventory Turnover Ratio"), cogs, avg_stock],

View File

@@ -0,0 +1,73 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import frappe
from frappe.utils import today
from erpnext.accounts.report.financial_ratios.financial_ratios import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestFinancialRatios(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.abbr = "_TC"
# The report matches the group accounts by their account_type, which the
# standard chart of accounts does not set on group accounts by default.
self.set_account_type("Fixed Assets", "Fixed Asset")
self.set_account_type("Direct Income", "Direct Income")
def set_account_type(self, account_name, account_type):
frappe.db.set_value("Account", f"{account_name} - {self.abbr}", "account_type", account_type)
def test_fixed_asset_turnover_uses_net_fixed_assets(self):
# Acquire a fixed asset worth 10,000 funded by equity.
self.make_journal_entry("Buildings", "Capital Stock", 10000)
# Book sales of 20,000 collected in cash. Total assets now = 30,000
# (Buildings 10,000 + Cash 20,000), while net fixed assets stay at 10,000.
self.make_journal_entry("Cash", "Sales", 20000)
columns, data = execute(self.get_report_filters())
year_key = columns[1]["fieldname"]
ratio_row = next((row for row in data if row.get("ratio") == "Fixed Asset Turnover Ratio"), None)
self.assertIsNotNone(ratio_row, "Fixed Asset Turnover Ratio row not found in report output")
# Net Sales / Net Fixed Assets = 20,000 / 10,000 = 2.0
# (the old behaviour divided by total assets, giving 20,000 / 30,000 = 0.667)
self.assertEqual(ratio_row[year_key], 2.0)
def get_report_filters(self):
active_fy = frappe.db.get_value(
"Fiscal Year",
{"disabled": 0, "year_start_date": ("<=", today()), "year_end_date": (">=", today())},
["name", "year_start_date", "year_end_date"],
as_dict=True,
)
return frappe._dict(
company=self.company,
from_fiscal_year=active_fy.name,
to_fiscal_year=active_fy.name,
period_start_date=active_fy.year_start_date,
period_end_date=active_fy.year_end_date,
filter_based_on="Fiscal Year",
periodicity="Yearly",
)
def make_journal_entry(self, debit_account, credit_account, amount):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.posting_date = today()
journal_entry.company = self.company
for account, debit, credit in (
(debit_account, amount, 0),
(credit_account, 0, amount),
):
journal_entry.append(
"accounts",
{
"account": f"{account} - {self.abbr}",
"debit_in_account_currency": debit,
"credit_in_account_currency": credit,
},
)
journal_entry.insert()
journal_entry.submit()

View File

@@ -179,11 +179,10 @@ def get_data(
company_currency = get_appropriate_currency(company, filters)
gl_entries_by_account = {}
for root in frappe.db.sql(
"""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""",
root_type,
as_dict=1,
for root in frappe.get_all(
"Account",
filters={"root_type": root_type, "parent_account": ["is", "not set"]},
fields=["lft", "rgt"],
):
set_gl_entries_by_account(
company,
@@ -373,13 +372,23 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
def get_accounts(company, root_type):
return frappe.db.sql(
"""
select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt
from `tabAccount`
where company=%s and root_type=%s order by lft""",
(company, root_type),
as_dict=True,
return frappe.get_all(
"Account",
filters={"company": company, "root_type": root_type},
fields=[
"name",
"account_number",
"parent_account",
"lft",
"rgt",
"root_type",
"report_type",
"account_name",
"include_in_gross",
"account_type",
"is_group",
],
order_by="lft",
)
@@ -529,7 +538,11 @@ def get_accounting_entries(
gl_entry.credit_in_account_currency
if not group_by_account
else Sum(gl_entry.credit_in_account_currency).as_("credit_in_account_currency"),
gl_entry.account_currency,
# when grouping by account the non-aggregated columns must be aggregated for postgres;
# account_currency is constant per account so Max() returns the same value.
gl_entry.account_currency
if not group_by_account
else Max(gl_entry.account_currency).as_("account_currency"),
)
.where(gl_entry.company == filters.company)
)
@@ -547,15 +560,29 @@ def get_accounting_entries(
ignore_is_opening = frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting")
if doctype == "GL Entry":
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
# aggregate the non-grouped columns when grouping by account (postgres requirement)
if group_by_account:
query = query.select(
Max(gl_entry.posting_date).as_("posting_date"),
Max(gl_entry.is_opening).as_("is_opening"),
Max(gl_entry.fiscal_year).as_("fiscal_year"),
)
else:
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
query = query.force_index("posting_date_company_index")
# FORCE INDEX is MySQL-only; postgres has no index hints (its planner uses the index anyway)
if frappe.db.db_type != "postgres":
query = query.force_index("posting_date_company_index")
if ignore_opening_entries and not ignore_is_opening:
query = query.where(gl_entry.is_opening == "No")
else:
query = query.select(gl_entry.closing_date.as_("posting_date"))
query = query.select(
Max(gl_entry.closing_date).as_("posting_date")
if group_by_account
else gl_entry.closing_date.as_("posting_date")
)
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)

View File

@@ -12,7 +12,13 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestGeneralAndPaymentLedger(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.company = "_Test Company"
self.debit_to = "Debtors - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.cost_center = "Main - _TC"
self.income_account = "Sales - _TC"
self.warehouse = "Stores - _TC"
self.creditors = "Creditors - _TC"
self.cleanup()
def cleanup(self):

View File

@@ -35,7 +35,7 @@ def execute(filters=None):
if filters and filters.get("print_in_account_currency") and not filters.get("account"):
frappe.throw(_("Select an account to print in account currency"))
for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1):
for acc in frappe.get_all("Account", fields=["name", "is_group"]):
account_details.setdefault(acc.name, acc)
if filters.get("party"):
@@ -650,10 +650,8 @@ def get_result_as_list(data, filters):
def get_supplier_invoice_details():
inv_details = {}
for d in frappe.db.sql(
""" select name, bill_no from `tabPurchase Invoice`
where docstatus = 1 and bill_no is not null and bill_no != '' """,
as_dict=1,
for d in frappe.get_all(
"Purchase Invoice", filters={"docstatus": 1, "bill_no": ["is", "set"]}, fields=["name", "bill_no"]
):
inv_details[d.name] = d.bill_no

View File

@@ -14,7 +14,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestGeneralLedger(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.clear_old_entries()
def clear_old_entries(self):
doctype_list = [

View File

@@ -713,20 +713,25 @@ class GrossProfitGenerator:
)
def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql(
"""
select
si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against
from
`tabSales Invoice` si, `tabSales Invoice Item` si_item
where
si.name = si_item.parent
and si.docstatus = 1
and si.is_return = 1
and si.posting_date between %(from_date)s and %(to_date)s
""",
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
as_dict=1,
si = frappe.qb.DocType("Sales Invoice")
si_item = frappe.qb.DocType("Sales Invoice Item")
returned_invoices = (
frappe.qb.from_(si)
.inner_join(si_item)
.on(si.name == si_item.parent)
.select(
si.name,
si_item.item_code,
si_item.stock_qty.as_("qty"),
si_item.base_net_amount.as_("base_amount"),
si.return_against,
)
.where(
(si.docstatus == 1)
& (si.is_return == 1)
& si.posting_date.between(self.filters.from_date, self.filters.to_date)
)
.run(as_dict=1)
)
self.returned_invoices = frappe._dict()
@@ -1241,7 +1246,4 @@ class GrossProfitGenerator:
).setdefault(d.parent_item, []).append(d)
def load_non_stock_items(self):
self.non_stock_items = frappe.db.sql_list(
"""select name from tabItem
where is_stock_item=0"""
)
self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name")

View File

@@ -14,73 +14,17 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestGrossProfit(ERPNextTestSuite):
def setUp(self):
self.create_company()
self.create_item()
self.create_bundle()
self.create_customer()
self.create_sales_invoice()
self.clear_old_entries()
def create_company(self):
company_name = "_Test Gross Profit"
abbr = "_GP"
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.creditors = "Creditors - " + abbr
def create_item(self):
item = create_item(
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
item2 = create_item(
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item2 = item2 if isinstance(item2, str) else item2.item_code
# This will be parent item
bundle = create_item(
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
# Create Product Bundle
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
def create_customer(self):
name = "_Test GP Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.finished_warehouse = "Finished Goods - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.item = "_Test Item"
self.item2 = "_Test Item Home Desktop 100"
self.bundle = "_Test Product Bundle Item"
self.customer = "_Test Customer"
def create_sales_invoice(
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
@@ -214,7 +158,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"warehouse": "Stores - _TC",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 150.0,
@@ -243,7 +187,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"warehouse": "Stores - _TC",
"qty": 1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 100.0,
@@ -275,7 +219,7 @@ class TestGrossProfit(ERPNextTestSuite):
"item_code": self.item2,
"s_warehouse": "",
"t_warehouse": self.finished_warehouse,
"qty": 1,
"qty": 2,
"basic_rate": 100,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
@@ -375,7 +319,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"warehouse": "Stores - _TC",
"qty": 4.0,
"avg._selling_rate": 100.0,
"valuation_rate": 125.0,
@@ -416,10 +360,10 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"warehouse": "Stores - _TC",
"qty": 0.0,
"avg._selling_rate": 100,
"valuation_rate": 0.0,
"avg._selling_rate": 100.0,
"valuation_rate": 100.0,
"selling_amount": 0.0,
"buying_amount": 0.0,
"gross_profit": 0.0,
@@ -439,7 +383,7 @@ class TestGrossProfit(ERPNextTestSuite):
"""
# Make Cr Note
sinv = self.create_sales_invoice(
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
qty=-1, rate=200, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv.items[0].allow_zero_valuation_rate = 1
@@ -462,14 +406,14 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"warehouse": "Stores - _TC",
"qty": -1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"avg._selling_rate": 200.0,
"valuation_rate": 100.0,
"selling_amount": -200.0,
"buying_amount": -100.0,
"gross_profit": -100.0,
"gross_profit_%": -100.0,
"gross_profit_%": -50.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
report_output = {k: v for k, v in gp_entry[0].items() if k in expected_entry}
@@ -555,7 +499,7 @@ class TestGrossProfit(ERPNextTestSuite):
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"warehouse": "Stores - _TC",
"qty": 4.0,
"avg._selling_rate": 800.0,
"valuation_rate": 700.0,
@@ -618,7 +562,7 @@ class TestGrossProfit(ERPNextTestSuite):
def test_gross_profit_groupby_invoices(self):
create_sales_invoice(
qty=1,
rate=100,
rate=200,
company=self.company,
customer=self.customer,
item_code=self.item,
@@ -640,10 +584,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.selling_amount, 200.0)
self.assertEqual(total.buying_amount, 100.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
self.assertEqual(total.get("gross_profit_%"), 50.0)
def test_profit_for_later_period_return(self):
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
@@ -652,7 +596,7 @@ class TestGrossProfit(ERPNextTestSuite):
return_inv_date = add_days(month_end_date, 1)
# create sales invoice on month start date
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
sinv = self.create_sales_invoice(qty=1, rate=200, do_not_save=True, do_not_submit=True)
sinv.set_posting_time = 1
sinv.posting_date = sales_inv_date
sinv.save().submit()
@@ -671,10 +615,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.selling_amount, 200.0)
self.assertEqual(total.buying_amount, 100.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)
self.assertEqual(total.get("gross_profit_%"), 50.0)
# extend filters upto returned period
filters.update({"to_date": return_inv_date})
@@ -692,10 +636,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, -100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.selling_amount, -200.0)
self.assertEqual(total.buying_amount, -100.0)
self.assertEqual(total.gross_profit, -100.0)
self.assertEqual(total.get("gross_profit_%"), -100.0)
self.assertEqual(total.get("gross_profit_%"), -50.0)
def test_sales_person_wise_gross_profit(self):
sales_person = make_sales_person("_Test Sales Person")
@@ -726,10 +670,10 @@ class TestGrossProfit(ERPNextTestSuite):
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total[5], 1000.0)
self.assertEqual(total[6], 0.0)
self.assertEqual(total[7], 1000.0)
self.assertEqual(total[8], 100.0)
self.assertEqual(total[5], 1000.0) # selling amount
self.assertEqual(total[6], 1000.0) # buying amount
self.assertEqual(total[7], 0.0) # gross profit
self.assertEqual(total[8], 0.0) # gross profit %
def test_drop_ship(self):
from erpnext.buying.doctype.purchase_order.mapper import make_purchase_invoice

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder import CustomFunction
from frappe.query_builder.functions import CurDate, DateDiff
from frappe.utils import cint
@@ -102,11 +102,11 @@ def get_sales_details(filters):
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
child = frappe.qb.DocType(child_doctype)
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
current_date = CustomFunction("CURRENT_DATE", [])
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
days_since_last_order = date_diff(current_date(), date_col)
# DateDiff is cross-database (DATEDIFF on MariaDB, date subtraction on postgres); CurDate()
# renders the bare CURRENT_DATE keyword. Yields the integer number of days.
days_since_last_order = DateDiff(CurDate(), date_col)
sales_data = (
frappe.qb.from_(parent)

View File

@@ -0,0 +1,32 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.report.inactive_sales_items.inactive_sales_items import execute
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestSuite
class TestInactiveSalesItems(ERPNextTestSuite):
def test_days_since_last_order_is_computed(self):
# Exercises the date-arithmetic path (DATEDIFF/CURRENT_DATE on mariadb, date subtraction on
# postgres) which must produce the same integer day count on both databases.
item = make_item("_Test Inactive Sales Item").name
old_date = add_days(today(), -120)
so = make_sales_order(item=item, qty=3, rate=150, transaction_date=old_date)
so.items[0].delivery_date = add_days(old_date, 7)
so.save()
so.submit()
columns, data = execute(frappe._dict({"based_on": "Sales Order", "days": 30}))
self.assertTrue(columns)
row = next((r for r in data if r.get("item") == item and r.get("days_since_last_order")), None)
self.assertIsNotNone(row, "Inactive item should appear in the report")
self.assertGreaterEqual(row["days_since_last_order"], 30)
def test_report_runs_for_sales_invoice(self):
columns, _data = execute(frappe._dict({"based_on": "Sales Invoice", "days": 30}))
self.assertTrue(columns)

View File

@@ -376,7 +376,7 @@ def get_items(filters, additional_table_columns):
def get_aii_accounts():
return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany"))
return dict(frappe.get_all("Company", fields=["name", "stock_received_but_not_billed"], as_list=True))
def get_purchase_receipts_against_purchase_order(item_list):
@@ -384,16 +384,11 @@ def get_purchase_receipts_against_purchase_order(item_list):
po_item_rows = list(set(d.po_detail for d in item_list))
if po_item_rows:
purchase_receipts = frappe.db.sql(
"""
select parent, purchase_order_item
from `tabPurchase Receipt Item`
where docstatus=1 and purchase_order_item in (%s)
group by purchase_order_item, parent
"""
% (", ".join(["%s"] * len(po_item_rows))),
tuple(po_item_rows),
as_dict=1,
purchase_receipts = frappe.get_all(
"Purchase Receipt Item",
filters={"docstatus": 1, "purchase_order_item": ["in", po_item_rows]},
fields=["parent", "purchase_order_item"],
group_by="purchase_order_item, parent",
)
for pr in purchase_receipts:

View File

@@ -9,9 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWisePurchaseRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_supplier()
self.create_item()
self.company = "_Test Company"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
def create_purchase_invoice(self, do_not_submit=False):
pi = make_purchase_invoice(

View File

@@ -9,9 +9,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
si = create_sales_invoice(

View File

@@ -9,42 +9,12 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPaymentLedger(ERPNextTestSuite):
def setUp(self):
self.create_company()
self.cleanup()
def cleanup(self):
doctypes = []
doctypes.append(qb.DocType("GL Entry"))
doctypes.append(qb.DocType("Payment Ledger Entry"))
doctypes.append(qb.DocType("Sales Invoice"))
doctypes.append(qb.DocType("Payment Entry"))
for doctype in doctypes:
qb.from_(doctype).delete().where(doctype.company == self.company).run()
def create_company(self):
name = "Test Payment Ledger"
company = None
if frappe.db.exists("Company", name):
company = frappe.get_doc("Company", name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses" + " - " + company.abbr
self.income_account = company.default_income_account
self.expense_account = company.default_expense_account
self.debit_to = company.default_receivable_account
self.company = "_Test Company"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
def test_unpaid_invoice_outstanding(self):
sinv = create_sales_invoice(

View File

@@ -4,6 +4,8 @@
import frappe
from frappe import _
from frappe.query_builder import Case
from frappe.query_builder.functions import IfNull
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
@@ -46,40 +48,47 @@ def execute(filters=None):
def get_pos_entries(filters, group_by_field):
conditions = get_conditions(filters)
order_by = "p.posting_date"
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
if group_by_field == "mode_of_payment":
select_mop_field = (
", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount"
p = frappe.qb.DocType("POS Invoice")
query = (
frappe.qb.from_(p)
.select(
p.posting_date,
p.name.as_("pos_invoice"),
p.pos_profile,
p.company,
p.owner,
p.customer,
p.is_return,
p.base_grand_total.as_("grand_total"),
)
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND"
order_by += ", sip.mode_of_payment"
elif group_by_field:
order_by += f", p.{group_by_field}"
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
# nosemgrep
return frappe.db.sql(
f"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile, p.company,
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
WHERE
p.docstatus = 1 and
{group_by_mop_condition}
{conditions}
ORDER BY
{order_by}
""",
filters,
as_dict=1,
.where(p.docstatus == 1)
)
for condition in get_conditions(filters, p):
query = query.where(condition)
if group_by_field == "mode_of_payment":
sip = frappe.qb.DocType("Sales Invoice Payment")
paid_amount = sip.base_amount - Case().when(sip.type == "Cash", p.change_amount).else_(0)
query = (
query.inner_join(sip)
.on(sip.parent == p.name)
.select(sip.mode_of_payment, paid_amount.as_("paid_amount"))
.where(IfNull(paid_amount, 0) != 0)
.orderby(p.posting_date)
.orderby(sip.mode_of_payment)
)
elif group_by_field:
query = (
query.select((p.base_paid_amount - p.change_amount).as_("paid_amount"))
.orderby(p.posting_date)
.orderby(p[group_by_field])
)
else:
query = query.orderby(p.posting_date)
return query.run(as_dict=1)
def concat_mode_of_payments(pos_entries):
mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
@@ -127,27 +136,34 @@ def validate_filters(filters):
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
def get_conditions(filters):
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s"
def get_conditions(filters, p):
conditions = [
p.company == filters.get("company"),
p.posting_date >= filters.get("from_date"),
p.posting_date <= filters.get("to_date"),
]
if filters.get("pos_profile"):
conditions += " AND pos_profile = %(pos_profile)s"
conditions.append(p.pos_profile == filters.get("pos_profile"))
if filters.get("owner"):
conditions += " AND owner = %(owner)s"
conditions.append(p.owner == filters.get("owner"))
if filters.get("customer"):
conditions += " AND customer = %(customer)s"
conditions.append(p.customer == filters.get("customer"))
if filters.get("is_return"):
conditions += " AND is_return = %(is_return)s"
conditions.append(p.is_return == filters.get("is_return"))
if filters.get("mode_of_payment"):
conditions += """
AND EXISTS(
SELECT name FROM `tabSales Invoice Payment` sip
WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s
)"""
sip = frappe.qb.DocType("Sales Invoice Payment")
conditions.append(
p.name.isin(
frappe.qb.from_(sip)
.select(sip.parent)
.where(IfNull(sip.mode_of_payment, "") == filters.get("mode_of_payment"))
)
)
return conditions

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.report.pos_register.pos_register import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestPOSRegister(ERPNextTestSuite):
def test_report_executes(self):
# Smoke-guards the raw-SQL -> query-builder port: the report's POS Invoice query must
# compile and run on both MariaDB and postgres (it returns columns + a row list either way).
company = frappe.db.get_value("Company", {}, "name")
columns, data = execute(
frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()})
)
self.assertTrue(columns)
self.assertIsInstance(data, list)

View File

@@ -14,9 +14,11 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestProfitAndLossStatement(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
def create_sales_invoice(self, qty=1, rate=150, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _, msgprint
from frappe.query_builder import Case
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from pypika.terms import Bracket, LiteralValue, Order
@@ -307,14 +309,17 @@ def get_account_columns(invoice_list, include_payments):
unrealized_profit_loss_account_columns = []
if invoice_list:
expense_accounts = frappe.db.sql_list(
"""select distinct expense_account
from `tabPurchase Invoice Item` where docstatus = 1
and (expense_account is not null and expense_account != '')
and parenttype='Purchase Invoice'
and parent in (%s) order by expense_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple([inv.name for inv in invoice_list]),
expense_accounts = frappe.get_all(
"Purchase Invoice Item",
filters={
"docstatus": 1,
"expense_account": ["is", "set"],
"parenttype": "Purchase Invoice",
"parent": ["in", [inv.name for inv in invoice_list]],
},
pluck="expense_account",
distinct=True,
order_by="expense_account",
)
purchase_taxes_query = get_taxes_query(invoice_list, "Purchase Taxes and Charges", "Purchase Invoice")
@@ -326,13 +331,16 @@ def get_account_columns(invoice_list, include_payments):
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.db.sql_list(
"""SELECT distinct unrealized_profit_loss_account
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
unrealized_profit_loss_accounts = frappe.get_all(
"Purchase Invoice",
filters={
"docstatus": 1,
"name": ["in", [inv.name for inv in invoice_list]],
"unrealized_profit_loss_account": ["is", "set"],
},
pluck="unrealized_profit_loss_account",
distinct=True,
order_by="unrealized_profit_loss_account",
)
for account in expense_accounts:
@@ -454,16 +462,11 @@ def get_payments(filters):
def get_invoice_expense_map(invoice_list):
expense_details = frappe.db.sql(
"""
select parent, expense_account, sum(base_net_amount) as amount
from `tabPurchase Invoice Item`
where parent in (%s) and parenttype='Purchase Invoice'
group by parent, expense_account
"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
expense_details = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Purchase Invoice"},
fields=["parent", "expense_account", {"SUM": "base_net_amount", "as": "amount"}],
group_by="parent, expense_account",
)
invoice_expense_map = {}
@@ -475,13 +478,16 @@ def get_invoice_expense_map(invoice_list):
def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql(
"""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
and is_internal_supplier = 1 and company = represents_company"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
pi = frappe.qb.DocType("Purchase Invoice")
unrealized_amount_details = (
frappe.qb.from_(pi)
.select(pi.name, pi.unrealized_profit_loss_account, pi.base_net_total.as_("amount"))
.where(
pi.name.isin([inv.name for inv in invoice_list])
& (pi.is_internal_supplier == 1)
& (pi.company == pi.represents_company)
)
.run(as_dict=1)
)
internal_invoice_map = {}
@@ -493,18 +499,23 @@ def get_internal_invoice_map(invoice_list):
def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, include_payments=False):
tax_details = frappe.db.sql(
"""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
ptc = frappe.qb.DocType("Purchase Taxes and Charges")
tax_amount = (
Case()
.when(ptc.add_deduct_tax == "Add", Sum(ptc.base_tax_amount_after_discount_amount))
.else_(Sum(ptc.base_tax_amount_after_discount_amount) * -1)
)
tax_details = (
frappe.qb.from_(ptc)
.select(ptc.parent, ptc.account_head, tax_amount.as_("tax_amount"))
.where(
ptc.parent.isin([inv.name for inv in invoice_list])
& ptc.category.isin(["Total", "Valuation and Total"])
& (ptc.base_tax_amount_after_discount_amount != 0)
& (ptc.parenttype == "Purchase Invoice")
)
.groupby(ptc.parent, ptc.account_head, ptc.add_deduct_tax)
.run(as_dict=1)
)
if include_payments:
@@ -525,15 +536,10 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
def get_invoice_po_pr_map(invoice_list):
pi_items = frappe.db.sql(
"""
select parent, purchase_order, purchase_receipt, po_detail, project
from `tabPurchase Invoice Item`
where parent in (%s) and parenttype='Purchase Invoice'
"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
pi_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Purchase Invoice"},
fields=["parent", "purchase_order", "purchase_receipt", "po_detail", "project"],
)
invoice_po_pr_map = {}
@@ -547,10 +553,11 @@ def get_invoice_po_pr_map(invoice_list):
if d.purchase_receipt:
pr_list = [d.purchase_receipt]
elif d.po_detail:
pr_list = frappe.db.sql_list(
"""select distinct parent from `tabPurchase Receipt Item`
where docstatus=1 and purchase_order_item=%s""",
d.po_detail,
pr_list = frappe.get_all(
"Purchase Receipt Item",
filters={"docstatus": 1, "purchase_order_item": d.po_detail},
pluck="parent",
distinct=True,
)
if pr_list:
@@ -565,12 +572,8 @@ def get_invoice_po_pr_map(invoice_list):
def get_account_details(invoice_list):
account_map = {}
accounts = list(set([inv.credit_to for inv in invoice_list]))
for acc in frappe.db.sql(
"""select name, parent_account from tabAccount
where name in (%s)"""
% ", ".join(["%s"] * len(accounts)),
tuple(accounts),
as_dict=1,
for acc in frappe.get_all(
"Account", filters={"name": ["in", accounts]}, fields=["name", "parent_account"]
):
account_map[acc.name] = acc.parent_account

View File

@@ -346,12 +346,15 @@ def get_account_columns(invoice_list, include_payments):
unrealized_profit_loss_account_columns = []
if invoice_list:
income_accounts = frappe.db.sql_list(
"""select distinct income_account
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
order by income_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
# frappe drops ORDER BY for distinct queries on postgres (db_query), so sort in python to keep
# the generated account-column order deterministic and identical on both backends.
income_accounts = sorted(
frappe.get_all(
"Sales Invoice Item",
filters={"docstatus": 1, "parent": ["in", [inv.name for inv in invoice_list]]},
pluck="income_account",
distinct=True,
)
)
sales_taxes_query = get_taxes_query(invoice_list, "Sales Taxes and Charges", "Sales Invoice")
@@ -363,14 +366,18 @@ def get_account_columns(invoice_list, include_payments):
advance_tax_accounts = advance_taxes_query.run(as_dict=True, pluck="account_head")
tax_accounts = set(tax_accounts + advance_tax_accounts)
unrealized_profit_loss_accounts = frappe.db.sql_list(
"""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s)
and is_internal_customer = 1
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
unrealized_profit_loss_accounts = sorted(
frappe.get_all(
"Sales Invoice",
filters={
"docstatus": 1,
"name": ["in", [inv.name for inv in invoice_list]],
"is_internal_customer": 1,
"unrealized_profit_loss_account": ["is", "set"],
},
pluck="unrealized_profit_loss_account",
distinct=True,
)
)
for account in income_accounts:
@@ -494,12 +501,11 @@ def get_payments(filters):
def get_invoice_income_map(invoice_list):
income_details = frappe.db.sql(
"""select parent, income_account, sum(base_net_amount) as amount
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
income_details = frappe.get_all(
"Sales Invoice Item",
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
fields=["parent", "income_account", {"SUM": "base_net_amount", "as": "amount"}],
group_by="parent, income_account",
)
invoice_income_map = {}
@@ -511,13 +517,16 @@ def get_invoice_income_map(invoice_list):
def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql(
"""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabSales Invoice` where name in (%s)
and is_internal_customer = 1 and company = represents_company"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
si = frappe.qb.DocType("Sales Invoice")
unrealized_amount_details = (
frappe.qb.from_(si)
.select(si.name, si.unrealized_profit_loss_account, si.base_net_total.as_("amount"))
.where(
si.name.isin([inv.name for inv in invoice_list])
& (si.is_internal_customer == 1)
& (si.company == si.represents_company)
)
.run(as_dict=1)
)
internal_invoice_map = {}
@@ -529,14 +538,15 @@ def get_internal_invoice_map(invoice_list):
def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, include_payments=False):
tax_details = frappe.db.sql(
"""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
group by parent, account_head"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
tax_details = frappe.get_all(
"Sales Taxes and Charges",
filters={"parent": ["in", [inv.name for inv in invoice_list]], "parenttype": "Sales Invoice"},
fields=[
"parent",
"account_head",
{"SUM": "base_tax_amount_after_discount_amount", "as": "tax_amount"},
],
group_by="parent, account_head",
)
if include_payments:
@@ -557,13 +567,11 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
def get_invoice_so_dn_map(invoice_list):
si_items = frappe.db.sql(
"""select parent, sales_order, delivery_note, so_detail
from `tabSales Invoice Item` where parent in (%s)
and (sales_order != '' or delivery_note != '')"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
si_items = frappe.get_all(
"Sales Invoice Item",
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
or_filters=[["sales_order", "!=", ""], ["delivery_note", "!=", ""]],
fields=["parent", "sales_order", "delivery_note", "so_detail"],
)
invoice_so_dn_map = {}
@@ -577,10 +585,11 @@ def get_invoice_so_dn_map(invoice_list):
if d.delivery_note:
delivery_note_list = [d.delivery_note]
elif d.sales_order:
delivery_note_list = frappe.db.sql_list(
"""select distinct parent from `tabDelivery Note Item`
where docstatus=1 and so_detail=%s""",
d.so_detail,
delivery_note_list = frappe.get_all(
"Delivery Note Item",
filters={"docstatus": 1, "so_detail": d.so_detail},
pluck="parent",
distinct=True,
)
if delivery_note_list:
@@ -592,13 +601,11 @@ def get_invoice_so_dn_map(invoice_list):
def get_invoice_cc_wh_map(invoice_list):
si_items = frappe.db.sql(
"""select parent, cost_center, warehouse
from `tabSales Invoice Item` where parent in (%s)
and (cost_center != '' or warehouse != '')"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,
si_items = frappe.get_all(
"Sales Invoice Item",
filters={"parent": ["in", [inv.name for inv in invoice_list]]},
or_filters=[["cost_center", "!=", ""], ["warehouse", "!=", ""]],
fields=["parent", "cost_center", "warehouse"],
)
invoice_cc_wh_map = {}
@@ -619,12 +626,11 @@ def get_invoice_cc_wh_map(invoice_list):
def get_mode_of_payments(invoice_list):
mode_of_payments = {}
if invoice_list:
inv_mop = frappe.db.sql(
"""select parent, mode_of_payment
from `tabSales Invoice Payment` where parent in (%s) group by parent, mode_of_payment"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(invoice_list),
as_dict=1,
inv_mop = frappe.get_all(
"Sales Invoice Payment",
filters={"parent": ["in", list(invoice_list)]},
fields=["parent", "mode_of_payment"],
group_by="parent, mode_of_payment",
)
for d in inv_mop:

View File

@@ -10,9 +10,13 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.company = "_Test Company"
self.customer = "_Test Customer"
self.item = "_Test Item"
self.debit_to = "Debtors - _TC"
self.cost_center = "Main - _TC"
self.income_account = "Sales - _TC"
self.cash = "Cash - _TC"
self.create_child_cost_center()
def create_child_cost_center(self):

View File

@@ -62,15 +62,10 @@ def get_columns(filters):
def get_all_transfers(date, shareholder):
condition = " "
# if company:
# condition = 'AND company = %(company)s '
return frappe.db.sql(
f"""SELECT * FROM `tabShare Transfer`
WHERE ((DATE(date) <= %(date)s AND from_shareholder = %(shareholder)s {condition})
OR (DATE(date) <= %(date)s AND to_shareholder = %(shareholder)s {condition}))
AND docstatus = 1
ORDER BY date""",
{"date": date, "shareholder": shareholder},
as_dict=1,
return frappe.get_all(
"Share Transfer",
filters={"date": ["<=", date], "docstatus": 1},
or_filters=[["from_shareholder", "=", shareholder], ["to_shareholder", "=", shareholder]],
fields=["*"],
order_by="date",
)

View File

@@ -9,10 +9,9 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestSupplierLedgerSummary(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.create_supplier()
self.create_item()
self.clear_old_entries()
self.company = "_Test Company"
self.supplier = "_Test Supplier"
self.item = "_Test Item"
def create_purchase_invoice(self, do_not_submit=False):
frappe.set_user("Administrator")

View File

@@ -20,8 +20,7 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.clear_old_entries()
self.company = "_Test Company"
create_records()
def test_tax_withholding_for_customers(self):

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Max, Sum
from frappe.utils import add_days, cstr, flt, formatdate, getdate
import erpnext
@@ -82,12 +82,21 @@ def validate_filters(filters):
def get_data(filters):
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, is_group, lft, rgt
from `tabAccount` where company=%s order by lft""",
filters.company,
as_dict=True,
accounts = frappe.get_all(
"Account",
filters={"company": filters.company},
fields=[
"name",
"account_number",
"parent_account",
"account_name",
"root_type",
"report_type",
"is_group",
"lft",
"rgt",
],
order_by="lft",
)
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
@@ -240,7 +249,8 @@ def get_opening_balance(
frappe.qb.from_(closing_balance)
.select(
closing_balance.account,
closing_balance.account_currency,
# account_currency is constant per grouped account -> Max() keeps the GROUP BY postgres-valid
Max(closing_balance.account_currency).as_("account_currency"),
Sum(closing_balance.debit).as_("debit"),
Sum(closing_balance.credit).as_("credit"),
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),

View File

@@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Max, Sum
def execute(filters=None):
@@ -43,7 +43,13 @@ def get_data(filters):
gle = frappe.qb.DocType("GL Entry")
query = (
frappe.qb.from_(gle)
.select(gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit"))
# voucher_type is constant per grouped voucher_no -> Max() keeps the GROUP BY valid on postgres
.select(
Max(gle.voucher_type).as_("voucher_type"),
gle.voucher_no,
Sum(gle.debit).as_("debit"),
Sum(gle.credit).as_("credit"),
)
.where(gle.is_cancelled == 0)
.groupby(gle.voucher_no)
)

View File

@@ -12,7 +12,7 @@ import frappe
from frappe import _
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
from frappe.query_builder.functions import Abs, Max, Sum
from frappe.utils import flt
import erpnext
@@ -150,7 +150,7 @@ def calculate_total_advance_from_ledger(doc) -> list:
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
return (
frappe.qb.from_(adv)
.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
.select(Abs(Sum(adv.amount)).as_("amount"), Max(adv.currency).as_("account_currency"))
.where(adv.company == doc.company)
.where(adv.delinked == 0)
.where(adv.against_voucher_type == doc.doctype)

View File

@@ -141,7 +141,7 @@ def make_exchange_gain_loss_journal(
.where(
(je.docstatus == 1)
& (je.name.isin(parents))
& (je.voucher_type == "Exchange Gain or Loss")
& (je.voucher_type == "Exchange Gain Or Loss")
)
.run()
)

View File

@@ -200,7 +200,6 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
@frappe.whitelist()
def get_balance_on(
account: str | None = None,
date: DateTimeLikeObject | None = None,

View File

@@ -96,7 +96,9 @@ def get_depreciable_assets_data(date):
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
.where(ds.journal_entry.isnull())
.where(ds.schedule_date <= date)
.groupby(ads.name)
# a.name/a.creation are constant per ads.name; include them so postgres accepts the
# SELECT and ORDER BY (one row per Asset Depreciation Schedule either way)
.groupby(ads.name, a.name, a.creation)
.orderby(a.creation, order=Order.desc)
)

View File

@@ -7,7 +7,7 @@ import sys
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import DateDiff, Sum
from frappe.utils import getdate
@@ -68,7 +68,7 @@ def get_item_workdays(scorecard):
frappe.qb.from_(PO_Item)
.join(PO)
.on(PO_Item.parent == PO.name)
.select(Sum(frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
.select(Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty)))
.where(PO.supplier == scorecard.supplier)
.where(PO_Item.received_qty < PO_Item.qty)
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date]) # Équivalent du BETWEEN
@@ -153,7 +153,7 @@ def get_total_days_late(scorecard):
.on(PR_Item.purchase_order_item == PO_Item.name)
.join(PO)
.on(PO_Item.parent == PO.name)
.select(Sum(frappe.qb.fn.DATEDIFF(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
.select(Sum(DateDiff(PR.posting_date, PO_Item.schedule_date) * PR_Item.qty))
.where(PO.supplier == scorecard.supplier)
.where(PO_Item.schedule_date[scorecard.start_date : scorecard.end_date])
.where(PO_Item.schedule_date < PR.posting_date)
@@ -170,10 +170,7 @@ def get_total_days_late(scorecard):
.join(PO)
.on(PO_Item.parent == PO.name)
.select(
Sum(
frappe.qb.fn.DATEDIFF(scorecard.end_date, PO_Item.schedule_date)
* (PO_Item.qty - PO_Item.received_qty)
)
Sum(DateDiff(scorecard.end_date, PO_Item.schedule_date) * (PO_Item.qty - PO_Item.received_qty))
)
.where(PO.supplier == scorecard.supplier)
.where(PO_Item.received_qty < PO_Item.qty)
@@ -530,7 +527,7 @@ def get_rfq_response_days(scorecard):
.on(sq_item.request_for_quotation_item == rfq_item.name)
.join(sq)
.on(sq_item.parent == sq.name)
.select(frappe.qb.fn.Sum(frappe.qb.fn.Datediff(sq.transaction_date, rfq.transaction_date)))
.select(frappe.qb.fn.Sum(DateDiff(sq.transaction_date, rfq.transaction_date)))
.where(rfq_sup.supplier == scorecard.supplier)
.where(sq.supplier == scorecard.supplier)
.where(rfq.transaction_date[scorecard.start_date : scorecard.end_date])

View File

@@ -6,7 +6,7 @@ import copy
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce, Sum
from frappe.query_builder.functions import Coalesce, Max, Sum
from frappe.utils import cint, date_diff, flt, getdate
@@ -44,13 +44,15 @@ def get_data(filters):
.on(mr_item.parent == mr.name)
.select(
mr.name.as_("material_request"),
mr.transaction_date.as_("date"),
mr_item.schedule_date.as_("required_date"),
# non-grouped columns are constant per grouped mr.name / item_code -> Max() keeps the
# GROUP BY valid on postgres while returning the same value MySQL picked.
Max(mr.transaction_date).as_("date"),
Max(mr_item.schedule_date).as_("required_date"),
mr_item.item_code.as_("item_code"),
Sum(Coalesce(mr_item.qty, 0)).as_("qty"),
Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"),
Coalesce(mr_item.uom, "").as_("uom"),
Coalesce(mr_item.stock_uom, "").as_("stock_uom"),
Max(Coalesce(mr_item.uom, "")).as_("uom"),
Max(Coalesce(mr_item.stock_uom, "")).as_("stock_uom"),
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_(
@@ -58,9 +60,9 @@ def get_data(filters):
),
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
(Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))).as_("qty_to_order"),
mr_item.item_name,
mr_item.description,
mr.company,
Max(mr_item.item_name).as_("item_name"),
Max(mr_item.description).as_("description"),
Max(mr.company).as_("company"),
)
.where(
(mr.material_request_type == "Purchase")
@@ -72,7 +74,7 @@ def get_data(filters):
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
query = query.groupby(mr.name, mr_item.item_code).orderby(mr.transaction_date, mr.schedule_date)
query = query.groupby(mr.name, mr_item.item_code).orderby(Max(mr.transaction_date), Max(mr.schedule_date))
data = query.run(as_dict=True)
return data

View File

@@ -115,6 +115,26 @@ class AccountsController(TransactionBase):
PaymentScheduleService(self).set_payment_schedule()
def before_insert(self):
self.clear_clearance_date_on_amend()
def clear_clearance_date_on_amend(self):
"""Drop the bank reconciliation clearance date copied over while amending.
The framework copies `no_copy` fields when amending, so a reconciled
voucher would carry a stale clearance date into its amendment even though
the linked bank transaction gets unreconciled on cancellation.
"""
if not self.get("amended_from"):
return
if self.meta.has_field("clearance_date"):
self.clearance_date = None
for payment in self.get("payments") or []:
if payment.meta.has_field("clearance_date"):
payment.clearance_date = None
def on_update(self):
from erpnext.controllers.taxes_and_totals import process_item_wise_tax_details
@@ -1563,13 +1583,13 @@ def update_invoice_status():
total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.grand_total)
.when(invoice.disable_rounded_total == 1, invoice.grand_total)
.else_(invoice.rounded_total)
)
base_total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.base_grand_total)
.when(invoice.disable_rounded_total == 1, invoice.base_grand_total)
.else_(invoice.base_rounded_total)
)
@@ -1582,7 +1602,7 @@ def update_invoice_status():
& (invoice.outstanding_amount > 0)
& (invoice.status.like("Unpaid%") | invoice.status.like("Partly Paid%"))
& (
((invoice.is_pos & invoice.due_date < today) | is_overdue)
(((invoice.is_pos == 1) & (invoice.due_date < today)) | is_overdue)
if doctype == "Sales Invoice"
else is_overdue
)

View File

@@ -416,20 +416,21 @@ class BuyingController(SubcontractingController):
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items:
if d.item_code:
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
remaining_amount = total_actual_tax_amount
for i, item in enumerate(self.get("items")):
if item.item_code and (item.qty or item.get("rejected_qty")):
item_tax_amount, actual_tax_amount = 0.0, 0.0
if i == (last_item_idx - 1):
item_tax_amount = total_valuation_amount
actual_tax_amount = total_actual_tax_amount
actual_tax_amount = remaining_amount
else:
# calculate item tax amount
item_tax_amount = self.get_item_tax_amount(item, tax_accounts)
@@ -442,7 +443,8 @@ class BuyingController(SubcontractingController):
stock_and_asset_items_amount,
stock_and_asset_items_qty,
)
total_actual_tax_amount -= actual_tax_amount
remaining_amount -= actual_tax_amount
# This code is required here to calculate the correct valuation for stock items
if item.item_code not in stock_and_asset_items:

View File

@@ -632,7 +632,7 @@ def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str
inspection_fieldname = INSPECTION_FIELDNAME_MAP.get(doctype)
if inspection_fieldname is None:
return []
return items if doctype == "Stock Entry" else []
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"

View File

@@ -18,39 +18,9 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import (
prepare_data_for_internal_transfer,
)
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.tests.utils import ERPNextTestSuite
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.customer_type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name
def make_supplier(supplier_name, currency=None):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.supplier_type = "Individual"
supplier.supplier_group = "All Supplier Groups"
if currency:
supplier.default_currency = currency
supplier.save()
return supplier.name
else:
return supplier_name
class TestAccountsController(ERPNextTestSuite):
"""
Test Exchange Gain/Loss booking on various scenarios.
@@ -67,79 +37,28 @@ class TestAccountsController(ERPNextTestSuite):
"""
def setUp(self):
self.create_company()
self.company = "_Test Company"
self.company_abbr = "_TC"
self.cost_center = "Main - _TC"
self.warehouse = "Stores - _TC"
self.finished_warehouse = "Finished Goods - _TC"
self.income_account = "Sales - _TC"
self.expense_account = "Cost of Goods Sold - _TC"
self.debit_to = "Debtors - _TC"
self.debit_usd = "_Test Receivable USD - _TC"
self.debtors_usd = "_Test Receivable USD - _TC"
self.cash = "Cash - _TC"
self.creditors = "Creditors - _TC"
self.creditors_usd = "_Test Payable USD - _TC"
self.item = "_Test Item"
self.customer = "_Test Customer USD"
self.supplier = "_Test Supplier USD"
self.create_account()
self.create_item()
self.create_parties()
self.clear_old_entries()
frappe.flags.is_reverse_depr_entry = False
def create_company(self):
company_name = "_Test Company"
self.company_abbr = abbr = "_TC"
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
def create_item(self):
item = create_item(
item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_parties(self):
self.create_customer()
self.create_supplier()
def create_customer(self):
self.customer = make_customer("_Test MC Customer USD", "USD")
def create_supplier(self):
self.supplier = make_supplier("_Test MC Supplier USD", "USD")
def create_account(self):
# Advance accounts are not in persistent test data — create them on demand.
accounts = [
frappe._dict(
{
"attribute_name": "debtors_usd",
"name": "Debtors USD",
"account_type": "Receivable",
"account_currency": "USD",
"parent_account": "Accounts Receivable - " + self.company_abbr,
}
),
frappe._dict(
{
"attribute_name": "creditors_usd",
"name": "Creditors USD",
"account_type": "Payable",
"account_currency": "USD",
"parent_account": "Accounts Payable - " + self.company_abbr,
}
),
# Advance accounts under Asset and Liability header
frappe._dict(
{
"attribute_name": "advance_received_usd",
@@ -185,6 +104,7 @@ class TestAccountsController(ERPNextTestSuite):
company.save()
customer = frappe.get_doc("Customer", self.customer)
customer.accounts = []
customer.append(
"accounts",
{
@@ -196,6 +116,7 @@ class TestAccountsController(ERPNextTestSuite):
customer.save()
supplier = frappe.get_doc("Supplier", self.supplier)
supplier.accounts = []
supplier.append(
"accounts",
{
@@ -321,18 +242,6 @@ class TestAccountsController(ERPNextTestSuite):
pinv.submit()
return pinv
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
@@ -955,7 +864,7 @@ class TestAccountsController(ERPNextTestSuite):
# Create a Sales Invoice
sinv = frappe.new_doc("Sales Invoice")
sinv.customer = self.customer
sinv.customer = "_Test Customer"
sinv.company = self.company
sinv.currency = "INR"
sinv.taxes_and_charges = "_Test Tax - _TC"
@@ -971,7 +880,7 @@ class TestAccountsController(ERPNextTestSuite):
def test_19_fetch_taxes_based_on_item_tax_template_template(self):
# Create a Sales Invoice
sinv = frappe.new_doc("Sales Invoice")
sinv.customer = self.customer
sinv.customer = "_Test Customer"
sinv.company = self.company
sinv.currency = "INR"
sinv.append(

View File

@@ -5,8 +5,7 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Date
from frappe.query_builder.functions import Date, GroupConcat
Opportunity = DocType("Opportunity")
OpportunityLostReasonDetail = DocType("Opportunity Lost Reason Detail")
@@ -72,6 +71,9 @@ def get_columns():
def get_data(filters):
# db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) with a ", " separator
lost_reasons = GroupConcat(OpportunityLostReasonDetail.lost_reason, ", ", alias="lost_reason")
query = (
frappe.qb.from_(Opportunity)
.left_join(OpportunityLostReasonDetail)
@@ -85,7 +87,7 @@ def get_data(filters):
Opportunity.party_name,
Opportunity.customer_name,
Opportunity.opportunity_type,
GROUP_CONCAT(OpportunityLostReasonDetail.lost_reason, alias="lost_reason").separator(", "),
lost_reasons,
Opportunity.sales_stage,
Opportunity.territory,
)

View File

@@ -0,0 +1,22 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.utils import add_days, today
from erpnext.crm.report.lost_opportunity.lost_opportunity import execute
from erpnext.tests.utils import ERPNextTestSuite
class TestLostOpportunity(ERPNextTestSuite):
def test_report_aggregates_lost_reasons(self):
# Exercises the db-aware GROUP_CONCAT (MariaDB) / STRING_AGG (postgres) aggregation of the
# child "Opportunity Lost Reason Detail" rows. The MySQL-only GROUP_CONCAT term would fail to
# compile on postgres, so simply running the report query guards the portability fix on both
# databases.
company = frappe.db.get_value("Company", {}, "name")
columns, data = execute(
frappe._dict({"company": company, "from_date": add_days(today(), -365), "to_date": today()})
)
self.assertTrue(columns)
self.assertIsInstance(data, list)

View File

@@ -86,10 +86,12 @@ class SalesPipelineAnalytics:
if self.filters.get("range") == "Monthly":
self.group_by_period = Month(opp.expected_closing)
self.duration = MonthName(opp.expected_closing).as_("month")
self.duration_expr = MonthName(opp.expected_closing)
self.duration = self.duration_expr.as_("month")
else:
self.group_by_period = Quarter(opp.expected_closing)
self.duration = Quarter(opp.expected_closing).as_("quarter")
self.duration_expr = Quarter(opp.expected_closing)
self.duration = self.duration_expr.as_("quarter")
self.pipeline_by = {"Owner": "opportunity_owner", "Sales Stage": "sales_stage"}[
self.filters.get("pipeline_by")
@@ -101,27 +103,35 @@ class SalesPipelineAnalytics:
self.get_fields()
opp = frappe.qb.DocType("Opportunity")
query = frappe.qb.get_query(
"Opportunity",
filters=self.get_conditions(),
ignore_permissions=True,
)
pipeline_field = opp._assign if self.group_by_based_on == "_assign" else opp.sales_stage
if self.filters.get("based_on") == "Number":
# Ask get_query for exactly the grouped columns via `fields`, instead of taking its
# default un-grouped "name" select and stripping it. Group by the displayed period
# expression too, so postgres accepts MonthName alongside the numeric Month used for
# chronological ordering (for Quarterly they're the same expression).
self.query_result = (
query.select(
pipeline_field.as_(self.pipeline_by),
frappe.query_builder.functions.Count("*").as_("count"),
self.duration,
frappe.qb.get_query(
"Opportunity",
filters=self.get_conditions(),
fields=[
pipeline_field.as_(self.pipeline_by),
frappe.query_builder.functions.Count("*").as_("count"),
self.duration,
],
ignore_permissions=True,
)
.groupby(pipeline_field, self.group_by_period)
.groupby(pipeline_field, self.group_by_period, self.duration_expr)
.orderby(self.group_by_period)
.run(as_dict=True)
)
if self.filters.get("based_on") == "Amount":
query = frappe.qb.get_query(
"Opportunity",
filters=self.get_conditions(),
ignore_permissions=True,
)
self.query_result = query.select(
pipeline_field.as_(self.pipeline_by),
opp.opportunity_amount.as_("amount"),

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-14 10:35+0000\n"
"PO-Revision-Date: 2026-06-14 17:01\n"
"PO-Revision-Date: 2026-06-18 18:25\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Bosnian\n"
"MIME-Version: 1.0\n"
@@ -720,8 +720,8 @@ msgid "<h3>About Product Bundle</h3>\n\n"
"<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n"
"<h4>Example:</h4>\n"
"<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>"
msgstr "<h3>O Paketu Proizvoda</h3>\n\n"
"<p>Spoji grupu <b>artikala</b> u drugi <b>artikal</b>. Ovo je korisno ako spajate određene <b>Artikle</b> u paket i održavate zalihe upakiranih <b>artikala</b>, a ne zbirn <b>artikal</b>.</p>\n"
msgstr "<h3>O Paketu Artikala</h3>\n\n"
"<p>Spoji grupu <b>artikala</b> u drugi <b>artikal</b>. Ovo je korisno ako spajate određene <b>Artikle</b> u paket i održavate zalihe upakiranih <b>artikala</b>, a ne zbirni <b>artikal</b>.</p>\n"
"<p>Paketni <b>Artikal</b> će imati <code>artikle na zalihi</code> kao <b>Ne</b> i <code>Prodajni Artikal</code> kao <b>Da </b>.</p>\n"
"<h4>Primjer:</h4>\n"
"<p>Ako prodajete prijenosna računala i ruksake odvojeno i imate posebnu cijenu ako Klijent kupi oboje, tada će prijenosno računalo + ruksak biti novi artikal paketa proizvoda.</p>"
@@ -1102,7 +1102,7 @@ msgstr "Klijent mora imati primarni kontakt e-poštu."
#. Description of the 'Disabled' (Check) field in DocType 'Product Bundle'
#: erpnext/selling/doctype/product_bundle/product_bundle.json
msgid "A disabled Product Bundle cannot be selected in transactions."
msgstr ""
msgstr "Onemogućeni Paket Artikal ne može se odabrati u transakcijama."
#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:59
msgid "A driver must be set to submit."
@@ -4487,7 +4487,7 @@ msgstr "Dozvoli Uređivanje Količine Jedinice Zaliha za Dokumente Prodaje"
#. DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Allow to edit stock UOM qty for Stock Entry"
msgstr ""
msgstr "Omogući uređivanje količine jedinice zaliha za Unos Zaliha"
#. Label of the allow_to_make_quality_inspection_after_purchase_or_delivery
#. (Check) field in DocType 'Stock Settings'
@@ -4597,7 +4597,7 @@ msgstr "Alternativni Artikal"
#: erpnext/stock/report/item_where_used/item_where_used.py:427
msgid "Alternative For Item"
msgstr ""
msgstr "Artikal Alternativa"
#. Label of the alternative_item_code (Link) field in DocType 'Item
#. Alternative'
@@ -6918,7 +6918,7 @@ msgstr "Alat Poređenja Sastavnica"
#: erpnext/stock/report/item_where_used/item_where_used.py:178
msgid "BOM Component"
msgstr ""
msgstr "Komponenta Sastavnice"
#. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM'
#: erpnext/manufacturing/doctype/bom/bom.json
@@ -6949,7 +6949,7 @@ msgstr "Artikal Sastavnice Konstruktora"
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535
msgid "BOM Creator Item with name {0} does not exist"
msgstr ""
msgstr "Artikal Sastavnice s nazivom {0} ne postoji"
#. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item
#. Supplied'
@@ -7049,7 +7049,7 @@ msgstr "Operativno Vrijeme Sastavnice"
#: erpnext/stock/report/item_where_used/item_where_used.py:248
msgid "BOM Output"
msgstr ""
msgstr "Sastavnica"
#: erpnext/stock/report/item_prices/item_prices.py:60
msgid "BOM Rate"
@@ -8222,13 +8222,13 @@ msgstr "Datum Fakture"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Bill Even If Previous Invoice Unpaid"
msgstr ""
msgstr "Fakturiši čak i ako prethodna faktura nije plaćena"
#. Option for the 'Generate Invoice At' (Select) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Bill N days before period start"
msgstr ""
msgstr "Fakturiši N dana prije početka perioda"
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
@@ -8410,13 +8410,13 @@ msgstr "e-pošta Fakture"
#. Label of the billing_heatmap (HTML) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing Heatmap"
msgstr ""
msgstr "Toplinska mapa Fakturisanja"
#. Label of the billing_history_section (Section Break) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing History"
msgstr ""
msgstr "Historija Fakturisanja"
#. Label of the billing_hours (Float) field in DocType 'Sales Invoice
#. Timesheet'
@@ -8450,7 +8450,7 @@ msgstr "Faktura Interval u Planu pretplate mora biti Mjesec koji prati kalendars
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing Period"
msgstr ""
msgstr "Period Fakturisanja"
#. Label of the billing_rate (Currency) field in DocType 'Activity Cost'
#. Label of the billing_rate (Currency) field in DocType 'Timesheet Detail'
@@ -9278,7 +9278,7 @@ msgstr "Izračunaj procijenjeno vrijeme dolaska"
#. Settings'
#: erpnext/selling/doctype/selling_settings/selling_settings.json
msgid "Calculate Product Bundle price based on child Item's rates"
msgstr "Obračunaj Cijenu Paketa Proizvoda na osnovu cijena Podređenih Artikala"
msgstr "Obračunaj Cijenu Paketa Artikala na osnovu cijena Podređenih Artikala"
#. Description of the 'Hidden Line (Internal Use Only)' (Check) field in
#. DocType 'Financial Report Row'
@@ -9547,7 +9547,7 @@ msgstr "Otkaži Pretplatu nakon perioda odgode"
#. Label of the cancel_at_period_end (Check) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Cancel When Period Ends"
msgstr ""
msgstr "Otkaži kada se završi period"
#. Label of the cancelation_date (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
@@ -9556,7 +9556,7 @@ msgstr "Datum Otkazivanja"
#: erpnext/manufacturing/doctype/job_card/job_card.py:1567
msgid "Cancelled Job Card cannot be processed."
msgstr ""
msgstr "Otkazani Radni Nalog ne može se obraditi."
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:76
msgid "Cannot Assign Cashier"
@@ -9691,7 +9691,7 @@ msgstr "Nije moguće pretvoriti u Grupu jer je odabran Tip Računa."
#: erpnext/accounts/doctype/sales_invoice/mapper.py:372
msgid "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. Please check the existing linked {2}s."
msgstr ""
msgstr "Nije moguće kreirati {0} između poduzeća. Svi početni artikli {1} su već u potpunosti fakturisani. Provjeri postojeće povezane {2}."
#: erpnext/stock/doctype/purchase_receipt/services/reservation.py:49
msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts."
@@ -9770,7 +9770,7 @@ msgstr "Nije moguće omogućiti račun zaliha po artiklima, jer postoje postoje
#: erpnext/crm/doctype/crm_settings/crm_settings.py:37
msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
msgstr ""
msgstr "Nije moguće omogućiti kreiranje prilike iz kontakta jer je kontakt obrazac onemogućen."
#: erpnext/selling/doctype/sales_order/sales_order.py:624
#: erpnext/selling/doctype/sales_order/sales_order.py:647
@@ -9878,7 +9878,7 @@ msgstr "Nije moguće započeti brisanje. Drugo brisanje {0} je već u redu čeka
#: erpnext/manufacturing/doctype/job_card/job_card.py:922
msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission."
msgstr ""
msgstr "Nije moguće podnijeti Radni Nalog {0} dok je na čekanju. Nastavi i završi posao prije podnošenja."
#: erpnext/accounts/services/child_item_update.py:283
msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation"
@@ -13410,7 +13410,7 @@ msgstr "Kreiraj novi trag"
#: erpnext/selling/doctype/product_bundle/product_bundle.js:16
msgid "Create New Version"
msgstr ""
msgstr "Kreiraj novu verziju"
#: banking/src/components/common/LinkFieldCombobox.tsx:284
msgid "Create New {0}"
@@ -14291,12 +14291,12 @@ msgstr "Trenutni Valuta kurs"
#. Label of the current_invoice_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Current Invoice End"
msgstr ""
msgstr "Trenutni Završni Datum Fakture"
#. Label of the current_invoice_start (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Current Invoice Start"
msgstr ""
msgstr "Trenutni Početni Datum Fakture"
#. Label of the current_level (Int) field in DocType 'BOM Update Log'
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -17243,7 +17243,7 @@ msgstr "Onemogućeni Bankovni Račun"
#: erpnext/stock/doctype/packed_item/packed_item.py:216
msgid "Disabled Product Bundle"
msgstr ""
msgstr "Onemogući Paket Artikala"
#: erpnext/stock/utils.py:434
msgid "Disabled Warehouse {0} cannot be used for this transaction."
@@ -18939,7 +18939,7 @@ msgstr "Omogući Program Bodova Lojalnosti"
#. DocType 'CRM Settings'
#: erpnext/crm/doctype/crm_settings/crm_settings.json
msgid "Enable Opportunity Creation from Contact Us"
msgstr ""
msgstr "Omogući Kreiranje Prilika iz Kontaktiraj Nas obrasca"
#. Label of the enable_parallel_reposting (Check) field in DocType 'Stock
#. Reposting Settings'
@@ -19151,9 +19151,9 @@ msgid "Enabling this will do the following:\n"
msgstr "Omogućavanje ovoga će učiniti sljedeće:\n"
"<ul style=\"padding-left:16px\">\n"
"<li>Omogućiti uređivanje kolone cjene u svim tabelama Pakiranih/Paketnih artikala.</li>\n"
"<li>Izračunati cijene svih <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">paketa proizvoda</a> u tabeli artikala na osnovu cijena njihovih podređenih artikala navedenih u tabeli pakiranih/paketiranih artikala. </li>\n"
"<li>Izračunati cijene svih <a href=\"/desk/product-bundle\" rel=\"noopener noreferrer\">paketa artikala</a> u tabeli artikala na osnovu cijena njihovih podređenih artikala navedenih u tabeli pakiranih/paketiranih artikala. </li>\n"
"</ul>\n"
"Napomena: Ako je ovo omogućeno, ažuriranje cjene proizvoda u paketu u tabeli artikala neće promijeniti njegovu cijenu. Cijena će se vratiti na cijenu zasnovanu na podređenim artiklima prilikom spremanja dokumenta."
"Napomena: Ako je ovo omogućeno, ažuriranje cjene artikala u paketu u tabeli artikala neće promijeniti njegovu cijenu. Cijena će se vratiti na cijenu zasnovanu na podređenim artiklima prilikom spremanja dokumenta."
#. Label of the encashment_date (Date) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
@@ -19541,7 +19541,7 @@ msgstr "Uloga Odobravatelja Izuzetka Proračuna"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:53
msgid "Excess Disassembly"
msgstr "Prekomjerna Demontaža"
msgstr "Prekomjerno Rastavljanje"
#: erpnext/manufacturing/report/work_order_consumed_materials/work_order_consumed_materials.js:55
msgid "Excess Materials Consumed"
@@ -19977,7 +19977,7 @@ msgstr "Račun troškova je obavezan za artikal {0}"
#. Description of the 'Enable Deferred Revenue' (Check) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
msgid "Expense for this item will be recognized over a period of months. Eg: prepaid insurance or annual software license"
msgstr "Trošak za ovaj artikal bit će priznat tokom nekoliko mjeseci. Npr: unaprijed plaćeno osiguranje ili godišnja licenca za softver"
msgstr "Trošak za ovaj artikal bit će priznat tokom nekoliko mjeseci. Npr: unaprijed plaćeno osiguranje ili godišnja licenca za program"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:85
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:145
@@ -20225,7 +20225,7 @@ msgstr "Ažuriranje prioriteta pravila nije uspjelo"
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:521
msgid "Failed to update subscription status for {0} {1}"
msgstr ""
msgstr "Nije uspjelo ažuriranje statusa pretplate za {0} {1}"
#. Label of the failure_date (Datetime) field in DocType 'Asset Repair'
#: erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -20765,7 +20765,7 @@ msgstr "Gotov Proizvod {0} ne odgovara Radnom Nalogu {1}"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:71
msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row."
msgstr ""
msgstr "Količina gotovog proizvoda koja se troši ({0} u jedinici zaliha) mora biti jednaka količini za rastavljanje ({1}). Ne mijenjaj jedinicu, faktor konverzije ili količinu u redu gotovog proizvoda."
#: erpnext/selling/doctype/sales_order/sales_order.js:615
msgid "First Delivery Date"
@@ -23435,13 +23435,13 @@ msgstr "Ako je odbrano, ovaj artikal se tretira kao direktna dostava u Prodajnim
#. Description of the 'Update Stock' (Check) field in DocType 'Sales Invoice'
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately."
msgstr ""
msgstr "Ako je oodabrano, ažurira inventar; zalihe i knjigovodstveni unosi se kreiraju zajedno. Ostavi neodabrano ako se Dostavnica kreira zasebno."
#. Description of the 'Update Stock' (Check) field in DocType 'Purchase
#. Invoice'
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately."
msgstr ""
msgstr "Ako je odabrano, ažurira se inventar; unosi zaliha i knjigoovodstva se kreiraju zajedno. Ostavi neodabrano ako Kupovni Račun kreira zasebno."
#: erpnext/public/js/setup_wizard.js:56
msgid "If checked, we will create demo data for you to explore the system. This demo data can be erased later."
@@ -25255,7 +25255,7 @@ msgstr "Nevažeće poduzeće za transakcije među poduzećima."
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:972
msgid "Invalid Configuration"
msgstr ""
msgstr "Nevažeća Konfiguracija"
#: erpnext/accounts/services/taxes.py:294
#: erpnext/assets/doctype/asset/asset.py:361
@@ -25273,12 +25273,12 @@ msgstr "Nevažeći Datum Dostave"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:110
msgid "Invalid Disassembly Item"
msgstr ""
msgstr "Nevažeći Artikala za Rastavljanje"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:76
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:125
msgid "Invalid Disassembly Quantity"
msgstr ""
msgstr "Nevažeća Količina za Rastavljanje"
#: erpnext/selling/page/point_of_sale/pos_item_cart.js:414
msgid "Invalid Discount"
@@ -26144,7 +26144,7 @@ msgstr "Je Fantomski Artikal"
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
msgid "Is Product Bundle"
msgstr ""
msgstr "Je Paket Artikala"
#. Label of the po_required (Select) field in DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -27652,7 +27652,7 @@ msgstr "Detalji Težine Artikla"
#. Name of a report
#: erpnext/stock/report/item_where_used/item_where_used.json
msgid "Item Where Used"
msgstr ""
msgstr "Gdje se koristi Artikal"
#. Label of a Link in the Buying Workspace
#. Name of a report
@@ -27775,7 +27775,7 @@ msgstr "Artikal {0} dodan je više puta pod isti nadređeni artikal {1} u redovi
#: erpnext/selling/doctype/product_bundle/product_bundle.js:54
msgid "Item {0} already has an active Product Bundle ({1}). Submitting this will create a new version and deactivate {1}."
msgstr ""
msgstr "Artikal {0} već ima aktivni Paket Artikala ({1}). Podnošenjem ovoga kreiraće te novu verziju i deaktivirati {1}."
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:119
msgid "Item {0} cannot be added as a sub-assembly of itself"
@@ -28106,7 +28106,7 @@ msgstr "Stavka Radne Kartice"
#: erpnext/manufacturing/doctype/job_card/job_card.py:925
msgid "Job Card On Hold"
msgstr ""
msgstr "Radni Nalog je na čekanju"
#. Name of a DocType
#: erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
@@ -30309,7 +30309,7 @@ msgstr "Usklađeno"
#: erpnext/stock/report/item_where_used/item_where_used.py:57
msgid "Matched Field"
msgstr ""
msgstr "Usklađeno polje"
#. Label of the matched_transaction_rule (Link) field in DocType 'Bank
#. Transaction'
@@ -30928,7 +30928,7 @@ msgstr "Metar/Sekunda"
#: erpnext/manufacturing/doctype/workstation/workstation.py:546
msgid "Method {0} is not allowed to be run on a Job Card."
msgstr ""
msgstr "Metoda {0} se ne smije izvršavati na Radnom Nalogu."
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
@@ -32258,13 +32258,13 @@ msgstr "Newton"
#. Label of the next_billing_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Next Billing Period End"
msgstr ""
msgstr "Sljedeći Perioda Fakturisanja Završava"
#. Label of the next_billing_period_start (Date) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Next Billing Period Start"
msgstr ""
msgstr "Sljedeći Perioda Fakturisanja Počinje"
#. Label of the next_depreciation_date (Date) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
@@ -33476,7 +33476,7 @@ msgstr "Samo jedna operacija može imati odabranu opciju 'Je li Gotov Proizvod'
#. Description of the 'Is Active' (Check) field in DocType 'Product Bundle'
#: erpnext/selling/doctype/product_bundle/product_bundle.json
msgid "Only one version of a Product Bundle can be active at a time for a given Parent Item. Activating a version deactivates the previously active one."
msgstr ""
msgstr "Samo jedna verzija Paketa Artikala može biti aktivna u datom trenutku za dati Nadređeni Artikal. Aktiviranje verzije deaktivira prethodno aktivnu verziju."
#: erpnext/stock/doctype/stock_entry/stock_entry.py:719
msgid "Only one {0} entry can be created against the Work Order {1}"
@@ -39083,7 +39083,7 @@ msgstr "Vremenska oznaka knjiženja mora biti nakon {0}"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Postpaid (bill at period end)"
msgstr ""
msgstr "Naknadno Plaćeno (faktura na završetku perioda)"
#. Description of a DocType
#: erpnext/crm/doctype/opportunity/opportunity.json
@@ -39186,7 +39186,7 @@ msgstr "Preferirana e-pošta"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Prepaid (bill at period start)"
msgstr ""
msgstr "Unaprijed Plaćeno (faktura na početku perioda)"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:34
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:51
@@ -40174,7 +40174,7 @@ msgstr "Stanje Paketa Proizvoda"
#: erpnext/stock/report/item_where_used/item_where_used.py:278
msgid "Product Bundle Component"
msgstr ""
msgstr "Komponenta Paketa Artikala"
#. Label of the product_bundle_help (HTML) field in DocType 'POS Invoice'
#. Label of the product_bundle_help (HTML) field in DocType 'Sales Invoice'
@@ -40195,11 +40195,11 @@ msgstr "Pomoć Paketa Proizvoda"
#: erpnext/selling/doctype/product_bundle_item/product_bundle_item.json
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
msgid "Product Bundle Item"
msgstr "Artikal Paketa Proizvoda"
msgstr "Artikal Paketa Artikala"
#: erpnext/stock/report/item_where_used/item_where_used.py:305
msgid "Product Bundle Parent"
msgstr ""
msgstr "Nadređeni Paket Artikala"
#. Description of the 'Product Bundle' (Link) field in DocType 'Purchase
#. Invoice Item'
@@ -40213,15 +40213,15 @@ msgstr ""
#: erpnext/stock/doctype/packed_item/packed_item.json
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
msgid "Product Bundle version this row was packed from"
msgstr ""
msgstr "Verzija Paketa Artikala iz koje je ovaj red preuzet iz"
#: erpnext/stock/doctype/packed_item/packed_item.py:453
msgid "Product Bundle {0} is disabled and cannot be used in transactions."
msgstr ""
msgstr "Paket Artikala {0} je onemogućen i ne može se koristiti u transakcijama."
#: erpnext/stock/doctype/packed_item/packed_item.py:450
msgid "Product Bundle {0} is not submitted"
msgstr ""
msgstr "Paket Artikala {0} nije podnešen"
#. Label of the product_discount_scheme_section (Section Break) field in
#. DocType 'Pricing Rule'
@@ -43881,7 +43881,7 @@ msgstr "Osvježite Plaid Link"
#. Option for the 'Status' (Select) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Refunded"
msgstr ""
msgstr "Povraćeno"
#: erpnext/stock/reorder_item.py:390
msgid "Regards,"
@@ -43992,7 +43992,7 @@ msgstr "Povezano"
#: erpnext/stock/report/item_where_used/item_where_used.py:50
msgid "Related Item"
msgstr ""
msgstr "Povezani Artikal"
#. Label of the relation (Data) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
@@ -46064,7 +46064,7 @@ msgstr "Red #{0}: Gotov Proizvod artikla nije navedena zaservisni artikal {1}"
#: erpnext/manufacturing/doctype/bom/bom.py:371
msgid "Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table."
msgstr ""
msgstr "Red #{0}: Artikal Gotovog Proizvoda {1} ne može se dodati u tabelu Sekundarnih Artikala."
#: erpnext/buying/doctype/purchase_order/services/subcontracting.py:28
#: erpnext/selling/doctype/sales_order/services/subcontracting.py:27
@@ -46155,7 +46155,7 @@ msgstr "Red #{0}: Artikal {1} nije artikal na zalihama"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:106
msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly."
msgstr ""
msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može se dodati ovom rastavljanju."
#: erpnext/controllers/subcontracting_inward_controller.py:79
msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
@@ -46167,7 +46167,7 @@ msgstr "Red #{0}: Artikla {1} se ne slaže. Promjena koda artikla nije dozvoljen
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:115
msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows."
msgstr ""
msgstr "Red #{0}: Količina artikla {1} ({2} u jedinici zaliha) ne odgovara količini izvedenoj iz izvora ({3}). Ne mijenjaj jedinicu, faktor konverzije ili količinu redova za rastavljanje."
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:780
msgid "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher"
@@ -46233,7 +46233,7 @@ msgstr "Red #{0}: Procentualni Gubitka Procesa treba da bude manji od 100% za {1
#: erpnext/stock/doctype/packed_item/packed_item.py:213
msgid "Row #{0}: Product Bundle {1} is disabled and cannot be used in transactions."
msgstr ""
msgstr "Red #{0}: Paket Artikal {1} je onemogućen i ne može se koristiti u transakcijama."
#: erpnext/public/js/utils/barcode_scanner.js:425
msgid "Row #{0}: Qty increased by {1}"
@@ -49490,7 +49490,7 @@ msgstr "Serijski i Šaržni Paket {0} nije podnešen"
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2251
msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified."
msgstr ""
msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mijenjati."
#. Label of the section_break_45 (Section Break) field in DocType
#. 'Subcontracting Receipt Item'
@@ -52668,7 +52668,7 @@ msgstr "Podizvođačka Dostava"
#: erpnext/stock/report/item_where_used/item_where_used.py:362
msgid "Subcontracting Finished Good"
msgstr ""
msgstr "Podizvođački Gotov Proizvod"
#. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling
#. Settings'
@@ -52852,7 +52852,7 @@ msgstr "Podizvođački Prodajni Nalog"
#: erpnext/stock/report/item_where_used/item_where_used.py:336
msgid "Subcontracting Service Item"
msgstr ""
msgstr "Podizvođački Uslužni Artikal"
#. Label of the subcontract (Tab Break) field in DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -52900,7 +52900,7 @@ msgstr "Podnesi Ponudu"
#: erpnext/manufacturing/doctype/job_card/job_card.py:1570
msgid "Submitted Job Card cannot be processed."
msgstr ""
msgstr "Podnešeni Radni Nalog ne može biti obrađen."
#. Label of the subscription_section (Section Break) field in DocType 'Payment
#. Request'
@@ -55486,7 +55486,7 @@ msgstr "Otpremljena datoteka nije u važećem MT940 formatu."
#: erpnext/edi/doctype/code_list/code_list_import.py:40
msgid "The uploaded file does not match the selected Code List."
msgstr "Učitani fajl ne odgovara odabranoj Listi Kodova."
msgstr "Učitana datoteka ne odgovara odabranoj Listi Kodova."
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js:10
msgid "The user cannot submit the Serial and Batch Bundle manually"
@@ -61720,7 +61720,7 @@ msgstr "Ne možete podnijeti nalog bez plaćanja."
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:968
msgid "You cannot update stock for a Debit Note. A Debit Note is a financial document that should not affect inventory. Please disable 'Update Stock'."
msgstr ""
msgstr "Ne možete ažurirati zalihe za debitnu notu. Debitna nota je finansijski dokument koji ne bi trebao utjecati na zalihe. Molimo vas da onemogućite opciju 'Ažuriraj Zalihe'."
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:106
msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
@@ -62086,7 +62086,7 @@ msgstr "izvodi bilo koje dolje:"
#. Item'
#: erpnext/stock/doctype/pick_list_item/pick_list_item.json
msgid "product bundle item row's name in sales order. Also indicates that picked item is to be used for a product bundle"
msgstr "naziv reda artikla paketa proizvoda u prodajnom nalogu. Također označava da odabrani artikal treba koristiti za paket proizvoda"
msgstr "naziv reda artikla paketa artikala u prodajnom nalogu. Također označava da odabrani artikal treba koristiti za paket artikala"
#. Option for the 'Plaid Environment' (Select) field in DocType 'Plaid
#. Settings'
@@ -62834,7 +62834,7 @@ msgstr "{0}, završi operaciju {1} prije operacije {2}."
#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:61
msgid "{0}, {1} or {2} are the only allowed options."
msgstr ""
msgstr "{0}, {1} ili {2} su jedine dozvoljene opcije."
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:525
msgid "{0}: Child table (auto-deleted with parent)"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-14 10:35+0000\n"
"PO-Revision-Date: 2026-06-14 17:01\n"
"PO-Revision-Date: 2026-06-17 17:52\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@@ -4507,7 +4507,7 @@ msgstr "آیتم جایگزین"
#: erpnext/stock/report/item_where_used/item_where_used.py:427
msgid "Alternative For Item"
msgstr ""
msgstr "جایگزین برای آیتم"
#. Label of the alternative_item_code (Link) field in DocType 'Item
#. Alternative'
@@ -6828,7 +6828,7 @@ msgstr "ابزار مقایسه BOM"
#: erpnext/stock/report/item_where_used/item_where_used.py:178
msgid "BOM Component"
msgstr ""
msgstr "مولفه BOM"
#. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM'
#: erpnext/manufacturing/doctype/bom/bom.json
@@ -6859,7 +6859,7 @@ msgstr "آیتم ایجاد کننده BOM"
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535
msgid "BOM Creator Item with name {0} does not exist"
msgstr ""
msgstr "آیتم سازنده BOM با نام {0} وجود ندارد"
#. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item
#. Supplied'
@@ -6959,7 +6959,7 @@ msgstr "زمان عملیات BOM"
#: erpnext/stock/report/item_where_used/item_where_used.py:248
msgid "BOM Output"
msgstr ""
msgstr "خروجی BOM"
#: erpnext/stock/report/item_prices/item_prices.py:60
msgid "BOM Rate"
@@ -7630,7 +7630,7 @@ msgstr "تراکنش بانکی {0} به روز شد"
#: banking/src/pages/BankReconciliation.tsx:118
msgid "Bank Transactions"
msgstr ""
msgstr "تراکنش‌های بانکی"
#: erpnext/setup/setup_wizard/operations/install_fixtures.py:584
msgid "Bank account cannot be named as {0}"
@@ -8132,13 +8132,13 @@ msgstr "تاریخ صورتحساب"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Bill Even If Previous Invoice Unpaid"
msgstr ""
msgstr "صدور صورتحساب حتی اگر فاکتور قبلی پرداخت نشده باشد"
#. Option for the 'Generate Invoice At' (Select) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Bill N days before period start"
msgstr ""
msgstr "صورتحساب N روز قبل از شروع دوره"
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
@@ -8320,13 +8320,13 @@ msgstr "ایمیل صورتحساب"
#. Label of the billing_heatmap (HTML) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing Heatmap"
msgstr ""
msgstr "نقشه حرارتی صورتحساب"
#. Label of the billing_history_section (Section Break) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing History"
msgstr ""
msgstr "تاریخچه صورتحساب"
#. Label of the billing_hours (Float) field in DocType 'Sales Invoice
#. Timesheet'
@@ -8360,7 +8360,7 @@ msgstr "بازه صورتحساب در طرح اشتراک باید ماه با
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing Period"
msgstr ""
msgstr "دوره صورتحساب"
#. Label of the billing_rate (Currency) field in DocType 'Activity Cost'
#. Label of the billing_rate (Currency) field in DocType 'Timesheet Detail'
@@ -8905,11 +8905,11 @@ msgstr "ساختمان ها"
#: banking/src/components/features/ActionLog/ActionLogDialogBody.tsx:88
msgid "Bulk Bank Entry"
msgstr ""
msgstr "ثبت بانک انبوه"
#: banking/src/components/features/ActionLog/ActionLogDialogBody.tsx:76
msgid "Bulk Payment"
msgstr ""
msgstr "پرداخت انبوه"
#: erpnext/utilities/doctype/rename_tool/rename_tool.js:71
msgid "Bulk Rename Jobs"
@@ -9457,7 +9457,7 @@ msgstr "لغو اشتراک پس از دوره مهلت"
#. Label of the cancel_at_period_end (Check) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Cancel When Period Ends"
msgstr ""
msgstr "لغو هنگام پایان دوره"
#. Label of the cancelation_date (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
@@ -13320,7 +13320,7 @@ msgstr "ایجاد سرنخ جدید"
#: erpnext/selling/doctype/product_bundle/product_bundle.js:16
msgid "Create New Version"
msgstr ""
msgstr "ایجاد نسخه جدید"
#: banking/src/components/common/LinkFieldCombobox.tsx:284
msgid "Create New {0}"
@@ -14201,12 +14201,12 @@ msgstr "نرخ ارز فعلی"
#. Label of the current_invoice_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Current Invoice End"
msgstr ""
msgstr "پایان فاکتور فعلی"
#. Label of the current_invoice_start (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Current Invoice Start"
msgstr ""
msgstr "شروع فاکتور فعلی"
#. Label of the current_level (Int) field in DocType 'BOM Update Log'
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -17153,7 +17153,7 @@ msgstr ""
#: erpnext/stock/doctype/packed_item/packed_item.py:216
msgid "Disabled Product Bundle"
msgstr ""
msgstr "بسته محصول غیرفعال"
#: erpnext/stock/utils.py:434
msgid "Disabled Warehouse {0} cannot be used for this transaction."
@@ -17162,7 +17162,7 @@ msgstr "از انبار غیرفعال شده {0} نمی‌توان برای ا
#. Description of the 'Disabled' (Check) field in DocType 'Item'
#: erpnext/stock/doctype/item/item.json
msgid "Disabled items cannot be selected in any transaction."
msgstr ""
msgstr "اقلام غیرفعال را نمی‌توان در هیچ تراکنشی انتخاب کرد."
#: erpnext/accounts/services/internal_transfer.py:118
msgid "Disabled pricing rules since this {} is an internal transfer"
@@ -25029,7 +25029,7 @@ msgstr "مرجع فروش داخلی وجود ندارد"
#. 'Supplier'
#: erpnext/buying/doctype/supplier/supplier.json
msgid "Internal Supplier Details"
msgstr ""
msgstr "جزئیات تأمین‌کننده داخلی"
#: erpnext/buying/doctype/supplier/supplier.py:180
msgid "Internal Supplier for company {0} already exists"
@@ -25060,7 +25060,7 @@ msgstr "مرجع انتقال داخلی وجود ندارد"
#. DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Internal Transfer Rules"
msgstr ""
msgstr "قوانین انتقال داخلی"
#: erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py:37
msgid "Internal Transfers"
@@ -25196,7 +25196,7 @@ msgstr "نوع سند نامعتبر است"
#: erpnext/selling/report/sales_analytics/sales_analytics.py:529
msgid "Invalid Document Type {0}"
msgstr ""
msgstr "نوع سند نامعتبر {0}"
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:207
msgid "Invalid File Type"
@@ -25313,7 +25313,7 @@ msgstr "انبار منبع و هدف نامعتبر"
#: erpnext/selling/report/sales_analytics/sales_analytics.py:507
msgid "Invalid Tree Type {0}"
msgstr ""
msgstr "نوع درخت نامعتبر {0}"
#: erpnext/edi/doctype/code_list/code_list_import.py:37
msgid "Invalid Upload"
@@ -25374,11 +25374,11 @@ msgstr "پرسمان جستجوی نامعتبر"
#: erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py:99
msgid "Invalid value {0} for 'Based On'"
msgstr ""
msgstr "مقدار نامعتبر {0} برای 'Based On'"
#: erpnext/selling/report/inactive_customers/inactive_customers.py:20
msgid "Invalid value {0} for 'Doctype'"
msgstr ""
msgstr "مقدار نامعتبر {0} برای 'Doctype'"
#: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:109
#: erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py:119
@@ -25411,7 +25411,7 @@ msgstr "فهرست موجودی"
#. Default'
#: erpnext/stock/doctype/item_default/item_default.json
msgid "Inventory Account"
msgstr ""
msgstr "حساب موجودی"
#. Label of the inventory_account_currency (Link) field in DocType 'Item
#. Default'
@@ -26993,7 +26993,7 @@ msgstr "نام گروه آیتم"
#: erpnext/setup/doctype/item_group/item_group.js:119
msgid "Item Group Override"
msgstr ""
msgstr "بازتعریف گروه آیتم"
#: erpnext/setup/doctype/item_group/item_group.js:82
msgid "Item Group Tree"
@@ -27267,7 +27267,7 @@ msgstr "آیتم موجود نیست"
#. Default'
#: erpnext/stock/doctype/item_default/item_default.json
msgid "Item Override"
msgstr ""
msgstr "بازتعریف آیتم"
#. Label of a Link in the Buying Workspace
#. Label of a Link in the Selling Workspace
@@ -29370,7 +29370,7 @@ msgstr "نگهداری موجودی"
#. DocType 'Accounts Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Maintain same rate throughout internal Transaction"
msgstr ""
msgstr "حفظ نرخ یکسان در کل تراکنش‌های داخلی"
#. Label of the maintain_same_sales_rate (Check) field in DocType 'Selling
#. Settings'
@@ -30760,7 +30760,7 @@ msgstr "ادغام پیشرفت"
#. Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Merge similar Account Heads"
msgstr ""
msgstr "ادغام سر فصل‌های حساب مشابه"
#: erpnext/public/js/utils.js:1090
msgid "Merge taxes from multiple documents"
@@ -31140,7 +31140,7 @@ msgstr ""
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:929
msgid "Missing Dependency"
msgstr ""
msgstr "وابستگی گمشده"
#: erpnext/stock/report/serial_no_and_batch_traceability/serial_no_and_batch_traceability.py:44
msgid "Missing Filters"
@@ -31627,7 +31627,7 @@ msgstr "مقدار منفی مجاز نیست"
#. Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Negative Stock"
msgstr ""
msgstr "موجودی منفی"
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1608
#: erpnext/stock/serial_batch_bundle.py:1549
@@ -32295,7 +32295,7 @@ msgstr "هیچ تامین کننده ای برای Inter Company Transactions ی
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:976
msgid "No Tables Detected"
msgstr ""
msgstr "هیچ جدولی شناسایی نشد"
#: erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py:100
msgid "No Tax Withholding data found for the current posting date."
@@ -32341,7 +32341,7 @@ msgstr "هیچ BOM فعالی برای آیتم {0} یافت نشد. تحویل
#: erpnext/stock/doctype/item/item_prices.html:135
msgid "No active item prices found."
msgstr ""
msgstr "هیچ قیمت آیتم فعالی یافت نشد."
#: erpnext/stock/doctype/item_variant_settings/item_variant_settings.js:46
msgid "No additional fields available"
@@ -43778,7 +43778,7 @@ msgstr "پیوند شطرنجی را تازه کنید"
#. Option for the 'Status' (Select) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Refunded"
msgstr ""
msgstr "استرداد وجه شده"
#: erpnext/stock/reorder_item.py:390
msgid "Regards,"
@@ -43889,7 +43889,7 @@ msgstr "مربوط"
#: erpnext/stock/report/item_where_used/item_where_used.py:50
msgid "Related Item"
msgstr ""
msgstr "آیتم مرتبط"
#. Label of the relation (Data) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
@@ -48858,7 +48858,7 @@ msgstr "مبلغ فروش"
#. Default'
#: erpnext/stock/doctype/item_default/item_default.json
msgid "Selling Cost Center"
msgstr ""
msgstr "مرکز هزینه فروش"
#: erpnext/stock/report/item_price_stock/item_price_stock.py:48
msgid "Selling Price List"
@@ -49026,7 +49026,7 @@ msgstr "شماره های سریال / دسته ای"
#. Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Serial Item settings"
msgstr ""
msgstr "تنظیمات آیتم سریال"
#. Label of the serial_no (Text) field in DocType 'POS Invoice Item'
#. Label of the serial_no (Text) field in DocType 'Purchase Invoice Item'
@@ -50640,7 +50640,7 @@ msgstr "نمایش جزئیات پرداخت"
#. 'Accounts Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Show Payment Schedule in print"
msgstr ""
msgstr "نمایش زمان‌بندی پرداخت در چاپ"
#. Label of the show_remarks (Check) field in DocType 'Process Statement Of
#. Accounts'
@@ -50684,12 +50684,12 @@ msgstr ""
#. Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Show balances in Chart of Accounts"
msgstr ""
msgstr "نمایش ترازها در نمودار حساب‌ها"
#. Label of the show_barcode_field (Check) field in DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Show barcode field in stock transactions"
msgstr ""
msgstr "نمایش فیلد بارکد در تراکنش‌های موجودی"
#: erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.js:88
msgid "Show in Bucket View"
@@ -50704,7 +50704,7 @@ msgstr "نمایش در وب سایت"
#. Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Show inclusive tax in print"
msgstr ""
msgstr "نمایش مالیات فراگیر در چاپ"
#. Description of the 'Reverse Sign' (Check) field in DocType 'Financial Report
#. Row'
@@ -50738,7 +50738,7 @@ msgstr "نمایش ثبت‌های در انتظار"
#. Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Show taxes as table in print"
msgstr ""
msgstr "نمایش مالیات‌ها به صورت جدول در چاپ"
#: erpnext/accounts/report/consolidated_trial_balance/consolidated_trial_balance.js:80
#: erpnext/accounts/report/trial_balance/trial_balance.js:100
@@ -50839,7 +50839,7 @@ msgstr ""
#: erpnext/stock/doctype/stock_entry/stock_entry.py:503
msgid "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table."
msgstr ""
msgstr "از آنجایی که برای کالای نهایی {1}، اتلاف فرآیند {0} واحد وجود دارد، شما باید مقدار {0} واحد برای کالای نهایی {1} در جدول آیتم‌ها را کاهش دهید."
#: erpnext/manufacturing/doctype/bom/bom.py:355
msgid "Since you have enabled 'Track Semi Finished Goods', at least one operation must have 'Is Final Finished Good' checked. For that set the FG / Semi FG Item as {0} against an operation."
@@ -51444,7 +51444,7 @@ msgstr ""
#. Label of the statement_password (Password) field in DocType 'Bank Account'
#: erpnext/accounts/doctype/bank_account/bank_account.json
msgid "Statement PDF Password"
msgstr ""
msgstr "گذرواژه PDF صورتحساب"
#: erpnext/accounts/report/general_ledger/general_ledger.html:145
msgid "Statement Period"
@@ -52288,7 +52288,7 @@ msgstr ""
#. Label of the stock_frozen_upto (Date) field in DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Stock frozen up to"
msgstr ""
msgstr "موجودی منجمد تا"
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1140
msgid "Stock has been unreserved for work order {0}."
@@ -52556,7 +52556,7 @@ msgstr ""
#: erpnext/stock/report/item_where_used/item_where_used.py:362
msgid "Subcontracting Finished Good"
msgstr ""
msgstr "کالای نهایی پیمان‌کاری فرعی"
#. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling
#. Settings'
@@ -52740,7 +52740,7 @@ msgstr ""
#: erpnext/stock/report/item_where_used/item_where_used.py:336
msgid "Subcontracting Service Item"
msgstr ""
msgstr "آیتم خدمات پیمانکاری فرعی"
#. Label of the subcontract (Tab Break) field in DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -52776,7 +52776,7 @@ msgstr "فاکتورهای تولید شده را ارسال کنید"
#. Settings'
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
msgid "Submit Journal entries"
msgstr ""
msgstr "ارسال ثبت‌های دفتر روزنامه"
#: erpnext/manufacturing/doctype/work_order/work_order.js:185
msgid "Submit this Work Order for further processing."
@@ -52788,7 +52788,7 @@ msgstr "پیش‌فاکتور خود را ارسال کنید"
#: erpnext/manufacturing/doctype/job_card/job_card.py:1570
msgid "Submitted Job Card cannot be processed."
msgstr ""
msgstr "کارت شغلی ارسال‌شده قابل پردازش نیست."
#. Label of the subscription_section (Section Break) field in DocType 'Payment
#. Request'
@@ -53733,7 +53733,7 @@ msgstr "جدول برای آیتم که در وب سایت نشان داده خ
#: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:312
#: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:329
msgid "Table {0}"
msgstr ""
msgstr "جدول {0}"
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
@@ -54133,7 +54133,7 @@ msgstr "شناسه مالیاتی: {0}"
#. Label of the taxation_section (Section Break) field in DocType 'Supplier'
#: erpnext/buying/doctype/supplier/supplier.json
msgid "Tax Identification"
msgstr ""
msgstr "شناسایی مالیات"
#. Label of a Card Break in the Invoicing Workspace
#: erpnext/accounts/workspace/invoicing/invoicing.json
@@ -55545,7 +55545,7 @@ msgstr "هنگام انجام اقدام خطایی رخ داد."
#: banking/src/components/ui/error-banner.tsx:21
msgid "There was an error."
msgstr ""
msgstr "خطایی رخ داده است."
#: erpnext/accounts/doctype/bank/bank.js:112
#: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js:119

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: hello@frappe.io\n"
"POT-Creation-Date: 2026-06-14 10:35+0000\n"
"PO-Revision-Date: 2026-06-14 17:01\n"
"PO-Revision-Date: 2026-06-18 18:25\n"
"Last-Translator: hello@frappe.io\n"
"Language-Team: Croatian\n"
"MIME-Version: 1.0\n"
@@ -1102,7 +1102,7 @@ msgstr "Klijent mora imati primarni kontakt e-poštu."
#. Description of the 'Disabled' (Check) field in DocType 'Product Bundle'
#: erpnext/selling/doctype/product_bundle/product_bundle.json
msgid "A disabled Product Bundle cannot be selected in transactions."
msgstr ""
msgstr "Onemogućeni Paket Artikal ne može se odabrati u transakcijama."
#: erpnext/stock/doctype/delivery_trip/delivery_trip.py:59
msgid "A driver must be set to submit."
@@ -4487,7 +4487,7 @@ msgstr "Dopusti Uređivanje Količine Jedinice Zaliha za Dokumente Prodaje"
#. DocType 'Stock Settings'
#: erpnext/stock/doctype/stock_settings/stock_settings.json
msgid "Allow to edit stock UOM qty for Stock Entry"
msgstr ""
msgstr "Omogući uređivanje količine jedinice zaliha za Unos Zaliha"
#. Label of the allow_to_make_quality_inspection_after_purchase_or_delivery
#. (Check) field in DocType 'Stock Settings'
@@ -4597,7 +4597,7 @@ msgstr "Alternativni Artikal"
#: erpnext/stock/report/item_where_used/item_where_used.py:427
msgid "Alternative For Item"
msgstr ""
msgstr "Artikal Alternativa"
#. Label of the alternative_item_code (Link) field in DocType 'Item
#. Alternative'
@@ -6918,7 +6918,7 @@ msgstr "Alat Poređenja Sastavnica"
#: erpnext/stock/report/item_where_used/item_where_used.py:178
msgid "BOM Component"
msgstr ""
msgstr "Komponenta Sastavnice"
#. Label of the bom_conf_tab (Tab Break) field in DocType 'BOM'
#: erpnext/manufacturing/doctype/bom/bom.json
@@ -6949,7 +6949,7 @@ msgstr "Artikal Sastavnice Konstruktora"
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:392
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:535
msgid "BOM Creator Item with name {0} does not exist"
msgstr ""
msgstr "Artikal Sastavnice s nazivom {0} ne postoji"
#. Label of the bom_detail_no (Data) field in DocType 'Purchase Receipt Item
#. Supplied'
@@ -7049,7 +7049,7 @@ msgstr "Operativno Vrijeme Sastavnice"
#: erpnext/stock/report/item_where_used/item_where_used.py:248
msgid "BOM Output"
msgstr ""
msgstr "Sastavnica"
#: erpnext/stock/report/item_prices/item_prices.py:60
msgid "BOM Rate"
@@ -8222,13 +8222,13 @@ msgstr "Datum Fakture"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Bill Even If Previous Invoice Unpaid"
msgstr ""
msgstr "Fakturiraj čak i ako prethodna faktura nije plaćena"
#. Option for the 'Generate Invoice At' (Select) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Bill N days before period start"
msgstr ""
msgstr "Fakturiraj N dana prije početka perioda"
#. Label of the bill_no (Data) field in DocType 'Journal Entry'
#. Label of the bill_no (Data) field in DocType 'Subcontracting Receipt'
@@ -8410,13 +8410,13 @@ msgstr "e-pošta Fakture"
#. Label of the billing_heatmap (HTML) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing Heatmap"
msgstr ""
msgstr "Toplinska mapa Fakturisanja"
#. Label of the billing_history_section (Section Break) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing History"
msgstr ""
msgstr "Povijest Fakturiranja"
#. Label of the billing_hours (Float) field in DocType 'Sales Invoice
#. Timesheet'
@@ -8450,7 +8450,7 @@ msgstr "Faktura Interval u Planu pretplate mora biti Mjesec koji prati kalendars
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Billing Period"
msgstr ""
msgstr "Razdoblje Fakturiranja"
#. Label of the billing_rate (Currency) field in DocType 'Activity Cost'
#. Label of the billing_rate (Currency) field in DocType 'Timesheet Detail'
@@ -9547,7 +9547,7 @@ msgstr "Otkaži Pretplatu nakon perioda odgode"
#. Label of the cancel_at_period_end (Check) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Cancel When Period Ends"
msgstr ""
msgstr "Otkaži po završetku razdoblja"
#. Label of the cancelation_date (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
@@ -9556,7 +9556,7 @@ msgstr "Datum Otkazivanja"
#: erpnext/manufacturing/doctype/job_card/job_card.py:1567
msgid "Cancelled Job Card cannot be processed."
msgstr ""
msgstr "Otkazani Radni Nalog ne može se obraditi."
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:76
msgid "Cannot Assign Cashier"
@@ -9691,7 +9691,7 @@ msgstr "Nije moguće pretvoriti u Grupu jer je odabran Tip Računa."
#: erpnext/accounts/doctype/sales_invoice/mapper.py:372
msgid "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. Please check the existing linked {2}s."
msgstr ""
msgstr "Nije moguće stvoriti međutvrtku {0}. Svi artikli u izvoru {1} već su u potpunosti fakturirani. Provjeri postojeće povezane {2}."
#: erpnext/stock/doctype/purchase_receipt/services/reservation.py:49
msgid "Cannot create Stock Reservation Entries for future dated Purchase Receipts."
@@ -9770,7 +9770,7 @@ msgstr "Nije moguće omogućiti račun zaliha po stavkama jer postoje postojeći
#: erpnext/crm/doctype/crm_settings/crm_settings.py:37
msgid "Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
msgstr ""
msgstr "Nije moguće omogućiti stvaranje prilike iz Kontaktirajte Nas jer je obrazac Kontaktirajte Nas onemogućen."
#: erpnext/selling/doctype/sales_order/sales_order.py:624
#: erpnext/selling/doctype/sales_order/sales_order.py:647
@@ -9878,7 +9878,7 @@ msgstr "Brisanje nije moguće. Drugo brisanje {0} je već u redu čekanja/pokre
#: erpnext/manufacturing/doctype/job_card/job_card.py:922
msgid "Cannot submit Job Card {0} while it is On Hold. Please resume and complete the job before submission."
msgstr ""
msgstr "Nije moguće podnijeti Radni Nalog {0} dok je na čekanju. Nastavi i završi posao prije podnošenja."
#: erpnext/accounts/services/child_item_update.py:283
msgid "Cannot update rate as item {0} is already ordered or purchased against this quotation"
@@ -13410,7 +13410,7 @@ msgstr "Kreiraj novi trag"
#: erpnext/selling/doctype/product_bundle/product_bundle.js:16
msgid "Create New Version"
msgstr ""
msgstr "Stvori novu verziju"
#: banking/src/components/common/LinkFieldCombobox.tsx:284
msgid "Create New {0}"
@@ -14291,12 +14291,12 @@ msgstr "Trenutni Valuta kurs"
#. Label of the current_invoice_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Current Invoice End"
msgstr ""
msgstr "Trenutni Završni Datum Fakture"
#. Label of the current_invoice_start (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Current Invoice Start"
msgstr ""
msgstr "Trenutni Početni Datum Fakture"
#. Label of the current_level (Int) field in DocType 'BOM Update Log'
#: erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -17243,7 +17243,7 @@ msgstr "Onemogućeni Bankovni Račun"
#: erpnext/stock/doctype/packed_item/packed_item.py:216
msgid "Disabled Product Bundle"
msgstr ""
msgstr "Onemogući Paket Artikala"
#: erpnext/stock/utils.py:434
msgid "Disabled Warehouse {0} cannot be used for this transaction."
@@ -18939,7 +18939,7 @@ msgstr "Omogući Program Bodova Lojalnosti"
#. DocType 'CRM Settings'
#: erpnext/crm/doctype/crm_settings/crm_settings.json
msgid "Enable Opportunity Creation from Contact Us"
msgstr ""
msgstr "Omogući stvaranje Prilika iz Kontaktiraj Nas obrasca"
#. Label of the enable_parallel_reposting (Check) field in DocType 'Stock
#. Reposting Settings'
@@ -20225,7 +20225,7 @@ msgstr "Nije uspjelo ažuriranje prioriteta pravila"
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:521
msgid "Failed to update subscription status for {0} {1}"
msgstr ""
msgstr "Nije uspjelo ažuriranje statusa pretplate za {0} {1}"
#. Label of the failure_date (Datetime) field in DocType 'Asset Repair'
#: erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -20765,7 +20765,7 @@ msgstr "Gotov Proizvod {0} ne odgovara Radnom Nalogu {1}"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:71
msgid "Finished good quantity being consumed ({0} in stock UOM) must equal the quantity to disassemble ({1}). Do not change the UOM, conversion factor or quantity of the finished good row."
msgstr ""
msgstr "Količina gotovog proizvoda koja se troši ({0} u jedinici zaliha) mora biti jednaka količini za rastavljanje ({1}). Ne mijenjaj jedinicu, faktor konverzije ili količinu u redu gotovog proizvoda."
#: erpnext/selling/doctype/sales_order/sales_order.js:615
msgid "First Delivery Date"
@@ -23435,13 +23435,13 @@ msgstr "Ako je odbrano, ovaj artikal se tretira kao direktna dostava u Prodajnim
#. Description of the 'Update Stock' (Check) field in DocType 'Sales Invoice'
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.json
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately."
msgstr ""
msgstr "Ako je oodabrano, ažurira inventar; zalihe i knjigovodstveni unosi se kreiraju zajedno. Ostavi neodabrano ako se Dostavnica kreira zasebno."
#. Description of the 'Update Stock' (Check) field in DocType 'Purchase
#. Invoice'
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
msgid "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately."
msgstr ""
msgstr "Ako je odabrano, ažurira se inventar; unosi zaliha i knjigoovodstva se kreiraju zajedno. Ostavi neodabrano ako Kupovni Račun kreira zasebno."
#: erpnext/public/js/setup_wizard.js:56
msgid "If checked, we will create demo data for you to explore the system. This demo data can be erased later."
@@ -25255,7 +25255,7 @@ msgstr "Nevažeća Tvrtka za transakcije između tvrtki."
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:972
msgid "Invalid Configuration"
msgstr ""
msgstr "Nevažeća Konfiguracija"
#: erpnext/accounts/services/taxes.py:294
#: erpnext/assets/doctype/asset/asset.py:361
@@ -25273,12 +25273,12 @@ msgstr "Nevažeći Datum Dostave"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:110
msgid "Invalid Disassembly Item"
msgstr ""
msgstr "Nevažeći Artikala za Rastavljanje"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:76
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:125
msgid "Invalid Disassembly Quantity"
msgstr ""
msgstr "Nevažeća Količina za Rastavljanje"
#: erpnext/selling/page/point_of_sale/pos_item_cart.js:414
msgid "Invalid Discount"
@@ -26144,7 +26144,7 @@ msgstr "Je Fantomska Stavka"
#: erpnext/selling/doctype/sales_order_item/sales_order_item.json
#: erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
msgid "Is Product Bundle"
msgstr ""
msgstr "Je Paket Artikala"
#. Label of the po_required (Select) field in DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -27652,7 +27652,7 @@ msgstr "Detalji Težine Artikla"
#. Name of a report
#: erpnext/stock/report/item_where_used/item_where_used.json
msgid "Item Where Used"
msgstr ""
msgstr "Gdje se koristi Artikal"
#. Label of a Link in the Buying Workspace
#. Name of a report
@@ -27775,7 +27775,7 @@ msgstr "Artikal {0} dodan je više puta pod isti nadređeni artikal {1} u redovi
#: erpnext/selling/doctype/product_bundle/product_bundle.js:54
msgid "Item {0} already has an active Product Bundle ({1}). Submitting this will create a new version and deactivate {1}."
msgstr ""
msgstr "{0} već ima aktivan Paket Artikala ({1}). Podnošenjem ovog stvorit će se nova verzija i deaktivirati {1}."
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:119
msgid "Item {0} cannot be added as a sub-assembly of itself"
@@ -28106,7 +28106,7 @@ msgstr "Artikal Radne Kartice"
#: erpnext/manufacturing/doctype/job_card/job_card.py:925
msgid "Job Card On Hold"
msgstr ""
msgstr "Radni Nalog je na čekanju"
#. Name of a DocType
#: erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
@@ -30309,7 +30309,7 @@ msgstr "Usklađeno"
#: erpnext/stock/report/item_where_used/item_where_used.py:57
msgid "Matched Field"
msgstr ""
msgstr "Usklađeno polje"
#. Label of the matched_transaction_rule (Link) field in DocType 'Bank
#. Transaction'
@@ -30928,7 +30928,7 @@ msgstr "Metar/Sekunda"
#: erpnext/manufacturing/doctype/workstation/workstation.py:546
msgid "Method {0} is not allowed to be run on a Job Card."
msgstr ""
msgstr "Metodu {0} nije dopušteno pokretati na Radnom Nalogu."
#. Name of a UOM
#: erpnext/setup/setup_wizard/data/uom_data.json
@@ -32258,13 +32258,13 @@ msgstr "Newton"
#. Label of the next_billing_period_end (Date) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Next Billing Period End"
msgstr ""
msgstr "Sljedeći Perioda Fakturiranja Završava"
#. Label of the next_billing_period_start (Date) field in DocType
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Next Billing Period Start"
msgstr ""
msgstr "Sljedeći Perioda Fakturiranja Počinje"
#. Label of the next_depreciation_date (Date) field in DocType 'Asset'
#: erpnext/assets/doctype/asset/asset.json
@@ -33476,7 +33476,7 @@ msgstr "Samo jedna operacija može imati odabranu opciju 'Je li Gotov Proizvod'
#. Description of the 'Is Active' (Check) field in DocType 'Product Bundle'
#: erpnext/selling/doctype/product_bundle/product_bundle.json
msgid "Only one version of a Product Bundle can be active at a time for a given Parent Item. Activating a version deactivates the previously active one."
msgstr ""
msgstr "Samo jedna verzija Paketa Artikala može biti aktivna u datom trenutku za dati Nadređeni Artikal. Aktiviranje verzije deaktivira prethodno aktivnu verziju."
#: erpnext/stock/doctype/stock_entry/stock_entry.py:719
msgid "Only one {0} entry can be created against the Work Order {1}"
@@ -39083,7 +39083,7 @@ msgstr "Vremenska oznaka knjiženja mora biti nakon {0}"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Postpaid (bill at period end)"
msgstr ""
msgstr "Naknadno Plaćanje (faktura na kraju razdoblja)"
#. Description of a DocType
#: erpnext/crm/doctype/opportunity/opportunity.json
@@ -39186,7 +39186,7 @@ msgstr "Preferirana e-pošta"
#. 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Prepaid (bill at period start)"
msgstr ""
msgstr "Unaprijed Plaćeno (faktura na početku razdoblja)"
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:34
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:51
@@ -40174,7 +40174,7 @@ msgstr "Stanje Paketa Proizvoda"
#: erpnext/stock/report/item_where_used/item_where_used.py:278
msgid "Product Bundle Component"
msgstr ""
msgstr "Komponenta Paketa Artikala"
#. Label of the product_bundle_help (HTML) field in DocType 'POS Invoice'
#. Label of the product_bundle_help (HTML) field in DocType 'Sales Invoice'
@@ -40199,7 +40199,7 @@ msgstr "Artikal Paketa Proizvoda"
#: erpnext/stock/report/item_where_used/item_where_used.py:305
msgid "Product Bundle Parent"
msgstr ""
msgstr "Nadređeni Paket Artikala"
#. Description of the 'Product Bundle' (Link) field in DocType 'Purchase
#. Invoice Item'
@@ -40213,15 +40213,15 @@ msgstr ""
#: erpnext/stock/doctype/packed_item/packed_item.json
#: erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
msgid "Product Bundle version this row was packed from"
msgstr ""
msgstr "Verzija Paketa Artikala iz koje je ovaj red preuzet iz"
#: erpnext/stock/doctype/packed_item/packed_item.py:453
msgid "Product Bundle {0} is disabled and cannot be used in transactions."
msgstr ""
msgstr "Paket Artikala {0} je onemogućen i ne može se koristiti u transakcijama."
#: erpnext/stock/doctype/packed_item/packed_item.py:450
msgid "Product Bundle {0} is not submitted"
msgstr ""
msgstr "Paket Artikala {0} nije podnešen"
#. Label of the product_discount_scheme_section (Section Break) field in
#. DocType 'Pricing Rule'
@@ -43881,7 +43881,7 @@ msgstr "Osvježite Plaid Link"
#. Option for the 'Status' (Select) field in DocType 'Subscription'
#: erpnext/accounts/doctype/subscription/subscription.json
msgid "Refunded"
msgstr ""
msgstr "Povraćeno"
#: erpnext/stock/reorder_item.py:390
msgid "Regards,"
@@ -43992,7 +43992,7 @@ msgstr "Povezano"
#: erpnext/stock/report/item_where_used/item_where_used.py:50
msgid "Related Item"
msgstr ""
msgstr "Povezani Artikal"
#. Label of the relation (Data) field in DocType 'Employee'
#: erpnext/setup/doctype/employee/employee.json
@@ -46064,7 +46064,7 @@ msgstr "Red #{0}: Gotov Proizvod artikla nije navedena zaservisni artikal {1}"
#: erpnext/manufacturing/doctype/bom/bom.py:371
msgid "Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table."
msgstr ""
msgstr "Red #{0}: Artikal Gotovog Proizvoda {1} ne može se dodati u tablicu Sekundarnih Artikala."
#: erpnext/buying/doctype/purchase_order/services/subcontracting.py:28
#: erpnext/selling/doctype/sales_order/services/subcontracting.py:27
@@ -46155,7 +46155,7 @@ msgstr "Red #{0}: Artikal {1} nije artikal na zalihama"
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:106
msgid "Row #{0}: Item {1} is not part of the source manufacture entry and cannot be added to this disassembly."
msgstr ""
msgstr "Red #{0}: Artikal {1} nije dio unosa izvornog proizvođača i ne može se dodati ovom rastavljanju."
#: erpnext/controllers/subcontracting_inward_controller.py:79
msgid "Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
@@ -46167,7 +46167,7 @@ msgstr "Red #{0}: Artikla {1} se ne slaže. Promjena koda artikla nije dozvoljen
#: erpnext/stock/doctype/stock_entry/stock_entry_handler/disassemble.py:115
msgid "Row #{0}: Item {1} quantity ({2} in stock UOM) does not match the quantity derived from the source ({3}). Do not change the UOM, conversion factor or quantity of disassembly rows."
msgstr ""
msgstr "Red #{0}: Količina artikla {1} ({2} u jedinici zaliha) ne odgovara količini izvedeno iz izvora ({3}). Ne mijenjaj jedinicu, faktor konverzije ili količinu redova za rastavljanje."
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:780
msgid "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher"
@@ -46233,7 +46233,7 @@ msgstr "Red #{0}: Postotnii Gubitka Procesa treba da bude manji od 100% za {1} a
#: erpnext/stock/doctype/packed_item/packed_item.py:213
msgid "Row #{0}: Product Bundle {1} is disabled and cannot be used in transactions."
msgstr ""
msgstr "Red #{0}: Paket Artikal {1} je onemogućen i ne može se koristiti u transakcijama."
#: erpnext/public/js/utils/barcode_scanner.js:425
msgid "Row #{0}: Qty increased by {1}"
@@ -49490,7 +49490,7 @@ msgstr "Serijski i Šaržni Paket {0} nije podnešen"
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:2251
msgid "Serial and Batch Bundle {0} is submitted and its entries cannot be modified."
msgstr ""
msgstr "Serijski i Šaržni Paket {0} je podnešen i njegovi unosi se ne mogu mijenjati."
#. Label of the section_break_45 (Section Break) field in DocType
#. 'Subcontracting Receipt Item'
@@ -52668,7 +52668,7 @@ msgstr "Podizvođačka Dostava"
#: erpnext/stock/report/item_where_used/item_where_used.py:362
msgid "Subcontracting Finished Good"
msgstr ""
msgstr "Podizvođački Gotov Proizvod"
#. Label of the subcontracting_inward_tab (Tab Break) field in DocType 'Selling
#. Settings'
@@ -52852,7 +52852,7 @@ msgstr "Podizvođački Prodajni Nalog"
#: erpnext/stock/report/item_where_used/item_where_used.py:336
msgid "Subcontracting Service Item"
msgstr ""
msgstr "Podizvođački Uslužni Artikal"
#. Label of the subcontract (Tab Break) field in DocType 'Buying Settings'
#: erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -52900,7 +52900,7 @@ msgstr "Podnesi Ponudu"
#: erpnext/manufacturing/doctype/job_card/job_card.py:1570
msgid "Submitted Job Card cannot be processed."
msgstr ""
msgstr "Podnešeni Radni Nalog ne može biti obrađen."
#. Label of the subscription_section (Section Break) field in DocType 'Payment
#. Request'
@@ -61720,7 +61720,7 @@ msgstr "Ne možete podnijeti nalog bez plaćanja."
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:968
msgid "You cannot update stock for a Debit Note. A Debit Note is a financial document that should not affect inventory. Please disable 'Update Stock'."
msgstr ""
msgstr "Ne možete ažurirati zalihe za Terećenje. Terećenje je financijski dokument koji ne bi trebao utjecati na zalihe. Onemogući opciju 'Ažuriraj Zalihe'."
#: erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py:106
msgid "You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
@@ -62834,7 +62834,7 @@ msgstr "{0}, završi operaciju {1} prije operacije {2}."
#: erpnext/selling/report/sales_person_wise_transaction_summary/sales_person_wise_transaction_summary.py:61
msgid "{0}, {1} or {2} are the only allowed options."
msgstr ""
msgstr "{0}, {1} ili {2} su jedine dopuštene opcije."
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:525
msgid "{0}: Child table (auto-deleted with parent)"

View File

@@ -373,13 +373,6 @@ class BOM(WebsiteGenerator):
).format(item.idx, get_link_to_form("Item", item.item_code))
)
if not item.qty:
frappe.throw(
_("Row #{0}: Quantity should be greater than 0 for {1} Item {2}").format(
item.idx, item.secondary_item_type, get_link_to_form("Item", item.item_code)
)
)
if item.process_loss_per >= 100:
frappe.throw(
_("Row #{0}: Process Loss Percentage should be less than 100% for {1} Item {2}").format(

View File

@@ -138,8 +138,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"non_negative": 1,
"reqd": 1
"non_negative": 1
},
{
"default": "0",
@@ -218,7 +217,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-06-03 16:10:18.474377",
"modified": "2026-06-16 16:51:40.000000",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Secondary Item",

View File

@@ -4,7 +4,7 @@
"""BOM explosion helpers for Production Plan material planning."""
import frappe
from frappe.query_builder.functions import IfNull, Sum
from frappe.query_builder.functions import IfNull, Max, Min, Sum
from erpnext.manufacturing.doctype.production_plan.services.planning_queries import get_uom_conversion_factor
@@ -38,22 +38,25 @@ def _exploded_items_query(company, bom_no, include_non_stock_items, planned_qty)
def _exploded_item_columns(bei, bom, item, item_default, item_uom, planned_qty):
# only item_code/stock_uom are grouped; the rest are functionally dependent on the grouped item
# or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY valid on postgres with the same
# value MySQL picked.
return [
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
item.item_name,
item.name.as_("item_code"),
bei.description,
Max(item.item_name).as_("item_name"),
Max(item.name).as_("item_code"),
Max(bei.description).as_("description"),
bei.stock_uom,
item.min_order_qty,
bei.source_warehouse,
item.default_material_request_type,
item.min_order_qty,
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(bei.source_warehouse).as_("source_warehouse"),
Max(item.default_material_request_type).as_("default_material_request_type"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(item_default.default_warehouse).as_("default_warehouse"),
Max(item.purchase_uom).as_("purchase_uom"),
Max(item_uom.conversion_factor).as_("conversion_factor"),
Max(item.safety_stock).as_("safety_stock"),
Max(bom.item).as_("main_bom_item"),
Max(bom.name).as_("main_bom"),
]
@@ -106,30 +109,34 @@ def _subitems_query(company, bom_no, include_non_stock_items, parent_qty, planne
.select(*_subitem_columns(bom_item, bom, item, item_default, item_uom, parent_qty, planned_qty))
.where(_subitem_filter(bom_item, bom, item, bom_no, include_non_stock_items))
.groupby(bom_item.item_code)
.orderby(bom_item.idx)
# idx is not grouped; Min() preserves the original ordering and is valid on postgres
.orderby(Min(bom_item.idx))
).run(as_dict=True)
def _subitem_columns(bom_item, bom, item, item_default, item_uom, parent_qty, planned_qty):
qty = IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_("qty")
# only item_code is grouped; the rest are functionally dependent on the grouped item (item
# attributes) or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY valid on postgres
# while returning the same value MySQL picked.
return [
bom_item.item_code,
item.default_material_request_type,
item.item_name,
Max(item.default_material_request_type).as_("default_material_request_type"),
Max(item.item_name).as_("item_name"),
qty,
item.is_sub_contracted_item.as_("is_sub_contracted"),
bom_item.source_warehouse,
item.default_bom.as_("default_bom"),
bom_item.description.as_("description"),
bom_item.stock_uom.as_("stock_uom"),
item.min_order_qty.as_("min_order_qty"),
item.safety_stock.as_("safety_stock"),
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
bom_item.is_phantom_item,
Max(item.is_sub_contracted_item).as_("is_sub_contracted"),
Max(bom_item.source_warehouse).as_("source_warehouse"),
Max(item.default_bom).as_("default_bom"),
Max(bom_item.description).as_("description"),
Max(bom_item.stock_uom).as_("stock_uom"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(item.safety_stock).as_("safety_stock"),
Max(item_default.default_warehouse).as_("default_warehouse"),
Max(item.purchase_uom).as_("purchase_uom"),
Max(item_uom.conversion_factor).as_("conversion_factor"),
Max(bom.item).as_("main_bom_item"),
Max(bom.name).as_("main_bom"),
Max(bom_item.is_phantom_item).as_("is_phantom_item"),
]

View File

@@ -4,7 +4,7 @@
"""Sub-assembly resolution helpers for Production Plan."""
import frappe
from frappe.query_builder.functions import IfNull, Sum
from frappe.query_builder.functions import IfNull, Max, Sum
from frappe.utils import flt
from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children
@@ -184,24 +184,27 @@ def _sub_assembly_rm_query(company, bom_no, include_non_stock_items, planned_qty
def _sub_assembly_rm_columns(bei, bom, item, item_default, item_uom, planned_qty):
# only item_code/stock_uom are grouped; every other column is functionally dependent on the
# grouped item (item attributes) or arbitrary per BOM Item on MySQL -> Max() keeps the GROUP BY
# valid on postgres while returning the same value MySQL picked.
return [
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
item.item_name,
item.name.as_("item_code"),
bei.description,
Max(item.item_name).as_("item_name"),
Max(item.name).as_("item_code"),
Max(bei.description).as_("description"),
bei.stock_uom,
bei.is_phantom_item,
bei.bom_no,
item.min_order_qty,
bei.source_warehouse,
item.default_material_request_type,
item.min_order_qty,
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
bom.name.as_("main_bom"),
Max(bei.is_phantom_item).as_("is_phantom_item"),
Max(bei.bom_no).as_("bom_no"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(bei.source_warehouse).as_("source_warehouse"),
Max(item.default_material_request_type).as_("default_material_request_type"),
Max(item.min_order_qty).as_("min_order_qty"),
Max(item_default.default_warehouse).as_("default_warehouse"),
Max(item.purchase_uom).as_("purchase_uom"),
Max(item_uom.conversion_factor).as_("conversion_factor"),
Max(item.safety_stock).as_("safety_stock"),
Max(bom.item).as_("main_bom_item"),
Max(bom.name).as_("main_bom"),
]

View File

@@ -56,11 +56,12 @@ def _item_master_details(item):
def _item_is_alive(item_table):
return (
item_table.end_of_life.isnull()
| (item_table.end_of_life == "0000-00-00")
| (item_table.end_of_life > nowdate())
)
# "not set" end_of_life is NULL on postgres (the MariaDB zero-date '0000-00-00' is an invalid
# date constant there), so only add the zero-date term on MariaDB.
is_alive = item_table.end_of_life.isnull() | (item_table.end_of_life > nowdate())
if frappe.db.db_type != "postgres":
is_alive |= item_table.end_of_life == "0000-00-00"
return is_alive
def _default_bom_for_item(item, project):

View File

@@ -11,6 +11,7 @@ are called from other modules.
import frappe
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import (
cint,
date_diff,
@@ -268,13 +269,17 @@ class OperationsService:
self.doc.actual_end_date = max(end_dates)
def _set_dates_from_stock_entries(self):
data = frappe.get_all(
"Stock Entry",
fields=[{"TIMESTAMP": ["posting_date", "posting_time"], "as": "posting_datetime"}],
filters={
"work_order": self.doc.name,
"purpose": ("in", ["Material Transfer for Manufacture", "Manufacture"]),
},
# {"TIMESTAMP": [...]} renders MySQL's TIMESTAMP(date, time), invalid on postgres; use the
# portable CombineDatetime via query builder instead.
se = frappe.qb.DocType("Stock Entry")
data = (
frappe.qb.from_(se)
.select(CombineDatetime(se.posting_date, se.posting_time).as_("posting_datetime"))
.where(
(se.work_order == self.doc.name)
& (se.purpose.isin(["Material Transfer for Manufacture", "Manufacture"]))
)
.run(as_dict=True)
)
if not data:
return

View File

@@ -19,6 +19,7 @@ from erpnext.manufacturing.doctype.work_order.services.reservation import (
get_consumed_qty,
get_row_wise_serial_batch,
)
from erpnext.manufacturing.doctype.work_order.services.status import StatusService
from erpnext.stock.utils import get_bin, get_latest_stock_qty
@@ -146,6 +147,38 @@ class RequiredItemsService:
row, transferred_qty, row_wise_serial_batch
)
self.recompute_material_transferred_for_manufacturing(transferred_items)
def recompute_material_transferred_for_manufacturing(self, transferred_items):
"""Set material_transferred_for_manufacturing based on actual item-level transfers, not fg_completed_qty."""
# When fg_completed_qty > 0 (direct stock entries, excess transfer), preserve the
# SUM(fg_completed_qty) approach so excess-transfer tracking works correctly.
sum_fg_completed_qty = StatusService(self.doc).get_transferred_or_manufactured_qty(
"Material Transfer for Manufacture", "material_transferred_for_manufacturing"
)
if sum_fg_completed_qty:
self.doc.db_set("material_transferred_for_manufacturing", sum_fg_completed_qty)
return
# Pick list flow sets fg_completed_qty=0; use min-fraction of actual item transfers
# so partial availability does not prematurely mark the work order as fully transferred.
required_by_item = {}
for row in self.doc.required_items:
if not row.include_item_in_manufacturing or flt(row.required_qty) <= 0:
continue
required_by_item[row.item_code] = required_by_item.get(row.item_code, 0.0) + flt(row.required_qty)
if not required_by_item:
return
min_fraction = min(
flt(transferred_items.get(item_code) or 0) / required_qty
for item_code, required_qty in required_by_item.items()
)
min_fraction = min(min_fraction, 1.0)
material_transferred = min_fraction * flt(self.doc.qty)
self.doc.db_set("material_transferred_for_manufacturing", material_transferred)
def update_returned_qty(self):
returned_dict = self._material_transfer_qty_by_item(is_return=1)
for row in self.doc.required_items:
@@ -158,7 +191,13 @@ class RequiredItemsService:
frappe.qb.from_(ste)
.inner_join(ste_child)
.on(ste_child.parent == ste.name)
.select(ste_child.item_code, ste_child.original_item, fn.Sum(ste_child.transfer_qty).as_("qty"))
# original_item is arbitrary per grouped item_code on MySQL -> Max() keeps the GROUP BY valid
# on postgres while returning the same value (it is only used as a dict key fallback below)
.select(
ste_child.item_code,
fn.Max(ste_child.original_item).as_("original_item"),
fn.Sum(ste_child.transfer_qty).as_("qty"),
)
.where(self._material_transfer_filter(ste, is_return))
.groupby(ste_child.item_code)
)

Some files were not shown because too many files have changed in this diff Show More