Compare commits

..

248 Commits

Author SHA1 Message Date
Soham Kulkarni
f8951034be Merge pull request #58729 from sokumon/sidebar-changes
fix: more sidebar improvements
2026-09-03 15:16:36 +05:30
sokumon
56caf3c770 fix: more sidebar improvements 2026-09-03 14:46:30 +05:30
Sumit Jain
3b9535ed91 Merge pull request #58726 from frappe/fix-account-tree
feat(accounts): disabled marker and cost center parity in tree views
2026-09-03 14:05:18 +05:30
Sumit Jain
f8c53be376 fix(accounts): drop unsupported badge theme on account number 2026-09-03 13:53:27 +05:30
Sumit Jain
a1b402020e feat(accounts): enhance account and cost center tree views with additional flags and labels 2026-09-03 13:38:20 +05:30
Diptanil Saha
e3a976f115 fix(setup): strict permissions for transaction deletion record (#58687) 2026-09-03 06:28:23 +00:00
dependabot[bot]
68666c2ed0 chore(deps): bump browserslist from 4.28.4 to 4.28.8 in /banking (#58721)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.4 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.4...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 05:33:45 +00:00
Diptanil Saha
717af27559 ci: authenticate github clones in install.sh (#58718) 2026-09-03 10:50:32 +05:30
Sagar Vora
4944df8733 Merge pull request #58697 from sagarvora/frt-fixes
fix!: improve validation in financial report template
2026-09-02 17:08:04 +05:30
Sagar Vora
19aa3b20e0 fix!: improve validation in financial report template 2026-09-02 16:46:02 +05:30
rohitwaghchaure
09f5e76b77 fix: rearrange fields in BOM Operation (#58683) 2026-09-02 16:44:16 +05:30
rohitwaghchaure
646c7d042d fix: check write permission in whitelisted document methods (#58689)
* fix: check write permission in whitelisted document methods

* fix: keep production plan status roll-up permission agnostic
2026-09-02 15:40:10 +05:30
Shllokkk
3e08a085b9 Merge pull request #58684 from Shllokkk/lead-add-to-prospect-read-permission-check
fix(crm): check read permission on lead in add_lead_to_prospect
2026-09-02 14:54:01 +05:30
Krishna Pramod Shirsath
2918e98a2b fix(stock): allow creating stock closing balances (#58590) 2026-09-02 08:24:46 +00:00
Shllokkk
02fcdc0337 fix(crm): check read permission on lead in add_lead_to_prospect 2026-09-02 13:50:38 +05:30
Krishna Pramod Shirsath
a2071a6fdd fix: filter cancelled BOMs in BOM Stock Analysis (#58647) 2026-09-02 13:33:15 +05:30
Vishnu Priya Baskaran
dbe153a15e fix: include payment deductions in sales/purchase register ledger bal… (#58437) 2026-09-02 12:30:08 +05:30
Vishnu Priya Baskaran
f16f249a38 fix(pos): use company-currency change amount when netting pos gl entries (#58599) 2026-09-02 12:22:24 +05:30
Vishnu Priya Baskaran
9cb736a271 fix(stock): prorate landed cost charge into transaction currency (#58575)
* fix(stock): prorate landed cost charge into transaction currency

* test(stock): assert landed cost charge transaction-currency amount
2026-09-02 12:10:22 +05:30
Shllokkk
c93815b4ae fix: filter fully ordered items when creating RFQ from Material Request (#58534) 2026-09-02 06:15:04 +00:00
Pandiyan P
2b54a581f8 fix(stock): preserve exchange gain loss journals in lcv (#58505) 2026-09-02 11:44:38 +05:30
Mihir Kandoi
199cae9496 fix(stock): subtract stock qty of same-document rows from batch availability (#58669)
* fix(stock): subtract stock qty of same-document rows from batch availability

filter_batches subtracted a row's transaction-UOM qty from batch quantities that
are in the stock UOM, so a row in an alternate UOM freed less of the batch than
it consumes and the auto-pick could assign a batch that cannot cover the new
row.

* test(stock): cover batch availability with alternate UOM rows
2026-09-02 11:42:37 +05:30
Mihir Kandoi
9261c9b47f fix(stock): assign batch_no only when the first batch covers the full qty (#58668)
* fix(stock): assign batch_no only when the first batch covers the full qty

The auto-pick loop reduced the requested qty per batch and left the last
visited batch on the row, so a qty spanning batches got a batch that could
not fulfil it and failed at submit with a misleading negative-stock error.

Assign the first batch in pick order only when it alone covers the qty.
Otherwise leave batch_no empty so the auto-created Serial and Batch Bundle
splits the qty across batches at submit.

Batches are queried without qty so filter_batches subtracts rows already in
the document from the uncapped batch quantities. Querying on a copy also
stops get_auto_batch_nos from clearing warehouse on the kwargs later used to
pick serial nos.

Fixes #58640

* test(stock): cover batch auto-pick when qty spans batches

* fix(stock): pick serial nos across batches when no batch covers the qty

With batch_no left empty for a qty that spans batches, the serial pick for a
serialised and batched item filtered on [None] and returned nothing, leaving
the row with neither identity. Skip the batch filter when there is no batch so
the serial nos are picked in the configured order across batches; the bundle
built at submit derives each serial's batch.

* test(stock): cover serial pick across batches for batched serial items
2026-09-02 11:42:37 +05:30
Mihir Kandoi
0230879501 ci: allow shallow fetch when fast-forwarding the cached bench (#58666) 2026-09-02 11:20:21 +05:30
kaulith
56a391c522 fix(crm): validate contact email before saving an email campaign (#58667) 2026-09-02 05:34:22 +00:00
Pandiyan P
e74ab38eeb fix(manufacturing): use item warehouses in production plan work orders (#58663) 2026-09-02 04:34:05 +00:00
Pandiyan P
a9b857bdfe fix(stock): ignore cancelled batch entries in valuation (#58659) 2026-09-01 15:49:51 +00:00
Diptanil Saha
a20fc4f931 fix: add missing perm checks on various whitelisted methods (#58652) 2026-09-01 19:12:21 +05:30
Shllokkk
22e3b87621 Merge pull request #58607 from Shllokkk/trial-balance-party-exclude-zero-rounding
fix: round party balances to currency precision in Trial Balance for Party
2026-09-01 15:18:22 +05:30
Mihir Kandoi
e2f24eea15 feat(buying): track supplier quotation order status (#58572) 2026-09-01 08:18:29 +00:00
Pandiyan P
db52e465d0 fix: handle BOM price list currency update (#58638) 2026-09-01 13:31:37 +05:30
Raffael Meyer
5691a7b3c7 fix: explain missing fields when loading party details or taxes (#58627) 2026-09-01 01:31:06 +02:00
Raffael Meyer
0d78261bdf fix: translate label in party validation (#58624) 2026-09-01 00:47:53 +02:00
Diptanil Saha
120e62f208 fix(crm): add missing permission validation on get_contract_template (#58621) 2026-09-01 00:58:58 +05:30
Diptanil Saha
21a3b1c3dc fix: validate POS Settings invoice and search fields on the server (#58611) 2026-08-31 23:43:35 +05:30
Raffael Meyer
d8432d92c8 fix: do not map the same row twice in "Get Items From" (#58617) 2026-08-31 17:15:22 +02:00
Sumit Jain
31761699ae fix: enhance tree view functionality for accounts and cost centers (#58520) 2026-08-31 19:56:24 +05:30
Pandiyan P
7ecfa6b356 fix(stock): correct reservation and pick list quantities (#58613) 2026-08-31 19:38:31 +05:30
Mihir Kandoi
24209ae699 fix(manufacturing): handle duplicate root BOM items (#58614)
* fix(manufacturing): handle duplicate root BOM items

* test(manufacturing): remove duplicate root item test
2026-08-31 14:04:26 +00:00
Pandiyan P
26d000e15f fix(stock): validate serial batch bundle company (#58608) 2026-08-31 12:08:23 +00:00
Mihir Kandoi
dbae23765e fix: keep closed rows out of Update Items (#58609) 2026-08-31 12:03:00 +00:00
Mihir Kandoi
c755e24731 fix: compare updated item quantities in stock UOM (#58603) 2026-08-31 17:05:30 +05:30
Mihir Kandoi
d5789c2e8b feat: close individual transaction items (#57596) 2026-08-31 16:57:48 +05:30
Pandiyan P
0f14f8050f fix: correct bom sorting and stock translations (#58605) 2026-08-31 11:26:42 +00:00
Shllokkk
b1c7657dfa fix: round party balances to currency precision in Trial Balance for Party 2026-08-31 16:43:31 +05:30
rohitwaghchaure
b36895a4c3 feat: production plan visualizer page and summary report fix (#58541)
* fix: production plan summary report tree structure and quantities

* feat: production plan visualizer page

* feat: single screen production plan visualizer with material readiness

* fix: show live stock and received status for production plan materials

* fix: remove duplicate border under production plan visualizer header

* fix: drop page head border on production plan visualizer

* fix: add horizontal margin to production plan visualizer

* fix: apply record level permissions and resolve shared material owners

* fix: list shared raw materials under every finished good that needs them

* feat: open linked documents in a side panel from the visualizer

* fix: never fall back to stored qty when warehouse stock is not readable

* fix: include directly consuming finished goods in material ownership

* fix: show each finished good's own share of shared material demand

* fix: match production plan quantities and labels in the visualizer

* fix: resolve nested sub assembly owners when parent link is missing

* fix: keep every matching owner when resolving sub assemblies by item code

* feat: flat work order list in place of the items to manufacture tree

* fix: flatten items to manufacture rows without changing the table design

* fix: align table numbers, units and progress cells

* fix: scope nested owner resolution to the same sales order

* fix: keep quantity columns numeric and move uom to the item line

* fix: recover all finished goods for consolidated sub assembly rows

* fix: scope raw material owners to the same sales order
2026-08-31 16:24:12 +05:30
Mihir Kandoi
509501c299 feat(manufacturing): add multi-currency support to Blanket Orders (#58472) 2026-08-31 09:45:34 +00:00
Smit Vora
20ea09b4a0 Merge pull request #58483 from ljain112/opening-taxes
fix(taxes): skip tax addition for invoice created from opening invoice tool
2026-08-31 15:05:12 +05:30
Mihir Kandoi
8ca2905a33 feat: valuation method for BOM secondary items (#58431) 2026-08-31 14:51:45 +05:30
Pandiyan P
9087f2cdba fix: widen item name in stock projected qty (#58598) 2026-08-31 08:32:17 +00:00
Mihir Kandoi
6cca7d670b fix: restore isolated loyalty and subcontracting tests (#58587) 2026-08-31 13:40:14 +05:30
Diptanil Saha
4355f8e60e fix(pos): add permission checks on get_invoices (#58591) 2026-08-31 13:23:59 +05:30
Diptanil Saha
0e4b384af1 fix(accounts): added permission checks on get_available_payment_schedules (#58588) 2026-08-31 07:36:01 +00:00
Mihir Kandoi
83dea1a24e test: prevent update_doctypes from exporting files (#58589) 2026-08-31 07:31:41 +00:00
Aarol D'Souza
6d445d94ee Merge pull request #58586 from AarDG10/patch-erpnext
fix(sms_settings): add patch to pre-fill roles into SMS Settings Role Table
2026-08-31 12:27:32 +05:30
Aarol D'Souza
05e5e50a6f Merge branch 'develop' into patch-erpnext 2026-08-31 11:51:59 +05:30
AarDG10
3501beb2bd fix(sms_settings): add patch to pre-fill roles into SMS Settings Roles Table 2026-08-31 11:38:47 +05:30
Nishka Gosalia
7af1af35cd Merge pull request #58325 from Shllokkk/project-duplicate-read-permission-check
fix(projects): check read permission on source in create_duplicate_project
2026-08-31 11:14:40 +05:30
Mihir Kandoi
86852d954e test: narrow shared fixture hardening (#58581) 2026-08-31 05:26:28 +00:00
Diptanil Saha
d5df40986d fix(timesheet): scoping whitelisted methods output to projects and timesheets that are acccessible to users (#58267) 2026-08-30 23:16:32 +05:30
MochaMind
1cedd1421d chore: update POT file (#58566) 2026-08-30 13:00:36 +02:00
Mihir Kandoi
db56080285 fix(manufacturing): use packed row delivery date (#58568) 2026-08-30 10:44:23 +00:00
Pandiyan P
8b43394533 fix(stock): load available serial no report (#58558) 2026-08-29 20:24:58 +05:30
Diptanil Saha
caf8a36bdb fix(accounts): added permission checks on multiple payment entry whitelisted methods (#58555) 2026-08-29 18:06:25 +05:30
Diptanil Saha
a59792a605 refactor(stock): remove dead warehouse_condition branch on get_stock_ledger_entries (#58552) 2026-08-29 11:39:11 +00:00
Shllokkk
6d13e4a633 Merge pull request #58324 from Shllokkk/crm-note-edit-note-permission-check
fix(crm): check write permission in edit_note
2026-08-28 20:34:38 +05:30
Mihir Kandoi
90ac7db704 test: restore ERPNext test coverage (#58542) 2026-08-28 20:09:03 +05:30
Shllokkk
eb49f51d29 fix(crm): check write permission in edit_note 2026-08-28 17:17:04 +05:30
rohitwaghchaure
0223223385 feat: batch split operation to produce child batches per piece (#58530)
* feat: batch split operation to produce child batches per piece

* fix: single input validation, weight conserving lineage, cancel cleanup and naming race for batch split

* fix: delete cancelled batch split bundle along with unused child batches

* fix: retain all child batches when any sibling of a split bundle is in use

* fix: run batch split cancel cleanup only for batch split entries

* fix: make batch split flag read only on stock entry type

* fix: restrict cancel cleanup to child batches minted by the cancelled entry

* refactor: name child batches from item batch series and retain them on cancel

* feat: batch split tree report for parent to child batch traceability

* refactor: source each piece wholly from a single parent batch

* fix: weight per piece sizes the child batches instead of scaling raw material consumption

* fix: apportion child batch lineage proportionally to parent batch quantities

* fix: cap child batch lineage at the whole piece capacity of each parent batch

* fix: exclude batches of cancelled split entries from the batch split tree
2026-08-28 17:11:02 +05:30
Diptanil Saha
ca49de633f fix(stock): auto-select batch no before saving transaction records (#58536) 2026-08-28 17:01:23 +05:30
Soham Kulkarni
64186f229f Merge pull request #58476 from sokumon/module-sidebars
feat: erpnext sidebars and workspaces
2026-08-28 16:13:20 +05:30
Afsal Syed
074c84e880 fix: persist redistributed additional costs during stock entry repost (#58433)
* fix: persist redistributed additional costs during stock entry repost

* test: cover additional cost persistence on stock entry recalculation
2026-08-28 15:51:51 +05:30
rohitwaghchaure
1f7f8cd9d3 feat: alternative finished goods conversion against work order (#58479)
* feat: alternative finished goods conversion against work order

* fix: tighten validations for finished goods conversion

* fix: postgres compatible lock and qty checks post transfer qty for fg conversion

* fix: default single alternative item and hide Change Finished Item button without alternatives
2026-08-28 12:56:06 +05:30
Mihir Kandoi
9160182727 test: improve ERPNext test isolation and runtime (#58507) 2026-08-28 10:41:22 +05:30
sokumon
0985451276 fix: remove the conf file 2026-08-28 02:27:14 +05:30
sokumon
987408f203 chore: some more fixtures 2026-08-28 02:27:14 +05:30
sokumon
2a14b5d953 feat: ship new erpnext workspaces 2026-08-28 02:27:14 +05:30
Diptanil Saha
9018573179 fix(accounts): set pos profile on invoices respecting user permissions (#58508) 2026-08-27 22:06:36 +05:30
Raffael Meyer
e95ca2444c fix: translate doctype in map msg (#58515) 2026-08-27 16:14:49 +00:00
Raffael Meyer
971b6fd49d fix(manufacturing): classify MRP items without a BOM as Purchase (#58509) 2026-08-27 17:55:08 +02:00
rohitwaghchaure
457283f2b0 Revert "refactor(stock): use db.count for the empty ledger check" (#58506)
Revert "refactor(stock): use db.count for the empty ledger check (#58486)"

This reverts commit 2866be2816.
2026-08-27 13:51:49 +00:00
Mihir Kandoi
0849f187e7 fix(projects): ignore cancelled invoices in timesheet portal (#58501) 2026-08-27 12:52:30 +00:00
Pandiyan P
a8ba713f80 fix(selling): check sales order permission before work order creation (#58492) 2026-08-27 12:05:43 +00:00
Pandiyan P
5f99a3418d fix(selling): check quotation write permission before marking lost (#58493) 2026-08-27 12:04:27 +00:00
Mihir Kandoi
b90e3d4656 fix(manufacturing): preserve job card qty in mr transfer (#58482) 2026-08-27 17:26:17 +05:30
Sudharsanan Ashok
2866be2816 refactor(stock): use db.count for the empty ledger check (#58486)
Align the existence check in `reset_bin_without_stock_ledger_entries()` with
the version-15-hotfix backport in #58434, per review feedback there.
2026-08-27 11:07:09 +00:00
Abdeali Chharchhodawala
6842ebb186 fix: improve message formatting and translation for validation issues (#58425) 2026-08-27 12:56:49 +02:00
ljain112
e08a166281 fix(taxes): skip tax addition for invoice created from opening invoice tool 2026-08-27 15:32:39 +05:30
Pandiyan P
4d4cf034b5 fix: clarify duplicate internal party messages (#58469) 2026-08-27 07:38:25 +00:00
Shllokkk
62e6e23581 fix: validate items against source Sales Order in Material Request (#58443) 2026-08-27 12:14:36 +05:30
Pandiyan P
0d90608bc1 fix(manufacturing): account for pending job card qty (#58466) 2026-08-27 12:12:46 +05:30
Pandiyan P
dd033cbc10 fix(stock): guard serial batch editor grid lookup (#58464) 2026-08-27 11:28:02 +05:30
Henil Maru
d6956790d8 fix: Work Order picks wrong Delivery Date when Sales Order has the same item in multiple rows (#58448) 2026-08-26 20:41:14 +05:30
MochaMind
4def9ed20a fix: sync translations from crowdin (develop) (#58458)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-08-26 13:13:08 +00:00
Diptanil Saha
b2489eaf1c ci: ignore python tests on update of **.po (#58457) 2026-08-26 18:29:57 +05:30
MochaMind
88948bab42 fix: sync translations from crowdin (#58349) 2026-08-26 14:44:17 +02:00
Diptanil Saha
98be36ef12 ci: use release token for crowdin translation push (#58454) 2026-08-26 12:16:49 +00:00
Diptanil Saha
687bb9c839 ci: fix crowdin branch (#58452) 2026-08-26 11:38:01 +00:00
rohitwaghchaure
731f03e2f2 feat: option to skip delivery note for service items in sales order (#58297)
* feat: option to skip delivery note for service items in sales order

* fix: reset stale skip delivery flags when setting is disabled

* fix: clear stale skip delivery note flag for non-sales order types

* fix: reset auto skip delivery flags on switch to maintenance order

* refactor: replace sales order skip_delivery_note with item level skip_delivery

* chore: drop skip delivery migration patch

* fix: honor legacy skip_delivery_note flag instead of data migration
2026-08-26 16:28:31 +05:30
Diptanil Saha
fd2057befa ci: crowdin actions (#58447)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 16:26:38 +05:30
Jatin3128
4cfa42921f fix(accounts): resolve subscription plans for any reference doctype in Payment Request (#58438)
fix(accounts): resolve subscription plans for any reference doctype and require read permission

get_subscription_details() was hardcoded to only resolve plans for
Sales Invoice, but is_a_subscription in make_payment_request() was set
for any reference doctype with a `subscription` field. Since Purchase
Invoice also has this field (supplier-side subscriptions), creating a
Payment Request against a subscription-linked Purchase Invoice set
is_a_subscription=1 with an empty subscription_plans table.

get_subscription_details() is also whitelisted with no permission
check, letting any logged-in user query which Subscription/plan/qty is
linked to an arbitrary Sales Invoice or Purchase Invoice.

Make plan resolution generic (guarded by Meta.has_field so doctypes
without a subscription field never hit a nonexistent column), derive
is_a_subscription from the resolved plans so the two can't disagree,
and add a frappe.has_permission read check before returning any data.
2026-08-26 16:16:40 +05:30
Jatin3128
8d2aa69e61 feat(accounts): split bank charges from exchange gain/loss on multi-currency transfers (#58071)
In a multi-currency Internal Transfer, the paid-vs-received difference was
booked entirely to Exchange Gain/Loss, so a bank charge entered as a deduction
pushed the Difference Amount non-zero and blocked submission. The exchange
gain/loss row now absorbs only the residual after user-entered deductions,
letting a Bank Charges row and the Exchange Gain/Loss row coexist and net to
zero.
2026-08-26 13:16:24 +05:30
Sudharsanan Ashok
3f29cdf8d2 feat(analytics): filter sales and purchase analytics by entity (#58402)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-26 13:02:59 +05:30
Aarol D'Souza
ccc19f2554 Merge pull request #58351 from AarDG10/better-err-msg
fix(company): throw if linked to demo_company field
2026-08-26 12:53:01 +05:30
Mihir Kandoi
2ca02fb305 fix(stock): preserve LCV quantity across stock reconciliation (#58309)
Co-authored-by: RamachandranMD <ramachandran.md@finstein.ai>
2026-08-26 12:16:51 +05:30
Vishnu Priya Baskaran
1b81db4754 fix/payment-request-subscription-plans-population (#57494)
* fix(payment-request): populate subscription plans

* test: add coverage for subscription plans in payment request

---------

Co-authored-by: Dharanidharan2813 <dharanidharans1328@gmail.com>
2026-08-26 11:47:04 +05:30
Vishnu Priya Baskaran
918e5a28db fix(stock): carry accounting dimensions from Landed Cost Voucher char… (#56981)
* fix(stock): carry accounting dimensions from Landed Cost Voucher charges into GL entries

* feat(stock): add accounting dimension fields to Landed Cost Taxes and Charges

The charge row had no dimension fields, so a dimension marked mandatory for
Profit and Loss accounts could not be supplied anywhere on the voucher.

Add the accounting dimensions section, cost center and project, and register
the doctype in accounting_dimension_doctypes so custom dimension fields are
created on it. The section and column break are required for that hook to
place the generated fields correctly.

Cost center deliberately omits the ":Company" default used by Purchase Taxes
and Charges: this child table is also the additional costs table on Stock
Entry and Subcontracting Receipt, and auto-filling it there would change
existing postings.

* refactor(stock): group landed cost charges by expense account and dimensions

get_item_account_wise_lcv_entries keyed its inner map by expense account
alone, so two charge rows posting to the same account - whether in one voucher
or across vouchers - were merged. Amounts accumulated correctly but any
per-row context was lost to whichever row was seen first.

Key the grouping by (expense account, dimension values) and return a list of
charges per receipt item, each carrying its own dimensions, so rows that
differ only by dimension stay distinct.

Dimensions resolve from the charge row first, then the voucher item row.
Blanks are left blank so the GL composers can fall back to the receipt item
and receipt document as before.

* refactor(accounts): allow explicit accounting dimensions on add_gl_entry

get_gl_dict derives dimensions from the parent document and the item row, and
reads only custom dimensions off the item - never cost center or project.
Callers that need to set a dimension from some other source had no way to do
so except by building the args dict by hand.

Add a dimensions argument that is merged into the entry before get_gl_dict is
called, and thread it through the StockController and BaseGLComposer wrappers.

* fix(stock): carry landed cost charge dimensions onto the GL entries

Landed cost charges are posted into the receipt document's ledger, and their
expense account is a Profit and Loss account. Until now the entry took its
dimensions from the receipt item, which cannot know about a voucher created
after it was submitted, so a dimension mandatory for P&L accounts failed.

Take cost center, project and custom dimensions from the charge row, falling
back to the receipt item and receipt document when the row leaves them blank.
Only the leg posting to the charge account is affected; the reclass leg keeps
the item's dimensions so it still nets against the base item entry.

Also skip charges that prorate to zero, and hoist the landed cost lookup in
the Purchase Receipt composer out of the item loop - it was reloading every
voucher once per item.

* fix(stock): report missing mandatory dimensions on the Landed Cost Voucher row

Submitting a voucher re-makes the receipt document's GL entries, so a missing
mandatory dimension surfaced as a GL Entry error naming an account, raised
from the middle of update_landed_cost, with nothing pointing at the row that
caused it.

Check the charge rows during validate instead, against both the mandatory
for P&L / Balance Sheet flags and the per-account Accounting Dimension Filter,
and name the row, the dimension and the account in the message.

The check resolves values through the same fallback chain the GL composers
use, so it does not reject a voucher that would have posted successfully.

* test(stock): cover accounting dimensions on landed cost vouchers

Covers the charge row reaching the GL entry, cost center and project
overriding the receipt item, the blank row still falling back to it, and two
charge rows - and two vouchers - on the same expense account with different
dimensions staying separate entries.

Also covers the mandatory P&L dimension being satisfied from the charge row,
the missing one being reported on the voucher, dimensions surviving a repost,
and each dimension netting to zero on cancellation.

* refactor(lcv): apply custom dimension overrides via .update()

---------

Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-08-26 11:27:08 +05:30
Pandiyan P
c940bd1e66 fix(stock): validate serial inventory dimensions (#58394)
* fix(stock): validate serial inventory dimensions

* test(stock): cover serial inventory dimensions
2026-08-26 11:26:27 +05:30
Raffael Meyer
6bdc18b753 fix(asset): skip missing checkbox columns in asset type patch (#58416) 2026-08-26 02:46:20 +02:00
Khushi Rawat
35b3358a5d Merge pull request #58412 from khushi8112/fix-child-table-accounting-dimension
fix(accounts): prevent child table doctypes as accounting dimensions
2026-08-26 00:47:58 +05:30
khushi8112
55dd11f977 test(accounts): cover single doctype and non-scalar dimension safeguards 2026-08-26 00:36:29 +05:30
Shllokkk
6ddff9c386 Merge pull request #58400 from Shllokkk/reorder-email-single-company-perm
fix: send auto reorder email to all managers in single company setup
2026-08-25 17:38:19 +05:30
Raffael Meyer
84a32c40f4 fix(permissions): grant select on link targets to roles with write access (#58334)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 13:31:12 +02:00
khushi8112
7b32d07d1c fix(accounts): prevent child table doctypes as accounting dimensions 2026-08-25 16:09:59 +05:30
Mihir Kandoi
6a6f2ac127 ci: notify support when a PR is released (#58321) 2026-08-25 15:50:21 +05:30
Shllokkk
a01cc92184 test: auto reorder email reaches managers without company user permission 2026-08-25 15:44:29 +05:30
Shllokkk
ae119b1c29 fix: send auto reorder email to all managers in single company setup 2026-08-25 15:44:03 +05:30
Pandiyan P
ce23fcc055 fix: respect zero currency precision (#58395) 2026-08-25 10:06:06 +00:00
rohitwaghchaure
8b8422662c fix: production plan scheduling edge cases (#58388)
* fix: production plan scheduling edge cases

* fix: per-supplier schedule dates and item-wise amended row mapping

* fix: field-based matching for amended production plan rows

* fix: item-level lead time fallback for unconfigured suppliers

* fix: unambiguous amended row pairing and zero-day lead time fallback

* fix: clear sub assembly and material rows on production plan cancel
2026-08-25 14:45:05 +05:30
Sudharsanan Ashok
9cf76c6a68 fix(stock): keep pick list links when refetching stock entry items (#58374) 2026-08-25 07:50:50 +00:00
Sudharsanan Ashok
6fbcfade6c fix(stock): reset bin when no stock ledger entries remain (#58362)
* fix(stock): reset bin when no stock ledger entries remain

update_bin() only writes bins reachable through prev_sle_dict, and that
dict is empty once the last live sle for an item and warehouse is
cancelled or deleted. actual_qty is still recomputed, but stock_value
and valuation_rate stay stale and a repost cannot heal them, so bin
totals drift permanently from the stock balance.

zero those bins after the normal update, guarded by a re-check that no
live sle exists. also drop the prev_sle_dict seeding added earlier in
initialize_previous_data, which never took effect because
initialize_reposting() discards the dict before update_bin() reads it.

* test(stock): cover bin reset when ledger is empty

three cases that all leave an item and warehouse with no live sle:
cancelling the only voucher, deleting it with delete_linked_ledger_entries
on, and reposting over an already emptied ledger. each asserts actual_qty,
valuation_rate and stock_value are all zero.
2026-08-25 11:39:11 +05:30
Mihir Kandoi
5fa68dd068 fix(manufacturing): preserve job card transfer quantity (#58382) 2026-08-24 21:38:12 +05:30
Pandiyan P
ac2bacd3d0 fix: prevent duplicate supplier quotations from portal (#58377) 2026-08-24 18:32:12 +05:30
Pandiyan P
7ec34bab8d fix: hide supplier name in rfq portal (#58373) 2026-08-24 17:38:49 +05:30
Pandiyan P
75d6183bb6 fix: hide rfq status in supplier portal (#58368) 2026-08-24 16:29:05 +05:30
Vishnu Priya Baskaran
560ed25ff7 fix(italy): handle none price_list_rate in e-invoice xml generation (#58242) 2026-08-24 16:26:01 +05:30
Mohd Haris
ef7461cb5b fix: aggregate child warehouses in Stock Qty vs Serial No Count report (#58134)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-24 15:42:09 +05:30
Vishnu Priya Baskaran
abf94bc72d Fix/return qty validation different uom (#58298)
Co-authored-by: Afsal Syed <afsalsyed12@gmail.com>
2026-08-24 15:34:43 +05:30
Pandiyan P
59d80b29c4 fix: render missing terms before printing (#58358) 2026-08-24 14:53:08 +05:30
Mihir Kandoi
7149df398a fix: include time logs ending at midnight in timesheet billing summary (#58355) 2026-08-24 08:16:50 +00:00
Shllokkk
4a43e5203f Merge branch 'develop' into project-duplicate-read-permission-check 2026-08-24 13:14:19 +05:30
Shllokkk
8c8b282a2e fix(projects): check read permission on source in create_duplicate_project 2026-08-24 13:03:09 +05:30
Khushi Rawat
1b51f8cbd6 Merge pull request #58179 from aerele/fix-accounts-payable-supplier-group-filter
fix(accounts): supplier group filter not applied on accounts payable …
2026-08-24 12:22:21 +05:30
AarDG10
aaa3100efc fix(company): throw if linked to demo_company field
Throws an error msg if user is deleting demo company directly.
2026-08-24 11:10:50 +05:30
Raffael Meyer
64486f34e8 ci: bump po-review-action (#58346) 2026-08-23 17:05:22 +00:00
MochaMind
0ea36160fe fix: sync translations from crowdin (#58289) 2026-08-23 18:51:09 +02:00
MochaMind
6ba623e95f chore: update POT file (#58345) 2026-08-23 13:32:17 +02:00
Raffael Meyer
e71e08959f refactor: use Meta.get_translated_label (#58335) 2026-08-22 00:28:55 +02:00
Raffael Meyer
eae36c2d67 fix(i18n): translate get_label results at presentation with DocType context (#57217) 2026-08-21 23:19:36 +02:00
Jatin3128
275844d496 fix(journal-entry): avoid full grid re-render per row in set_exchange_rate (#58328)
refresh() loops over every row in the accounts child table and calls
set_exchange_rate() for each one. That function unconditionally ended
with frm.refresh_field("accounts"), rebuilding the whole grid (header,
pagination, current page) on every single row. For large child tables
this makes opening the form scale badly with row count.

Use grid.refresh_row(cdn) instead, which only re-renders the row that
actually changed and is a no-op for rows outside the current page.

Measured on a 1000-row Journal Entry: ~8.5s to first rendered row and
~7.9s of blocked main thread before this fix, ~2.3s and ~1.9s after.
2026-08-21 01:57:04 +05:30
Mihir Kandoi
624d402143 fix(projects): respect permissions in timesheet billing summary (#58320) 2026-08-20 09:43:18 +00:00
Mihir Kandoi
2328e6da94 fix: changing product bundle warehouse should change packed items war… (#58316) 2026-08-20 12:35:04 +05:30
Mihir Kandoi
941baffdda fix: correct to and from date filters in timesheet billing summary re… (#58317) 2026-08-20 04:12:30 +00:00
Pandiyan P
7faadb8282 fix: stop doubling totals in timesheet billing summary (#58311) 2026-08-20 02:26:23 +00:00
Mihir Kandoi
e8b214e7d3 chore: remove mistakenly committed file (#58310) 2026-08-19 15:49:02 +00:00
Mihir Kandoi
8320c77694 fix(manufacturing): scope secondary items to job card (#58260) 2026-08-19 20:53:53 +05:30
Mihir Kandoi
696f0df811 fix(stock): update pick list status for product bundles (#58306) 2026-08-19 20:52:47 +05:30
Mihir Kandoi
deeb787f57 fix(stock): support shared target UOM conversions (#58305) 2026-08-19 18:29:59 +05:30
Mihir Kandoi
478a2f4f4b fix(stock): ignore future stock during batch reservation (#58303) 2026-08-19 12:50:26 +00:00
Mihir Kandoi
cfca71782e fix(stock): preserve item UOM conversion factor (#58299) 2026-08-19 11:45:43 +00:00
Mihir Kandoi
f4ef7fdbcc fix(stock): preserve zero return incoming rates (#58302) 2026-08-19 11:45:29 +00:00
Mihir Kandoi
e5cd30c5ab fix(setup): handle Transaction Deletion Record CSV edge cases (#58300)
Co-authored-by: Pedro Barbosa <barbosamaverickv8@gmail.com>
2026-08-19 17:12:16 +05:30
Mihir Kandoi
8eeb0196e4 fix(stock): show backdated batches in stock entry (#58301) 2026-08-19 11:33:31 +00:00
rohitwaghchaure
d07f4bb857 feat: percentage based BOM (#58292)
* feat: percentage based BOM, fixed qty components and component qty tolerance

* fix: mandatory percentages, fixed qty explosion keys and expected qty aggregation

* fix: percentage BOM incompatible with semi FG tracking, fixed qty in production plan explosion

* refactor: remove component_qty_tolerance from Manufacturing Settings

* refactor: drop fixed qty components and consumption tolerance, keep percentage-based BOM only
2026-08-19 17:03:20 +05:30
Mihir Kandoi
2524af4758 fix: new docs should refetch incoming rates (#58097) 2026-08-19 12:36:22 +05:30
Sudharsanan Ashok
3a6e17a03d fix(stock): fetch item stock UOM in stock reconciliation (#58284) 2026-08-19 10:27:07 +05:30
Diptanil Saha
d070a1daa1 Merge pull request #58286 from diptanilsaha/template-text-context-escaping
fix: escape interpolated values in text positions across portal and desk templates
2026-08-19 02:30:23 +05:30
diptanilsaha
8d4c10871d fix: read project from url argument in project portal script 2026-08-19 02:06:25 +05:30
diptanilsaha
449c90f05f fix: escape event subject in crm activities template 2026-08-19 02:06:24 +05:30
diptanilsaha
96f51092a0 fix: escape help article title, description and category on support page 2026-08-19 02:06:22 +05:30
diptanilsaha
02434ee98c fix: escape partner introduction and address details in partner pages 2026-08-19 02:06:21 +05:30
diptanilsaha
09e1c6024d fix: escape project name and task subject in project portal templates 2026-08-19 02:06:20 +05:30
diptanilsaha
e88164e8c7 fix: escape items preview in transaction row template 2026-08-19 02:06:19 +05:30
diptanilsaha
a5aec7ed84 fix: escape party name and contact display on order page 2026-08-19 02:06:17 +05:30
Diptanil Saha
25dfcb17c8 fix: escape on status image for workstations in production status (#58279) 2026-08-18 17:58:23 +00:00
Diptanil Saha
a667cd7cc6 Merge pull request #58273 from diptanilsaha/template-output-escaping
fix: escape interpolated values in portal, print and desk templates
2026-08-18 23:09:09 +05:30
diptanilsaha
93d6ccd041 refactor: build document links with get_form_link in form scripts 2026-08-18 22:49:47 +05:30
diptanilsaha
cfb664804c refactor: build document links with get_form_link in report formatters 2026-08-18 22:49:45 +05:30
diptanilsaha
57c2887ab4 fix: escape attribute values in shared client side templates 2026-08-18 22:49:43 +05:30
diptanilsaha
09eae9886a fix: escape activity type in project dashboard template 2026-08-18 22:49:41 +05:30
diptanilsaha
79284d8430 fix: escape item and document values in manufacturing preview templates 2026-08-18 22:49:39 +05:30
diptanilsaha
ffb7a708e4 fix: escape item, warehouse and label values in form grid templates 2026-08-18 22:49:28 +05:30
diptanilsaha
0541635f48 fix: escape item image url and item name in item image print formats 2026-08-18 22:49:26 +05:30
diptanilsaha
dc7822b319 fix: escape item image url in compact item print description 2026-08-18 22:49:24 +05:30
diptanilsaha
303c4f9081 fix: escape help article route on support page 2026-08-18 22:49:23 +05:30
diptanilsaha
9a4cb48429 fix: escape partner logo url and partner name in partner pages 2026-08-18 22:49:22 +05:30
diptanilsaha
53394478d9 fix: escape pathname and document name in transaction row link 2026-08-18 22:49:10 +05:30
diptanilsaha
0b1dd6cccf fix: encode image url and escape alt text in shared media macros 2026-08-18 22:49:09 +05:30
diptanilsaha
a3032f2f66 fix: serialize document values with tojson in rfq portal script 2026-08-18 22:49:07 +05:30
diptanilsaha
c42d653da5 fix: render terms and conditions through sanitize_html on order page 2026-08-18 22:49:06 +05:30
diptanilsaha
bc0fd611d4 fix: escape forum topics and get started sections on help page 2026-08-18 22:49:05 +05:30
diptanilsaha
5c4fb99f68 fix: urlencode project name in project portal page and script 2026-08-18 22:49:03 +05:30
diptanilsaha
4c0f0299ea fix: escape assigned user full name in project portal row templates 2026-08-18 22:49:02 +05:30
Mihir Kandoi
d91fce90c7 fix: use user data fields hook (#58274) 2026-08-18 16:07:06 +00:00
MochaMind
f6ab73d5ac fix: sync translations from crowdin (#58225) 2026-08-18 10:15:01 +00:00
Mihir Kandoi
27b06fcb61 fix(accounts): match returns to source invoice items (#58250) 2026-08-18 08:14:48 +00:00
rohitwaghchaure
b68324ce78 fix: work order finish dialog with process loss qty from job card (#58256)
* fix(manufacturing): cap job card completed qty by previous operation and show process loss on finish dialog

* fix: revert job card finish dialog changes
2026-08-18 13:26:56 +05:30
Pandiyan P
1fa057b943 fix: skip covered rows when ordering from the mrp report (#58249)
* test: cover the rows that have nothing left to order in the mrp report

a row whose requirement is already met by stock or by an order placed earlier
fails the order it is selected for, takes the rows beside it down with it, and
what rounding leaves behind of it is ordered as if it were a real quantity. the
work order made from a row of the schedule also has to keep the work in progress
warehouse the company keeps for it.

* fix: skip covered rows when ordering from the mrp report

a row whose requirement is already met by stock or by orders that were placed
earlier nets down to a required qty of zero. making an order from it threw
"Qty To Manufacture cannot be 0", and since nothing caught it, none of the other
selected rows were created either. such rows are now left alone, and selecting
only covered rows says so instead of failing.

the quantity ordered stays the one that is still needed. taking the planned qty
instead would order everything that stock and the open orders already cover. it
is read at the precision an order stores it in, so what is left of a covered row
after all the subtracting does not become an order line of its own.
2026-08-18 11:22:18 +05:30
rohitwaghchaure
1f83906189 fix: don't set work order status to In Process only due to skip material transfer (#58244) 2026-08-17 18:29:24 +05:30
Mihir Kandoi
d40679cffe fix(accounts): allocate drop-ship cost by invoice quantity (#58226) 2026-08-17 18:04:02 +05:30
rohitwaghchaure
bf1ec51680 Pick list stock availability (#58243)
* feat: stock availability insight on pick list

* fix: show holding pick lists inline in stock availability dialog

* fix: dashboard layout for stock availability dialog

* fix: reword stock release hint in availability dialog

* fix: tree layout for stock held by section

* fix: escape values in blocking pick lists table
2026-08-17 17:58:15 +05:30
Ejaaz Khan
e9c6ec4d33 Merge pull request #58241 from iamejaaz/feat/pf-split-across-pages
feat: allow Terms to split across pages in standard print formats
2026-08-17 16:01:42 +05:30
Ejaaz Khan
bd2a709ed1 feat: allow Terms to split across pages in standard print formats 2026-08-17 15:35:43 +05:30
Mihir Kandoi
baa20e5e49 fix(manufacturing): preserve attribution through consumption (#58146)
* fix(manufacturing): preserve attribution through consumption

* fix(manufacturing): preserve consumed quantity matching

* fix(manufacturing): assign consumption once
2026-08-17 09:57:57 +00:00
Pandiyan P
4dc0b2ea52 fix(manufacturing): fall back to item group defaults for work order warehouses (#58231) 2026-08-17 08:06:01 +00:00
Diptanil Saha
2f82e0dd36 fix(bank_statement_import): add missing permission check on multiple whitelisted methods (#58221) 2026-08-16 17:29:37 +00:00
Diptanil Saha
9a71ab4d79 fix(bank_statement_import): add missing permission check on get_import_status (#58217) 2026-08-16 20:22:44 +05:30
Diptanil Saha
5a69bb9001 fix(crm)!: remove unused get_last_interaction endpoint (#58214) 2026-08-16 19:32:24 +05:30
MochaMind
6c7b6bd87d fix: sync translations from crowdin (#58203) 2026-08-16 18:03:42 +05:30
MochaMind
b082f60e48 chore: update POT file (#58206) 2026-08-16 18:03:12 +05:30
Pandiyan P
9ee41029c8 fix: drop removed Restaurant doctype from sales tax template dashboard (#58191) 2026-08-16 10:50:11 +00:00
Pandiyan P
4a5943e2fb fix: correct Item Group doctype name in item tax template dashboard (#58192) 2026-08-16 16:14:52 +05:30
Shllokkk
8b8223f48b Merge pull request #58198 from Shllokkk/supplier-quotation-comparison-status-filter
feat: add status filter to Supplier Quotation Comparison report
2026-08-16 15:51:51 +05:30
Diptanil Saha
1212a278c6 Merge pull request #58197 from diptanilsaha/fix/get_digest_msg
refactor(email_digest): convert `get_digest_msg` from module function to instance method
2026-08-15 21:05:44 +05:30
diptanilsaha
c6b172999c fix(email_digest): added permission check for get_msg_html 2026-08-15 20:53:38 +05:30
Shllokkk
7cc236d1a8 feat: add status filter to Supplier Quotation Comparison report 2026-08-15 20:51:30 +05:30
Diptanil Saha
d472e17c75 Revert "refactor(email_digest)!: convert get_digest_msg from module function to instance method" (#58196) 2026-08-15 19:59:33 +05:30
Diptanil Saha
4cd8fbf1c0 refactor(email_digest): convert get_digest_msg from module function to instance method (#58193) 2026-08-15 14:10:14 +00:00
Pandiyan P
cc60c80e68 fix: correct is_frozen description on customer and supplier (#58168) 2026-08-15 12:24:12 +00:00
rohitwaghchaure
70a8a2d0c5 feat: validate purchase receipt exchange rate parity on purchase invoice (#58177) 2026-08-15 14:20:55 +05:30
Antoine Maas
a5472b5080 feat: Belgian Charts of Accounts (commercial + non-profit, FR + NL) (#54679)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-08-14 22:06:40 +05:30
MochaMind
4316cf2f65 fix: sync translations from crowdin (#58145) 2026-08-14 20:03:05 +05:30
Mihir Kandoi
ef24974645 fix(stock): restore subcontracting inward warehouse checks (#58183) 2026-08-14 14:26:25 +00:00
Sudharsanan Ashok
fb7c2caf29 fix(stock): honour pick serial / batch based on in the batch selector (#58176) 2026-08-14 19:25:35 +05:30
Pandiyan P
1519770cb0 perf: filter stock projected qty bins by company in sql (#58173) 2026-08-14 19:20:43 +05:30
Pandiyan P
92c372c6a1 fix: rename misspelled delink_refernce_from_voucher (#58169) 2026-08-14 19:19:17 +05:30
ervishnucs
513f19924d fix(accounts): supplier group filter not applied on accounts payable report 2026-08-14 18:02:25 +05:30
Khushi Rawat
0b01b9bcd3 Merge pull request #57838 from khushi8112/hide/redundant-primary-address-contact-fields
refactor(ui): hide the primary address and contact preview fields
2026-08-14 17:32:09 +05:30
Jatin3128
cbf8f61101 fix: rewriting the description of the overdue billing checkbox (#58172)
fix: rewritting the description of the overdue billing checkbox
2026-08-14 11:18:38 +00:00
Nishka Gosalia
525930c5eb Merge pull request #58167 from nishkagosalia/gh-57661
fix: validation for task end date check
2026-08-14 16:22:07 +05:30
nishkagosalia
7c6da80f98 fix: validation for task end date check 2026-08-14 16:05:52 +05:30
rohitwaghchaure
e5c24fb398 feat: cross-plan load and overlap validation in production plan scheduling (#58127)
* feat: cross-plan load and overlap validation in production plan scheduling

* fix: row-level job card exclusion and locking read in schedule overlap check

* fix: operation-level job card exclusion and workstation locking in capacity check

* fix: keep plan schedule load when job cards carry no booked time

* fix: qty-coverage based job card exclusion for plan schedule load
2026-08-14 10:24:48 +00:00
Jatin3128
917badb82b fix: renaming the overdue billing checkbox (#58165) 2026-08-14 10:16:47 +00:00
Pandiyan P
10109674a4 feat(selling): add shipping contact person to sales order, delivery note and sales invoice (#58159) 2026-08-14 15:04:31 +05:30
Mihir Kandoi
73957dc84b refactor(selling): reorder sales invoice mapper arguments (#58162) 2026-08-14 14:36:37 +05:30
Diptanil Saha
3558ce3b59 feat(accounts): opt-in 'Consider Accounting Dimension' filter on General Ledger Report (#58156) 2026-08-14 08:12:08 +00:00
Khushi Rawat
7e2edb5c27 Merge pull request #58092 from aerele/fix/disallow-reverse-of-reverse-journal-entry
fix(accounts): disallow reversing a reverse journal entry
2026-08-14 11:30:44 +05:30
Mihir Kandoi
8632119f11 fix(accounts): filter parties by transaction company (#58147) 2026-08-14 11:02:13 +05:30
rohitwaghchaure
9239d1c2a3 fix: ignore historical negative batch stock in outward validation (#58148) 2026-08-14 10:56:27 +05:30
Mihir Kandoi
eae6d10304 fix(manufacturing): preserve alternative material attribution (#58131) 2026-08-14 03:44:39 +00:00
Diptanil Saha
081d269f6d Merge pull request #58141 from diptanilsaha/fix/fcrm_custom_field
fix(crm_settings): create `crm_deal` fields on enabling frappe crm data synchronization
2026-08-13 23:09:17 +05:30
diptanilsaha
9613d72d81 fix: patch to delete the crm_deal custom fields 2026-08-13 22:56:25 +05:30
diptanilsaha
be2dea0ba2 fix(crm_settings): create custom fields for Frappe CRM on enabling synchronization 2026-08-13 22:56:05 +05:30
Nikhil Kothari
0bbaf4da3e chore(deps): update eslint in banking app (#58129) 2026-08-13 12:29:13 +00:00
Pandiyan P
fd728dacca fix(buying): allow purchase returns against a closed purchase order (#58126) 2026-08-13 17:55:41 +05:30
Sudharsanan Ashok
1e583725a1 feat: delivery note billing based on quantity (#56149)
* feat(stock): bill delivery note by qty when invoiced amount is short

* test(stock): add test for qty fallback in delivery note billing
2026-08-13 16:28:35 +05:30
Sudharsanan Ashok
a2976dd29e fix(stock): confirm before changing item qty from the batch selector (#58123)
the batch selector silently overwrote the item qty with the bundle total,
so editing a row qty in the dialog changed the delivered qty without any
warning. prompt for confirmation when the rows do not add up to the qty
to fetch, and only proceed if the user agrees.
2026-08-13 16:27:45 +05:30
Khushi Rawat
99cb98d96c Merge pull request #58120 from khushi8112/stale-exchange-rate-description
fix: describe stale exchange rate settings
2026-08-13 15:10:41 +05:30
Mihir Kandoi
1534e2fad6 fix(manufacturing): derive material transfers from actual coverage (#58110) 2026-08-13 15:06:03 +05:30
khushi8112
84cdd0daf0 fix: describe stale exchange rate settings 2026-08-13 14:51:31 +05:30
Jatin3128
4a8342496e fix: keep Currency and Price List section open for foreign currency (#58074)
The section is marked collapsible with no condition, so it always
rendered collapsed. When the transaction currency differs from the
company currency the exchange rate is relevant and was hidden behind
a click.

Adds collapsible_depends_on so the section starts expanded whenever
the transaction currency differs from the company currency, and stays
collapsed otherwise.
2026-08-13 12:48:43 +05:30
Khushi Rawat
7212e6d989 Merge pull request #58096 from khushi8112/refactor/item-prices-embedded-list
refactor: use EmbeddedList for item prices
2026-08-13 12:33:51 +05:30
Khushi Rawat
c973de27d4 Merge pull request #58078 from khushi8112/fix/ar-sales-person-user-permission
fix: apply Sales Person user permissions in Accounts Receivable
2026-08-13 11:45:28 +05:30
khushi8112
51cefeffef refactor: use EmbeddedList for item prices
Replaces the hand-rolled item_prices.html table with frappe.ui.EmbeddedList,
the same primitive the proforma list uses. Drops the custom markup and styles.

The 10-row cap and the "View All Prices" link stay: the query fetches 11 rows
to return 10 plus a has_more flag, and the link routes to the Item Price list
filtered by item.
2026-08-13 11:34:12 +05:30
pandiyan
9dd37d5f32 fix(accounts): disallow reversing a reverse journal entry
check read permission on the source entry before the guards run, so the reversal relationship is not disclosed to a user who cannot read it.
2026-08-12 15:59:16 +05:30
pandiyan
80422d2108 test(accounts): cover reversal of a reverse journal entry
also assert that a user without read access on the entry gets a permission error instead of the reversal relationship.
2026-08-12 15:59:16 +05:30
khushi8112
8b09ba429e fix: apply Sales Person user permissions in Accounts Receivable
The report only narrowed by sales person when the filter was set, so a user
restricted to a Sales Person saw every row once the filter was cleared.

Resolve the permitted Sales Persons from user permissions and apply them on
top of the filter. Each Sales Team parent type is matched against its own
applicable_for scope, so a permission scoped to one doctype cannot authorise
rows through the other. Descendants are already expanded by
get_user_permissions, so Hide Descendants is respected. Gated to Receivable,
since the class is shared with Accounts Payable.
2026-08-12 14:26:10 +05:30
khushi8112
757eaf5fcf refactor(ui): hide the primary address and contact section
The Address & Contact cards now show these details and mark which record
is primary, so the section below only repeats it. Values are still stored
and Customer.search_fields keeps working, since search reads the column
rather than the form.

Depends on frappe/frappe#41600.
2026-08-06 15:16:00 +05:30
597 changed files with 258121 additions and 84625 deletions

View File

@@ -4,6 +4,36 @@ set -e
cd ~ || exit
# Authenticate git against github.com with the job token: anonymous git-over-HTTPS from the
# runners gets throttled to a 401, which kills whichever clone is in flight — the frappe fetch
# below, or payments under `bench get-app`. See the PR description.
#
# A credential helper rather than a url.insteadOf rewrite, because `git clone` PERSISTS a
# rewritten URL into the new repo's .git/config: an insteadOf would leave the token sitting in
# apps/payments/.git/config on the runner. A helper is consulted only when github.com actually
# challenges, and leaves the stored remote URL untouched. Passing it through GIT_CONFIG_* keeps
# the token out of ~/.gitconfig too, and child processes inherit it (bench shells out to git).
ci_github_token=${CI_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}
if [ -n "$ci_github_token" ]; then
export CI_GITHUB_TOKEN="$ci_github_token"
export GIT_CONFIG_COUNT=3
# Reset first: git runs EVERY configured helper and calls `store` on them after a successful
# auth, so a `credential.helper=store` inherited from the image's gitconfig would write the
# token to ~/.git-credentials. An empty value clears the list before ours is added.
export GIT_CONFIG_KEY_0="credential.helper"
export GIT_CONFIG_VALUE_0=""
export GIT_CONFIG_KEY_1="credential.https://github.com.username"
export GIT_CONFIG_VALUE_1="x-access-token"
export GIT_CONFIG_KEY_2="credential.https://github.com.helper"
# Single-quoted: $CI_GITHUB_TOKEN is expanded by the shell git runs the helper in, so the
# token is read from the environment at call time and never stored anywhere. Answering only
# `get` makes the helper inert for git's `store`/`erase` calls.
export GIT_CONFIG_VALUE_2='!f() { test "$1" = get && echo "password=$CI_GITHUB_TOKEN"; }; f'
fi
# Whatever happens, never sit on a credential prompt: fail fast and legibly instead.
export GIT_TERMINAL_PROMPT=0
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-}
@@ -188,7 +218,7 @@ restore_warm_bench() {
# Phase 1 already fetched ~/frappe to the exact live develop SHA. Fetch that commit
# straight from it (bench init names the remote 'upstream', not 'origin', and points
# it at this local clone — so a plain `git fetch origin` does not work).
git fetch --no-tags "$HOME/frappe" HEAD || exit 1
git fetch --no-tags --update-shallow "$HOME/frappe" HEAD || exit 1
git checkout --force FETCH_HEAD || exit 1
); then
echo "Fast-forward to ${frappe_sha} failed; falling back to full init"

View File

@@ -0,0 +1,51 @@
name: Download translations from Crowdin
on:
schedule:
- cron: "0 4 * * 1"
workflow_dispatch:
concurrency:
group: crowdin-download
cancel-in-progress: false
permissions:
contents: read
jobs:
download-translations:
name: Download translations into ${{ matrix.branch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch: ["develop", "version-16-hotfix"]
steps:
- name: Checkout ${{ matrix.branch }}
uses: actions/checkout@v6
with:
ref: ${{ matrix.branch }}
fetch-depth: 0
persist-credentials: false
- name: Download translations and open PR
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
with:
config: crowdin.yml
upload_sources: false
upload_translations: false
download_translations: true
crowdin_branch_name: "[frappe.erpnext] ${{ matrix.branch }}"
skip_ref_checkout: true
localization_branch_name: l10n_crowdin_${{ matrix.branch }}
create_pull_request: true
pull_request_base_branch_name: ${{ matrix.branch }}
commit_message: "fix: sync translations from crowdin"
pull_request_title: "fix: sync translations from crowdin (${{ matrix.branch }})"
pull_request_labels: "translation, skip-release-notes"
pull_request_reviewers: barredterra
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}

View File

@@ -0,0 +1,54 @@
name: Upload main.pot to Crowdin
on:
push:
branches:
- develop
- version-16-hotfix
paths:
- "erpnext/locale/main.pot"
workflow_dispatch:
concurrency:
group: crowdin-upload-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
jobs:
upload-sources:
name: Upload sources from ${{ github.ref_name }}
runs-on: ubuntu-latest
steps:
- name: Checkout ${{ github.ref_name }}
uses: actions/checkout@v6
- name: Restore Crowdin cache
uses: actions/cache/restore@v6
with:
path: .crowdin
key: crowdin-${{ github.ref_name }}
restore-keys: crowdin-${{ github.ref_name }}-
- name: Upload main.pot to Crowdin
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
with:
config: crowdin.yml
upload_sources: true
upload_translations: false
download_translations: false
create_pull_request: false
crowdin_branch_name: "[frappe.erpnext] ${{ github.ref_name }}"
upload_sources_args: "--cache"
env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Save Crowdin cache
uses: actions/cache/save@v6
if: always()
with:
path: .crowdin
key: crowdin-${{ github.ref_name }}-${{ github.run_id }}

View File

@@ -0,0 +1,45 @@
name: Notify Support on PR release
on:
issue_comment:
types: [created]
permissions: {}
jobs:
notify-support:
if: >-
github.event.issue.pull_request
&& github.event.comment.user.id == 28699486
&& contains(github.event.comment.body, 'This PR is included in version')
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Notify support.frappe.io
env:
COMMENT_ID: ${{ github.event.comment.id }}
PR_NUMBER: ${{ github.event.issue.number }}
REPOSITORY: ${{ github.repository }}
SUPPORT_FRAPPE_AUTH: ${{ secrets.SUPPORT_FRAPPE_AUTH }}
run: |
payload="$(
jq -n \
--arg repository "$REPOSITORY" \
--argjson pr_number "$PR_NUMBER" \
--argjson comment_id "$COMMENT_ID" \
'{
repository: $repository,
pr_number: $pr_number,
comment_id: $comment_id
}'
)"
curl --fail-with-body \
--retry 3 \
--retry-all-errors \
--request POST \
--header "Authorization: $SUPPORT_FRAPPE_AUTH" \
--header "Content-Type: application/json" \
--data "$payload" \
"https://support.frappe.io/api/method/notify_pr_release"

View File

@@ -121,6 +121,8 @@ jobs:
env:
DB: mariadb
TYPE: server
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
- name: Run Patch Tests
run: |

View File

@@ -22,6 +22,6 @@ jobs:
pull-requests: write
steps:
- uses: alyf-de/po-review-action@v1.1.0
- uses: alyf-de/po-review-action@5928f84d6bc9094f9ad6e2c5780f01c0044b800e # v1.1.1
with:
hidden-po-files: eo.po

View File

@@ -129,6 +129,8 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
- name: Run Tests
run: |

View File

@@ -13,6 +13,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '**.po'
permissions:
contents: read

View File

@@ -13,6 +13,7 @@ on:
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
- '**.po'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"
@@ -101,6 +102,8 @@ jobs:
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
DB_HOST: 127.0.0.1
DB_USER_HOST: '%'
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb

View File

@@ -103,6 +103,8 @@ jobs:
DB: postgres
TYPE: server
FRAPPE_BRANCH: develop
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
CI_GITHUB_TOKEN: ${{ github.token }}
BENCH_CACHE_DIR: /home/runner/bench-cache
- name: Warm up test data

View File

@@ -55,11 +55,11 @@
"@types/node": "^25.3.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.5",
"eslint": "^10.8.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.62.1"
"typescript-eslint": "^8.67.0"
}
}

View File

@@ -206,65 +206,50 @@
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2":
"@eslint-community/regexpp@^4.12.2":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
"@eslint/config-array@^0.21.2":
version "0.21.2"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.2.tgz#f29e22057ad5316cf23836cee9a34c81fffcb7e6"
integrity sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==
"@eslint/config-array@^0.23.5":
version "0.23.5"
resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.23.5.tgz#56e86d243049195d8acc0c06a1b3dfdc3fa3de95"
integrity sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==
dependencies:
"@eslint/object-schema" "^2.1.7"
"@eslint/object-schema" "^3.0.5"
debug "^4.3.1"
minimatch "^3.1.5"
minimatch "^10.2.4"
"@eslint/config-helpers@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda"
integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==
"@eslint/config-helpers@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.7.0.tgz#09ee4aa07b73f059ec2d4c74bf4b2ff02b322377"
integrity sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==
dependencies:
"@eslint/core" "^0.17.0"
"@eslint/core" "^1.2.1"
"@eslint/core@^0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c"
integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==
"@eslint/core@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@eslint/core/-/core-1.2.1.tgz#c1da7cd1b82fa8787f98b5629fb811848a1b63ce"
integrity sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==
dependencies:
"@types/json-schema" "^7.0.15"
"@eslint/eslintrc@^3.3.6":
version "3.3.6"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.6.tgz#d22bfd6b3a7d8e1f2c0b2f2e6de111b53ec6e13e"
integrity sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==
dependencies:
ajv "^6.14.0"
debug "^4.3.2"
espree "^10.0.1"
globals "^14.0.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.3.0"
minimatch "^3.1.5"
strip-json-comments "^3.1.1"
"@eslint/js@9.39.5":
"@eslint/js@^9.39.5":
version "9.39.5"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.5.tgz#6f2fbcff75500d229d535e0a949ae13472c84787"
integrity sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==
"@eslint/object-schema@^2.1.7":
version "2.1.7"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad"
integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==
"@eslint/object-schema@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-3.0.5.tgz#88e9bf4d11d2b19c082e78ebe7ce88724a5eb091"
integrity sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==
"@eslint/plugin-kit@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2"
integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==
"@eslint/plugin-kit@^0.7.2":
version "0.7.2"
resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz#4b0962f3f2c7ce8bc98b3ecfe34525c09d2cb729"
integrity sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==
dependencies:
"@eslint/core" "^0.17.0"
"@eslint/core" "^1.2.1"
levn "^0.4.1"
"@floating-ui/core@^1.7.5":
@@ -1262,6 +1247,11 @@
dependencies:
"@types/ms" "*"
"@types/esrecurse@^4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec"
integrity sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==
"@types/estree-jsx@^1.0.0":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18"
@@ -1269,7 +1259,7 @@
dependencies:
"@types/estree" "*"
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6":
"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24"
integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==
@@ -1327,100 +1317,100 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4"
integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
"@typescript-eslint/eslint-plugin@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz#1736dcdca6cae3359d818456a47d18b674761f7f"
integrity sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==
"@typescript-eslint/eslint-plugin@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz#52f9f0e47d5a7571c4336e69bfeea581509ef2cf"
integrity sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==
dependencies:
"@eslint-community/regexpp" "^4.12.2"
"@typescript-eslint/scope-manager" "8.62.1"
"@typescript-eslint/type-utils" "8.62.1"
"@typescript-eslint/utils" "8.62.1"
"@typescript-eslint/visitor-keys" "8.62.1"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/type-utils" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
ignore "^7.0.5"
natural-compare "^1.4.0"
ts-api-utils "^2.5.0"
"@typescript-eslint/parser@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.62.1.tgz#d3f7ba18f1bf78bfb7256fea021d1927b48e7080"
integrity sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==
"@typescript-eslint/parser@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.67.0.tgz#0158022ec9927e0afcd58a8cc2ad57e01d892f5c"
integrity sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==
dependencies:
"@typescript-eslint/scope-manager" "8.62.1"
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/typescript-estree" "8.62.1"
"@typescript-eslint/visitor-keys" "8.62.1"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
debug "^4.4.3"
"@typescript-eslint/project-service@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.62.1.tgz#78d880eb1cf6859b5ec263d04f95403e9f90ae47"
integrity sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==
"@typescript-eslint/project-service@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.67.0.tgz#1552db007ca9206a1c6c7acf49e210bd17a8c56f"
integrity sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==
dependencies:
"@typescript-eslint/tsconfig-utils" "^8.62.1"
"@typescript-eslint/types" "^8.62.1"
"@typescript-eslint/tsconfig-utils" "^8.67.0"
"@typescript-eslint/types" "^8.67.0"
debug "^4.4.3"
"@typescript-eslint/scope-manager@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz#7ee65e9a6eb3ccdc4816593a4ff38840306de88a"
integrity sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==
"@typescript-eslint/scope-manager@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz#4d4c2da09560d10dd7d947cba2d29d14d25af16d"
integrity sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==
dependencies:
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/visitor-keys" "8.62.1"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
"@typescript-eslint/tsconfig-utils@8.62.1", "@typescript-eslint/tsconfig-utils@^8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz#e2b5f24fe721044189cb7e81117c96d75979d627"
integrity sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==
"@typescript-eslint/tsconfig-utils@8.67.0", "@typescript-eslint/tsconfig-utils@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz#f45a3eba6b9132fb47141ec03ce2f275f1ea991d"
integrity sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==
"@typescript-eslint/type-utils@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz#ebd30b13bacb13070917259a23309cf644121f9a"
integrity sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==
"@typescript-eslint/type-utils@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz#96bed105275559df3bcf0449b73a6414d35c59ce"
integrity sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==
dependencies:
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/typescript-estree" "8.62.1"
"@typescript-eslint/utils" "8.62.1"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
debug "^4.4.3"
ts-api-utils "^2.5.0"
"@typescript-eslint/types@8.62.1", "@typescript-eslint/types@^8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.62.1.tgz#c58be954e483b2fc98275374d5bcb40b99842dc1"
integrity sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==
"@typescript-eslint/types@8.67.0", "@typescript-eslint/types@^8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.67.0.tgz#4a8d00cc1faba5c14feabc60f85b7a32652f34b6"
integrity sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==
"@typescript-eslint/typescript-estree@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz#98c1bb17635d5b026b24193a8d29188ac64380ff"
integrity sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==
"@typescript-eslint/typescript-estree@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz#116c3a47c06119c5a050e8851861d6497dd64bc2"
integrity sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==
dependencies:
"@typescript-eslint/project-service" "8.62.1"
"@typescript-eslint/tsconfig-utils" "8.62.1"
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/visitor-keys" "8.62.1"
"@typescript-eslint/project-service" "8.67.0"
"@typescript-eslint/tsconfig-utils" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/visitor-keys" "8.67.0"
debug "^4.4.3"
minimatch "^10.2.2"
semver "^7.7.3"
tinyglobby "^0.2.15"
ts-api-utils "^2.5.0"
"@typescript-eslint/utils@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.62.1.tgz#1622b75c7e6df308181dd0b44855dc4228da0457"
integrity sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==
"@typescript-eslint/utils@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.67.0.tgz#3e478a3d69d330a1fc50c12746cc2ee0732ccfcd"
integrity sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==
dependencies:
"@eslint-community/eslint-utils" "^4.9.1"
"@typescript-eslint/scope-manager" "8.62.1"
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/typescript-estree" "8.62.1"
"@typescript-eslint/scope-manager" "8.67.0"
"@typescript-eslint/types" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/visitor-keys@8.62.1":
version "8.62.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz#499657d77ffafb8a99eb1d6c97847ca430234722"
integrity sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==
"@typescript-eslint/visitor-keys@8.67.0":
version "8.67.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz#601d40af9acf82a28da2286f3edafc69bba9017f"
integrity sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==
dependencies:
"@typescript-eslint/types" "8.62.1"
"@typescript-eslint/types" "8.67.0"
eslint-visitor-keys "^5.0.0"
"@ungap/structured-clone@^1.0.0":
@@ -1440,10 +1430,10 @@ acorn-jsx@^5.3.2:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^8.15.0:
version "8.17.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe"
integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==
acorn@^8.16.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940"
integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==
agent-base@6:
version "6.0.2"
@@ -1462,18 +1452,6 @@ ajv@^6.14.0:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.2.4:
version "1.2.6"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a"
@@ -1506,28 +1484,15 @@ bail@^2.0.0:
resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d"
integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
balanced-match@^4.0.2:
version "4.0.4"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a"
integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==
baseline-browser-mapping@^2.10.38:
version "2.10.40"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz#f372c8eb36ff4ad0b5e7ae467014abef124554ba"
integrity sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==
brace-expansion@^1.1.7:
version "1.1.15"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.15.tgz#a6d90d54067236e5f42570a3b7378d594d9b7738"
integrity sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
baseline-browser-mapping@^2.11.12:
version "2.11.20"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz#26078c7a4b08299656ea7ddceaebec955dc44303"
integrity sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==
brace-expansion@^5.0.5:
version "5.0.7"
@@ -1536,16 +1501,23 @@ brace-expansion@^5.0.5:
dependencies:
balanced-match "^4.0.2"
browserslist@^4.24.0:
version "4.28.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.4.tgz#dd8b8167a32845ff5f8cd6ce13f5abba16cd04c9"
integrity sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==
brace-expansion@^5.0.8:
version "5.0.9"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.9.tgz#7c72438809b5fa5babf54199a1f1c281a6984fcf"
integrity sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==
dependencies:
baseline-browser-mapping "^2.10.38"
caniuse-lite "^1.0.30001799"
electron-to-chromium "^1.5.376"
node-releases "^2.0.48"
update-browserslist-db "^1.2.3"
balanced-match "^4.0.2"
browserslist@^4.24.0:
version "4.28.8"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.8.tgz#a3c79ceb70028527e5da7dafc887f3200b5168c0"
integrity sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==
dependencies:
baseline-browser-mapping "^2.11.12"
caniuse-lite "^1.0.30001809"
electron-to-chromium "^1.5.402"
node-releases "^2.0.53"
update-browserslist-db "^1.3.0"
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
@@ -1555,29 +1527,16 @@ call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
es-errors "^1.3.0"
function-bind "^1.1.2"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001799:
version "1.0.30001800"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz#b896c773e1c39400809415162bb5320371291b36"
integrity sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==
caniuse-lite@^1.0.30001809:
version "1.0.30001810"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz#4970b477dea3278374de9bc43aa8f5d39fc3cda2"
integrity sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==
ccount@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
chalk@^4.0.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
character-entities-html4@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b"
@@ -1625,18 +1584,6 @@ cmdk@^1.1.1:
"@radix-ui/react-id" "^1.1.0"
"@radix-ui/react-primitive" "^2.0.2"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
@@ -1649,11 +1596,6 @@ comma-separated-tokens@^2.0.0:
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
convert-source-map@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
@@ -1755,10 +1697,10 @@ dunder-proto@^1.0.1:
es-errors "^1.3.0"
gopd "^1.2.0"
electron-to-chromium@^1.5.376:
version "1.5.383"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.383.tgz#5bd22306497d454103b289b0fef97260c56d0855"
integrity sha512-I2484/KkAvl8lm9VyjH2JnbOIV0d/UCqT7gbzs6l+o6Vmn9wgB66uVcKX+Vk6HrXtY6fbWTOEXuv8waDTuFNCw==
electron-to-chromium@^1.5.402:
version "1.5.420"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz#fc66d26a722d6f227e2092acdf38dd55b198cb44"
integrity sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==
engine.io-client@~6.5.1:
version "6.5.4"
@@ -1847,11 +1789,13 @@ eslint-plugin-react-refresh@^0.5.3:
resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz#0311218631193fc1ea1c37531a1e7085a813bb60"
integrity sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==
eslint-scope@^8.4.0:
version "8.4.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
eslint-scope@^9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-9.1.2.tgz#b9de6ace2fab1cff24d2e58d85b74c8fcea39802"
integrity sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==
dependencies:
"@types/esrecurse" "^4.3.1"
"@types/estree" "^1.0.8"
esrecurse "^4.3.0"
estraverse "^5.2.0"
@@ -1860,42 +1804,34 @@ eslint-visitor-keys@^3.4.3:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
eslint-visitor-keys@^5.0.0:
eslint-visitor-keys@^5.0.0, eslint-visitor-keys@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be"
integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==
eslint@9.39.5:
version "9.39.5"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.5.tgz#2a4e3c8b0f753196efae943c8ffaa8730fc6a3fa"
integrity sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==
eslint@^10.8.1:
version "10.8.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-10.8.1.tgz#fb37d514c19b6dd5b2d6b70169fd26fddfa97967"
integrity sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==
dependencies:
"@eslint-community/eslint-utils" "^4.8.0"
"@eslint-community/regexpp" "^4.12.1"
"@eslint/config-array" "^0.21.2"
"@eslint/config-helpers" "^0.4.2"
"@eslint/core" "^0.17.0"
"@eslint/eslintrc" "^3.3.6"
"@eslint/js" "9.39.5"
"@eslint/plugin-kit" "^0.4.1"
"@eslint-community/regexpp" "^4.12.2"
"@eslint/config-array" "^0.23.5"
"@eslint/config-helpers" "^0.7.0"
"@eslint/core" "^1.2.1"
"@eslint/plugin-kit" "^0.7.2"
"@humanfs/node" "^0.16.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@humanwhocodes/retry" "^0.4.2"
"@types/estree" "^1.0.6"
ajv "^6.14.0"
chalk "^4.0.0"
cross-spawn "^7.0.6"
debug "^4.3.2"
escape-string-regexp "^4.0.0"
eslint-scope "^8.4.0"
eslint-visitor-keys "^4.2.1"
espree "^10.4.0"
esquery "^1.5.0"
eslint-scope "^9.1.2"
eslint-visitor-keys "^5.0.1"
espree "^11.2.0"
esquery "^1.7.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^8.0.0"
@@ -1905,21 +1841,20 @@ eslint@9.39.5:
imurmurhash "^0.1.4"
is-glob "^4.0.0"
json-stable-stringify-without-jsonify "^1.0.1"
lodash.merge "^4.6.2"
minimatch "^3.1.5"
minimatch "^10.2.5"
natural-compare "^1.4.0"
optionator "^0.9.3"
espree@^10.0.1, espree@^10.4.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
espree@^11.2.0:
version "11.2.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-11.2.0.tgz#01d5e47dc332aaba3059008362454a8cc34ccaa5"
integrity sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==
dependencies:
acorn "^8.15.0"
acorn "^8.16.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^4.2.1"
eslint-visitor-keys "^5.0.1"
esquery@^1.5.0:
esquery@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d"
integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==
@@ -2096,11 +2031,6 @@ glob-parent@^6.0.2:
dependencies:
is-glob "^4.0.3"
globals@^14.0.0:
version "14.0.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
globals@^16.5.0:
version "16.5.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-16.5.0.tgz#ccf1594a437b97653b2be13ed4d8f5c9f850cac1"
@@ -2116,11 +2046,6 @@ graceful-fs@^4.2.4:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
@@ -2272,14 +2197,6 @@ ignore@^7.0.5:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9"
integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
import-fresh@^3.2.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf"
integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@@ -2355,13 +2272,6 @@ jotai@^2.20.2:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^4.3.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.1.tgz#01216c001d67f48e2cd560d708c7af21090a3848"
integrity sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==
dependencies:
argparse "^2.0.1"
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
@@ -2567,11 +2477,6 @@ lodash.isplainobject@^4.0.6:
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
longest-streak@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
@@ -3085,12 +2990,12 @@ minimatch@^10.2.2:
dependencies:
brace-expansion "^5.0.5"
minimatch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
minimatch@^10.2.4, minimatch@^10.2.5:
version "10.2.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.6.tgz#fd956bbe0b77241e9f15ac5dccb1c638060968ef"
integrity sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==
dependencies:
brace-expansion "^1.1.7"
brace-expansion "^5.0.8"
ms@^2.1.3:
version "2.1.3"
@@ -3107,10 +3012,10 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
node-releases@^2.0.48:
version "2.0.50"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969"
integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==
node-releases@^2.0.53:
version "2.0.54"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.54.tgz#09af17d5647aa9f221ec5cf2becb95b68a981afe"
integrity sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==
object-assign@^4.1.1:
version "4.1.1"
@@ -3143,13 +3048,6 @@ p-locate@^5.0.0:
dependencies:
p-limit "^3.0.2"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
dependencies:
callsites "^3.0.0"
parse-entities@^4.0.0:
version "4.0.2"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.2.tgz#61d46f5ed28e4ee62e9ddc43d6b010188443f159"
@@ -3447,11 +3345,6 @@ remark-stringify@^11.0.0:
mdast-util-to-markdown "^2.0.0"
unified "^11.0.0"
resolve-from@^4.0.0:
version "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.2.1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/rolldown/-/rolldown-1.2.3.tgz#103bdcbbd575d51265277b8b510f080827b6eb6f"
@@ -3548,11 +3441,6 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-to-js@^1.0.0:
version "1.1.21"
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d"
@@ -3567,13 +3455,6 @@ style-to-object@1.0.14:
dependencies:
inline-style-parser "0.2.7"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
swr@^2.4.2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/swr/-/swr-2.5.0.tgz#186c68b08b3419a5ebba4cea53d776467c9fa2d4"
@@ -3637,15 +3518,15 @@ type-check@^0.4.0, type-check@~0.4.0:
dependencies:
prelude-ls "^1.2.1"
typescript-eslint@^8.62.1:
version "8.62.1"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.62.1.tgz#eb93fd94d527aa04ec5b844fb0b4ada613cc7d3f"
integrity sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==
typescript-eslint@^8.67.0:
version "8.67.0"
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.67.0.tgz#1e92de09ee0ff2d96cc0848f5e9f345ea930d963"
integrity sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==
dependencies:
"@typescript-eslint/eslint-plugin" "8.62.1"
"@typescript-eslint/parser" "8.62.1"
"@typescript-eslint/typescript-estree" "8.62.1"
"@typescript-eslint/utils" "8.62.1"
"@typescript-eslint/eslint-plugin" "8.67.0"
"@typescript-eslint/parser" "8.67.0"
"@typescript-eslint/typescript-estree" "8.67.0"
"@typescript-eslint/utils" "8.67.0"
typescript@~5.9.3:
version "5.9.3"
@@ -3708,10 +3589,10 @@ unist-util-visit@^5.0.0:
unist-util-is "^6.0.0"
unist-util-visit-parents "^6.0.0"
update-browserslist-db@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==
update-browserslist-db@^1.3.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz#9d99fbff56c50bb11ba5fd35cece5916da595836"
integrity sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==
dependencies:
escalade "^3.2.0"
picocolors "^1.1.1"

View File

@@ -1,16 +1,5 @@
preserve_hierarchy: true
files:
- source: /erpnext/locale/main.pot
translation: /erpnext/locale/%two_letters_code%.po
pull_request_title: "fix: sync translations from crowdin"
pull_request_labels:
- translation
- skip-release-notes
pull_request_reviewers:
- barredterra # change to your GitHub username if you copied this file
commit_message: "fix: %language% translations"
append_commit_message: false
languages_mapping:
two_letters_code:
pt-BR: pt_BR
zh-CN: zh
zh-TW: zh_TW

View File

@@ -122,6 +122,7 @@
"description": "Setting Account Type helps in selecting this Account in transactions.",
"fieldname": "account_type",
"fieldtype": "Select",
"in_preview": 1,
"in_standard_filter": 1,
"label": "Account Type",
"oldfieldname": "account_type",
@@ -203,7 +204,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-04-14 18:14:42.202065",
"modified": "2026-09-03 12:59:42.190900",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -264,6 +265,46 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -52,6 +52,42 @@ frappe.treeview_settings["Account"] = {
],
root_label: "Accounts",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the account number renders as a badge (see
// onrender) instead of being glued into the name
return frappe.utils.escape_html(node.data.account_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.account_number) {
flags.push(frappe.ui.badge({ label: node.data.account_number }));
}
const company = frappe.treeview_settings["Account"].treeview?.page?.fields_dict?.company?.get_value();
const company_currency = company && erpnext.get_currency(company);
if (
node.data.account_currency &&
company_currency &&
node.data.account_currency !== company_currency
) {
flags.push(frappe.ui.badge({ label: node.data.account_currency, theme: "blue" }));
}
if (node.data.freeze_account === "Yes") {
flags.push(
frappe.ui.badge({
label: __("Frozen"),
icon: "lock",
title: __("Frozen - entries restricted"),
theme: "orange",
})
);
}
erpnext.utils.render_tree_node_flags(node, flags);
},
on_node_render: function (node, deep) {
const render_balances = () => {
for (let account of cur_tree.account_balance_data) {
@@ -232,7 +268,7 @@ frappe.treeview_settings["Account"] = {
frappe.treeview_settings["Account"].treeview["tree"] = treeview.tree;
if (treeview.can_create) {
treeview.page.set_primary_action(
__("New"),
{ label: __("Add Account"), short_label: __("Add") },
function () {
let root_company = treeview.page.fields_dict.root_company.get_value();
if (root_company) {
@@ -243,13 +279,14 @@ frappe.treeview_settings["Account"] = {
treeview.new_node();
}
},
"add"
"plus"
);
}
},
toolbar: [
{
label: __("Add Child"),
icon: "plus",
condition: function (node) {
return (
frappe.boot.user.can_create.indexOf("Account") !== -1 &&
@@ -272,6 +309,7 @@ frappe.treeview_settings["Account"] = {
return !node.root && frappe.boot.user.can_read.indexOf("GL Entry") !== -1;
},
label: __("View Ledger"),
icon: "book-open",
click: function (node, btn) {
frappe.route_options = {
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
@@ -286,6 +324,106 @@ frappe.treeview_settings["Account"] = {
},
btnClass: "hidden-xs",
},
{
// same label and mechanism as the Account form's Actions button:
// NOT frappe's generic rename (Allow Rename stays off) — this is
// ERPNext's controlled update that rebuilds the derived
// "number - name - abbr" document name
label: __("Update Account Name / Number"),
icon: "text-cursor-input",
condition: function (node) {
return !node.is_root && frappe.model.can_write("Account");
},
click: function (node) {
const dialog = new frappe.ui.Dialog({
title: __("Update Account Number / Name"),
fields: [
{
fieldtype: "Data",
fieldname: "account_name",
label: __("Account Name"),
reqd: 1,
default: node.data.account_name,
},
{
fieldtype: "Data",
fieldname: "account_number",
label: __("Account Number"),
default: node.data.account_number,
},
],
primary_action_label: __("Update"),
primary_action(values) {
dialog.hide();
frappe.dom.freeze(__("Updating {0}", [node.label]));
frappe.call({
method: "erpnext.accounts.doctype.account.account.update_account_number",
args: {
name: node.label,
account_name: values.account_name,
account_number: values.account_number,
},
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees["Account"];
node.parent_node && treeview.tree.load_children(node.parent_node);
},
always: function () {
frappe.dom.unfreeze();
},
});
},
});
dialog.show();
},
},
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Account");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Account")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Account", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -16,6 +16,8 @@ frappe.ui.form.on("Accounting Dimension", {
return {
filters: {
name: ["not in", invalid_doctypes],
istable: 0,
issingle: 0,
},
};
});

View File

@@ -60,6 +60,14 @@ class AccountingDimension(Document):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
meta = frappe.get_meta(self.document_type)
if meta.istable or meta.issingle:
frappe.throw(
_(
"{0} cannot be used as an accounting dimension as it is not a standalone document type."
).format(frappe.bold(self.document_type))
)
exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"])
if exists and self.is_new():

View File

@@ -51,6 +51,23 @@ class TestAccountingDimension(ERPNextTestSuite):
self.assertEqual(gle.get("department"), "_Test Department - _TC")
self.assertEqual(gle1.get("department"), "_Test Department - _TC")
def test_child_table_not_allowed_as_dimension(self):
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Sales Team"})
self.assertRaises(frappe.ValidationError, dimension.insert)
def test_single_doctype_not_allowed_as_dimension(self):
dimension = frappe.get_doc({"doctype": "Accounting Dimension", "document_type": "Selling Settings"})
self.assertRaises(frappe.ValidationError, dimension.insert)
def test_non_scalar_dimension_value_skipped_in_gl_dict(self):
si = create_sales_invoice(do_not_save=1)
si.department = "_Test Department - _TC"
self.assertEqual(si.get_gl_dict({}).get("department"), "_Test Department - _TC")
si.department = ["_Test Department - _TC"]
self.assertNotIn("department", si.get_gl_dict({}))
def test_mandatory(self):
location = frappe.get_doc("Accounting Dimension", "Location")
location.dimension_defaults[0].mandatory_for_bs = True

View File

@@ -95,13 +95,14 @@
"column_break_25",
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"receivable_payable_remarks_length",
"disable_include_dimensions",
"column_break_lvjk",
"general_ledger_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"default_ageing_range",
"column_break_ntmi",
"receivable_payable_remarks_length",
"legacy_section",
"ignore_is_opening_check_for_reporting",
"tab_break_dpet",
@@ -199,10 +200,12 @@
},
{
"default": "1",
"description": "System will use the latest saved Currency Exchange rate on or before the transaction date, however old it is. <br>\nUncheck to ignore rates older than Stale Days and fetch a fresh rate from the exchange rate provider instead.",
"fieldname": "allow_stale",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allow Stale Exchange Rates"
"label": "Allow Stale Exchange Rates",
"show_description_on_click": 1
},
{
"default": "1",
@@ -278,10 +281,10 @@
},
{
"default": "0",
"description": "Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer.",
"description": "Enabling this option prevents the creation of a new Sales Invoice when the customer has an overdue limit set and their outstanding overdue amount exceeds that limit.",
"fieldname": "enable_overdue_billing_threshold",
"fieldtype": "Check",
"label": "Restrict Customer Over Billing"
"label": "Prevent Sales Invoice when Customer is Overdue"
},
{
"depends_on": "eval:doc.enable_overdue_billing_threshold",
@@ -475,7 +478,7 @@
{
"fieldname": "remarks_section",
"fieldtype": "Section Break",
"label": "Remarks Column Length"
"label": "General Ledger Report"
},
{
"default": "0",
@@ -549,7 +552,7 @@
{
"fieldname": "accounts_receivable_payable_tuning_section",
"fieldtype": "Section Break",
"label": "Accounts Receivable / Payable Tuning"
"label": "Accounts Receivable / Payable Report"
},
{
"fieldname": "legacy_section",
@@ -788,6 +791,12 @@
"fieldname": "book_stock_expense_gl_entries",
"fieldtype": "Check",
"label": "Book Stock Expense GL Entries"
},
{
"default": "0",
"fieldname": "disable_include_dimensions",
"fieldtype": "Check",
"label": "Disable \"Consider Accounting Dimension\" Filter"
}
],
"grid_page_length": 50,
@@ -796,7 +805,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-07-15 17:00:00.000000",
"modified": "2026-08-14 15:26:49.070889",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -72,6 +72,7 @@ class AccountsSettings(Document):
default_ageing_range: DF.Data | None
delete_linked_ledger_entries: DF.Check
determine_address_tax_category_from: DF.Literal["Billing Address", "Shipping Address"]
disable_include_dimensions: DF.Check
enable_accounting_dimensions: DF.Check
enable_common_party_accounting: DF.Check
enable_discounts_and_margin: DF.Check
@@ -203,8 +204,8 @@ class AccountsSettings(Document):
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
frappe.throw(
_("You cannot enable both the settings '{0}' and '{1}'.").format(
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
frappe.bold(self.meta.get_translated_label("add_taxes_from_item_tax_template")),
frappe.bold(self.meta.get_translated_label("add_taxes_from_taxes_and_charges_template")),
),
title=_("Auto Tax Settings Error"),
)

View File

@@ -101,7 +101,7 @@
}
],
"links": [],
"modified": "2024-03-27 13:06:36.896195",
"modified": "2026-08-21 23:11:39.423431",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank",
@@ -118,11 +118,20 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -269,7 +269,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2026-04-11 19:46:27.609994",
"modified": "2026-08-21 23:11:39.585456",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -299,6 +299,22 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -4,29 +4,18 @@
import frappe
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(ERPNextTestSuite):
def setUp(self):
frappe.clear_cache()
create_warehouse(
warehouse_name="_Test Warehouse",
properties={"parent_warehouse": "All Warehouses - _TC"},
company="_Test Company",
)
create_item("_Test Item")
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
make_bank_account()
add_transactions()
@@ -139,11 +128,8 @@ def add_transactions():
def make_payment_entry():
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
supplier = create_supplier(supplier_name="_Test Supplier")
pi = make_purchase_invoice(
supplier=supplier.name,
supplier="_Test Supplier",
supplier_warehouse="_Test Warehouse - _TC",
expense_account="Cost of Goods Sold - _TC",
uom="Nos",
@@ -158,10 +144,6 @@ def make_payment_entry():
def make_pos_sales_invoice():
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
@@ -170,13 +152,13 @@ def make_pos_sales_invoice():
)
mode_of_payment.save()
customer = make_customer(customer="_Test Customer")
mode_of_payment = frappe.get_doc("Mode of Payment", "Wire Transfer")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank Clearance - _TC")
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
si = create_sales_invoice(
customer="_Test Customer", item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1
)
si.set("payments", [])
si.append("payments", {"mode_of_payment": "Wire Transfer", "amount": 1000})
si.insert()

View File

@@ -166,9 +166,10 @@ def get_transaction_reference(txn_data: dict) -> str:
).strip()
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
doc = frappe.get_doc("Bank Statement Import", data_import)
doc.check_permission("write")
_file_doc, content = get_file(mt940_file_path)
@@ -235,26 +236,30 @@ def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
def get_preview_from_template(
data_import: str, import_file: str | None = None, google_sheets_url: str | None = None
):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url
)
bsi = frappe.get_doc("Bank Statement Import", data_import)
bsi.check_permission()
return bsi.get_preview_from_template(import_file, google_sheets_url)
@frappe.whitelist()
def form_start_import(data_import: str):
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
return job_id is not None
bsi = frappe.get_doc("Bank Statement Import", data_import)
bsi.check_permission("write")
return bsi.start_import()
@frappe.whitelist()
def download_errored_template(data_import_name: str):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.check_permission()
data_import.export_errored_rows()
@frappe.whitelist()
def download_import_log(data_import_name: str):
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
bsi = frappe.get_doc("Bank Statement Import", data_import_name)
bsi.check_permission()
return bsi.download_import_log()
def is_mt940_format(content: str) -> bool:
@@ -393,6 +398,7 @@ def get_import_status(docname: str):
import_status = {}
data_import = frappe.get_doc("Bank Statement Import", docname)
data_import.check_permission()
import_status["status"] = data_import.status
logs = frappe.get_all(

View File

@@ -23,8 +23,6 @@ from erpnext.tests.utils import ERPNextTestSuite, if_lending_app_installed
class TestBankTransaction(ERPNextTestSuite):
def setUp(self):
make_pos_profile()
# generate and use a uniq hash identifier for 'Bank Account' and it's linked GL 'Account' to avoid validation error
uniq_identifier = frappe.generate_hash(length=10)
gl_account = create_gl_account("_Test Bank " + uniq_identifier)
@@ -32,6 +30,7 @@ class TestBankTransaction(ERPNextTestSuite):
gl_account=gl_account, bank_account_name="Checking Account " + uniq_identifier
)
make_pos_profile()
add_transactions(bank_account=bank_account)
add_vouchers(gl_account=gl_account)
@@ -47,7 +46,7 @@ class TestBankTransaction(ERPNextTestSuite):
from_date=bank_transaction.date,
to_date=utils.today(),
)
self.assertEqual(linked_payments[0]["party"], "Conrad Electronic")
self.assertIn("Conrad Electronic", [payment["party"] for payment in linked_payments])
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):

View File

@@ -729,6 +729,7 @@ def get_ordered_amount(params):
(child.item_code == item_code)
& (parent.docstatus == 1)
& (child.amount > child.billed_amt)
& (child.closed == 0)
& (parent.status != "Closed")
& Criterion.all(get_other_condition(params, child, parent, "Purchase Order"))
)

View File

@@ -16,6 +16,8 @@ frappe.ui.form.on("Chart of Accounts Importer", {
() => generate_tree_preview(frm),
() => create_import_button(frm),
() => frm.set_df_property("chart_preview", "hidden", 0),
// the preview is the point of this page — open it right away
() => frm.fields_dict.chart_preview.collapse(false),
]);
}
@@ -128,7 +130,6 @@ var create_import_button = function (frm) {
freeze_message: __("Creating Accounts..."),
callback: function (r) {
if (!r.exc) {
clearInterval(frm.page["interval"]);
frm.page.set_indicator(__("Import Successful"), "blue");
create_reset_button(frm);
}
@@ -142,42 +143,95 @@ var create_reset_button = function (frm) {
frm.page
.set_primary_action(__("Reset"), function () {
frm.page.clear_primary_action();
delete frm.page["show_import_button"];
frm.reload_doc();
})
.addClass("btn btn-primary");
};
var validate_coa = function (frm) {
if (frm.doc.import_file) {
let parent = __("All Accounts");
return frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
args: {
file_name: frm.doc.import_file,
parent: parent,
doctype: "Chart of Accounts Importer",
file_type: frm.doc.file_type,
for_validate: 1,
},
callback: function (r) {
if (r.message["show_import_button"]) {
frm.page["show_import_button"] = Boolean(r.message["show_import_button"]);
}
},
});
}
};
var generate_tree_preview = function (frm) {
let parent = __("All Accounts");
$(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
const wrapper = $(frm.fields_dict["chart_tree"].wrapper).empty(); // empty wrapper to load new data
// search + expand/collapse-all lean on frappe.ui.Tree helpers added with
// row mode; when running against an older frappe that predates them, skip
// this toolbar so the preview still renders (just without the extras)
const has_row_helpers =
typeof frappe.ui.Tree.prototype.get_expansion_state === "function" &&
typeof frappe.ui.Tree.prototype.filter_nodes === "function";
let tree;
let deep_loaded = false;
let search_text = "";
let update_buttons = () => {};
if (has_row_helpers) {
// same toolbar anatomy as the tree view: search on the left,
// expand/collapse-all on the right (three-state: fully collapsed ->
// Expand All, fully expanded -> Collapse All, partially expanded -> both)
const $toolbar = $('<div class="flex items-center gap-2 mb-2"></div>').appendTo(wrapper);
const search_control = frappe.ui.form.make_control({
df: { fieldtype: "Data", fieldname: "preview_search", placeholder: __("Search") },
parent: $toolbar,
only_input: true,
});
search_control.refresh();
$(search_control.wrapper).addClass("m-0").css("width", "220px");
search_control.$input.addClass("input-xs");
search_control.$input.on(
"input",
frappe.utils.debounce(() => {
search_text = search_control.$input.val();
const run = () => {
// a newer keystroke superseded this one while the deep load ran
if (search_text !== search_control.$input.val()) return;
tree.filter_nodes(search_text);
};
if (!search_text || deep_loaded) {
run();
return;
}
tree.load_children(tree.root_node, true).then(() => {
deep_loaded = true;
run();
});
}, 300)
);
const $actions = $('<div class="ms-auto flex items-center gap-1"></div>').appendTo($toolbar);
update_buttons = () => {
const state = tree.get_expansion_state();
$expand_all.prop("disabled", !(state === "collapsed" || state === "partial"));
$collapse_all.prop("disabled", !(state === "expanded" || state === "partial"));
};
// tooltip on a wrapper: a disabled es-button has pointer-events:none,
// so hover falls through to the wrapper and the tooltip still shows
const make_action = (icon, label, onclick) => {
const $btn = $(
frappe.ui.button({ icon, disabled: true, onclick, attrs: { "aria-label": label } })
);
const $wrapper = $('<span class="inline-flex"></span>').append($btn).appendTo($actions);
frappe.ui.tooltip($wrapper, { text: label });
return $btn;
};
var $expand_all = make_action("chevrons-up-down", __("Expand All"), () => {
tree.load_children(tree.root_node, true).then(() => {
deep_loaded = true;
});
});
var $collapse_all = make_action("chevrons-down-up", __("Collapse All"), () => {
tree.load_children(tree.root_node, false);
});
}
// generate tree structure based on the csv data
return new frappe.ui.Tree({
parent: $(frm.fields_dict["chart_tree"].wrapper),
tree = new frappe.ui.Tree({
parent: wrapper,
label: parent,
expandable: true,
// read-only preview: row-mode visuals without actions or hover cards
// (ignored by an older frappe, which renders the legacy tree)
row_style: true,
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa",
args: {
file_name: frm.doc.import_file,
@@ -185,8 +239,9 @@ var generate_tree_preview = function (frm) {
doctype: "Chart of Accounts Importer",
file_type: frm.doc.file_type,
},
onclick: function (node) {
parent = node.value;
},
on_node_render: () => update_buttons(),
// expanded flips right after this callback — check on the next tick
on_click: () => setTimeout(update_buttons, 0),
});
return tree;
};

View File

@@ -126,7 +126,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-04-14 18:15:27.367298",
"modified": "2026-08-21 23:11:40.799391",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -181,6 +181,54 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Projects Manager",
"select": 1
},
{
"role": "Projects User",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -12,6 +12,19 @@ frappe.treeview_settings["Cost Center"] = {
],
root_label: "Cost Centers",
get_tree_nodes: "erpnext.accounts.utils.get_children",
get_label: function (node) {
// clean display name — the number renders as a badge (see onrender)
return frappe.utils.escape_html(node.data.cost_center_name || node.title || node.label);
},
onrender: function (node) {
if (node.is_root || !node.data) return;
const flags = [];
if (node.data.cost_center_number) {
flags.push(frappe.ui.badge({ label: node.data.cost_center_number }));
}
erpnext.utils.render_tree_node_flags(node, flags);
},
add_tree_node: "erpnext.accounts.utils.add_cc",
menu_items: [
{
@@ -42,6 +55,37 @@ frappe.treeview_settings["Cost Center"] = {
},
],
ignore_fields: ["parent_cost_center"],
toolbar: [
{
label: __("Convert to Group"),
icon: "folder-tree",
condition: function (node) {
return !node.is_root && !node.expandable && frappe.model.can_write("Cost Center");
},
click: function (node) {
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_ledger_to_group");
},
},
{
label: __("Convert to Non-Group"),
icon: "file-text",
condition: function (node) {
// only on groups the user has opened and found empty — a
// group with children can't convert, so don't offer it
return (
!node.is_root &&
node.expandable &&
node.loaded &&
!node.$ul.children().length &&
frappe.model.can_write("Cost Center")
);
},
click: function (node) {
erpnext.accounts.convert_tree_node("Cost Center", node, "convert_group_to_ledger");
},
},
],
extend_toolbar: true,
onload: function (treeview) {
function get_company() {
return treeview.page.fields_dict.company.get_value();
@@ -82,3 +126,22 @@ frappe.treeview_settings["Cost Center"] = {
);
},
};
frappe.provide("erpnext.accounts");
// shared by the Account and Cost Center tree views (defined in both files,
// whichever loads first wins): run the doctype's whitelisted convert method,
// then re-render the branch so the node's group/leaf state updates
erpnext.accounts.convert_tree_node =
erpnext.accounts.convert_tree_node ||
function (doctype, node, method) {
frappe.call({
method: "run_doc_method",
args: { dt: doctype, dn: node.label, method: method },
callback: function (r) {
if (r.exc) return;
const treeview = frappe.views.trees[doctype];
node.parent_node && treeview.tree.load_children(node.parent_node);
frappe.show_alert({ message: __("{0} converted", [node.label]), indicator: "green" });
},
});
};

View File

@@ -125,7 +125,7 @@
}
],
"links": [],
"modified": "2024-11-19 16:35:11.836441",
"modified": "2026-08-21 23:11:41.010871",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Coupon Code",
@@ -179,11 +179,20 @@
"role": "Website Manager",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "coupon_name",
"track_changes": 1
}
}

View File

@@ -234,8 +234,10 @@ frappe.ui.form.on("Dunning", {
dn: frm.doc.name,
},
callback: function (r) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
if (!r.exc) {
var doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
},
});
},

View File

@@ -20,7 +20,7 @@
],
"icon": "fa fa-book",
"links": [],
"modified": "2024-03-27 13:09:44.514241",
"modified": "2026-08-21 23:11:42.386104",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Finance Book",
@@ -55,13 +55,26 @@
"report": 1,
"role": "Auditor",
"share": 1
},
{
"role": "HR Manager",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "finance_book_name",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -31,11 +31,4 @@ class TestFinanceBook(ERPNextTestSuite):
def create_finance_book():
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
finance_book = frappe.get_doc(
{"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
).insert()
else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
return finance_book
return frappe.get_doc("Finance Book", "Test Finance Book 1")

View File

@@ -32,6 +32,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_validat
AccountFilterValidator,
CalculationFormulaValidator,
DependencyValidator,
get_valid_api_method,
)
from erpnext.accounts.report.financial_statements import (
get_columns,
@@ -1182,10 +1183,12 @@ class RowProcessor:
def _process_api_row(self, row) -> RowData:
api_path = row.calculation_formula
# TODO
method = get_valid_api_method(api_path)
try:
values = frappe.call(api_path, filters=self.context.filters, periods=self.period_list, row=row)
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-codeinjection-eval
values = frappe.call(method, filters=self.context.filters, periods=self.period_list, row=row)
if row.reverse_sign:
values = [-1 * v for v in values]

View File

@@ -163,7 +163,7 @@ function show_accounts_tree(template_rows, has_selection) {
fieldname: "company",
fieldtype: "Link",
options: "Company",
label: "Company",
label: __("Company"),
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
onchange: () => {
@@ -176,7 +176,7 @@ function show_accounts_tree(template_rows, has_selection) {
fieldname: "view_type",
fieldtype: "Select",
options: ["Missing Accounts", "Filtered Accounts"],
label: "View",
label: __("View"),
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
reqd: 1,
onchange: () => {
@@ -192,10 +192,10 @@ function show_accounts_tree(template_rows, has_selection) {
{
fieldname: "tip",
fieldtype: "HTML",
label: "Tip",
label: __("Tip"),
options: `
<div class="alert alert-success" role="alert">
Tip: Select report lines to view their accounts
${__("Tip: Select report lines to view their accounts")}
</div>
`,
depends_on: has_selection ? "eval: false" : "eval: true",
@@ -203,7 +203,7 @@ function show_accounts_tree(template_rows, has_selection) {
{
fieldname: "tree_area",
fieldtype: "HTML",
label: "Chart of Accounts",
label: __("Chart of Accounts"),
read_only: 1,
depends_on: "eval: doc.company",
},
@@ -236,6 +236,8 @@ async function refresh_tree_view(dialog, account_rows) {
parent: wrapper,
label: company,
root_value: company,
// read-only preview: row-mode visuals without actions
row_style: true,
method: "erpnext.accounts.doctype.financial_report_template.financial_report_engine.get_children_accounts",
args: { doctype: "Account", company: company, filtered_accounts: filtered_accounts, missed: missed },
toolbar: [],
@@ -288,14 +290,14 @@ function update_formula_label(frm, data_source) {
if (!field) return;
const labels = {
"Account Data": "Account Filter",
"Custom API": "API Method Path",
"Account Data": __("Account Filter"),
"Custom API": __("API Method Path"),
};
grid.update_docfield_property(
"calculation_formula",
"label",
labels[data_source] || "Calculation Formula"
labels[data_source] || __("Calculation Formula")
);
}
@@ -370,7 +372,7 @@ function update_formula_description(frm, data_source) {
description_html = `
<div ${container_style}>
<h5 ${title_style}>Custom API Setup</h5>
<p ${text_style}>Path to your custom method that returns financial data.</p>
<p ${text_style}>Path to your custom whitelisted method that returns financial data. It must permit GET requests.</p>
<h6 ${subtitle_style}>Format:</h6>
<ul ${list_style}>
@@ -380,7 +382,8 @@ function update_formula_description(frm, data_source) {
<h6 ${subtitle_style}>Method Signature:</h6>
<div ${code_style}>
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br>&nbsp; # filters: dict — report filters (company, period, etc.) <br>&nbsp; # periods: list[dict] — period definitions <br>&nbsp; # row: dict — the current report row <br><br>&nbsp; return [1000.0, 1200.0, 1150.0] # one value per period</pre>
<!-- &#10; is used for line breaks since frappe.render replaces newlines with spaces -->
<pre ${pre_style} class="language-python">@frappe.whitelist(methods=["GET"])&#10;def get_custom_data(filters, periods, row):&#10; # filters: dict — report filters (company, period, etc.)&#10; # periods: list[dict] — period definitions&#10; # row: dict — the current report row&#10;&#10; return [1000.0, 1200.0, 1150.0] # one value per period</pre>
</div>
<h6 ${subtitle_style}>Return Format:</h6>

View File

@@ -8,17 +8,40 @@ from dataclasses import dataclass, field
from typing import Any
import frappe
from frappe import _
from frappe import _, is_whitelisted
from frappe.database.operator_map import OPERATOR_MAP
def get_valid_api_method(api_path: str):
"""Resolve `api_path`, ensuring it is whitelisted and permits GET (i.e. read-only)."""
method = frappe.get_attr(api_path)
is_whitelisted(method)
if "GET" not in frappe.allowed_http_methods_for_whitelisted_func.get(method, ()):
frappe.throw(
_("Method {0} must permit GET requests").format(frappe.bold(api_path)),
frappe.PermissionError,
title=_("Method Not Allowed"),
)
return method
def get_formula_field_label(data_source: str) -> str:
# Must mirror the `labels` map in financial_report_template.js (update_formula_label),
labels = {
"Account Data": _("Account Filter"),
"Custom API": _("API Method Path"),
}
return labels.get(data_source, _("Calculation Formula"))
@dataclass
class ValidationIssue:
"""Represents a single validation issue"""
message: str
row_idx: int | None = None
field: str | None = None
details: dict[str, Any] = None
def __post_init__(self):
@@ -26,10 +49,9 @@ class ValidationIssue:
self.details = {}
def __str__(self) -> str:
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
field_info = f"[{self.field}] " if self.field else ""
message = f"{prefix}{field_info}{self.message}"
return _(message)
if self.row_idx:
return _("Row {0}: {1}", context="Financial Report Template").format(self.row_idx, self.message)
return self.message
@dataclass
@@ -131,7 +153,9 @@ class TemplateStructureValidator(Validator):
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
result.add_error(
ValidationIssue(
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
message=_(
"Invalid line reference format: '{0}'. Must start with letter and contain only letters, numbers, underscores, and hyphens"
).format(ref_code),
row_idx=row.idx,
)
)
@@ -140,7 +164,7 @@ class TemplateStructureValidator(Validator):
if ref_code in used_codes:
result.add_error(
ValidationIssue(
message=f"Duplicate line reference: '{ref_code}'",
message=_("Duplicate line reference: '{0}'").format(ref_code),
row_idx=row.idx,
)
)
@@ -156,7 +180,7 @@ class TemplateStructureValidator(Validator):
if row.data_source == "Account Data" and not row.balance_type:
result.add_error(
ValidationIssue(
message="Balance Type is required for Account Data",
message=_("Balance Type is required for Account Data"),
row_idx=row.idx,
)
)
@@ -166,7 +190,9 @@ class TemplateStructureValidator(Validator):
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message=f"Formula is required for {row.data_source}",
message=_("{0} is required for {1}").format(
get_formula_field_label(row.data_source), row.data_source
),
row_idx=row.idx,
)
)
@@ -223,7 +249,7 @@ class DependencyValidator(Validator):
cycle = [*path[cycle_start:], node]
result.add_error(
ValidationIssue(
message=f"Circular dependency detected: {''.join(cycle)}",
message=_("Circular dependency detected: {0}").format("".join(cycle)),
)
)
return
@@ -255,7 +281,7 @@ class DependencyValidator(Validator):
row_idx = self._get_row_idx(ref_code)
result.add_error(
ValidationIssue(
message=f"Line References undefined in Formula: {', '.join(undefined)}",
message=_("Line References undefined in Formula: {0}").format(", ".join(undefined)),
row_idx=row_idx,
)
)
@@ -285,9 +311,10 @@ class CalculationFormulaValidator(Validator):
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message="Formula is required for Calculated Amount",
message=_("{0} is required for Calculated Amount").format(
get_formula_field_label(row.data_source)
),
row_idx=row.idx,
field="Formula",
)
)
return result
@@ -299,7 +326,7 @@ class CalculationFormulaValidator(Validator):
if not self._are_parentheses_balanced(formula):
result.add_error(
ValidationIssue(
message="Formula has unbalanced parentheses",
message=_("Formula has unbalanced parentheses"),
row_idx=row.idx,
)
)
@@ -311,7 +338,7 @@ class CalculationFormulaValidator(Validator):
if row.reference_code and row.reference_code in refs:
result.add_error(
ValidationIssue(
message=f"Formula references itself ('{row.reference_code}')",
message=_("Formula references itself ('{0}')").format(row.reference_code),
row_idx=row.idx,
)
)
@@ -321,7 +348,7 @@ class CalculationFormulaValidator(Validator):
if undefined:
result.add_error(
ValidationIssue(
message=f"Formula references undefined codes: {', '.join(undefined)}",
message=_("Formula references undefined codes: {0}").format(", ".join(undefined)),
row_idx=row.idx,
)
)
@@ -331,7 +358,7 @@ class CalculationFormulaValidator(Validator):
if eval_error:
result.add_error(
ValidationIssue(
message=f"Formula evaluation error: {eval_error}",
message=_("Formula evaluation error: {0}").format(eval_error),
row_idx=row.idx,
)
)
@@ -368,7 +395,7 @@ class CalculationFormulaValidator(Validator):
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
if not isinstance(result, (int, float)): # noqa: UP038
return f"Formula must return a numeric value, got {type(result).__name__}"
return _("Formula must return a numeric value, got {0}").format(type(result).__name__)
return None
except Exception as e:
@@ -391,9 +418,10 @@ class AccountFilterValidator(Validator):
if not row.calculation_formula:
result.add_error(
ValidationIssue(
message="Account filter is required for Account Data",
message=_("{0} is required for Account Data").format(
get_formula_field_label(row.data_source)
),
row_idx=row.idx,
field="Formula",
)
)
return result
@@ -409,18 +437,18 @@ class AccountFilterValidator(Validator):
if error:
result.add_error(
ValidationIssue(
message=error,
message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error),
row_idx=row.idx,
field="Account Filter",
)
)
except json.JSONDecodeError as e:
result.add_error(
ValidationIssue(
message=f"Invalid JSON format: {e!s}",
message=_("{0}: Invalid JSON format: {1}").format(
get_formula_field_label(row.data_source), str(e)
),
row_idx=row.idx,
field="Account Filter",
)
)
@@ -435,36 +463,38 @@ class AccountFilterValidator(Validator):
# simple condition: [field, operator, value]
if isinstance(filter_config, list):
if len(filter_config) != 3:
return "Filter must be [field, operator, value]"
return _("Filter must be [field, operator, value]")
field, operator, value = filter_config
if not isinstance(field, str) or not isinstance(operator, str):
return "Field and operator must be strings"
return _("Field and operator must be strings")
display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
display = (
field if advanced_filtering else self.account_meta.get_translated_label(field)
) or field
if field not in account_fields:
return f"Field '{display}' is not a valid Account field"
return _("Field '{0}' is not a valid Account field").format(display)
if operator.casefold() not in OPERATOR_MAP:
return f"Invalid operator '{operator}'"
return _("Invalid operator '{0}'").format(operator)
if operator in ["in", "not in"] and not isinstance(value, list):
return f"Operator '{operator}' requires a list value"
return _("Operator '{0}' requires a list value").format(operator)
# logical condition: {"and": [condition1, condition2]}
elif isinstance(filter_config, dict):
if len(filter_config) != 1:
return "Logical condition must have exactly one operator"
return _("Logical condition must have exactly one operator")
op = next(iter(filter_config.keys())).lower()
if op not in ["and", "or"]:
return "Logical operators must be 'and' or 'or'"
return _("Logical operators must be 'and' or 'or'")
conditions = filter_config[next(iter(filter_config.keys()))]
if not isinstance(conditions, list) or len(conditions) < 1:
return "Logical conditions need at least 1 sub-condition"
return _("Logical conditions need at least 1 sub-condition")
# recursive
for condition in conditions:
@@ -472,7 +502,7 @@ class AccountFilterValidator(Validator):
if error:
return error
else:
return "Filter must be a list or dict"
return _("Filter must be a list or dict")
return None
@@ -508,34 +538,31 @@ class FormulaValidator(Validator):
if "." not in api_path:
result.add_error(
ValidationIssue(
message="Custom API path should be in format: app.module.method",
message=_("{0} should be in format: app.module.method").format(
get_formula_field_label(row.data_source)
),
row_idx=row.idx,
field="Formula",
)
)
return result
# Method exists?
try:
module_path, method_name = api_path.rsplit(".", 1)
module = frappe.get_module(module_path)
if not hasattr(module, method_name):
result.add_error(
ValidationIssue(
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
row_idx=row.idx,
field="Formula",
)
)
get_valid_api_method(api_path)
except Exception as e:
result.add_error(
ValidationIssue(
message=f"Could not validate API path: {e!s}",
row_idx=row.idx,
field="Formula",
if isinstance(e, frappe.PermissionError | frappe.ValidationError):
# frappe.throw inside get_valid_api_method logs a message that would pop up in UI
frappe.clear_last_message()
if isinstance(e, frappe.PermissionError):
message = _("{0}: Method '{1}' must be whitelisted and permit GET requests").format(
get_formula_field_label(row.data_source), api_path
)
)
else:
message = _("Could not validate {0}: {1}").format(
get_formula_field_label(row.data_source), str(e)
)
result.add_error(ValidationIssue(message=message, row_idx=row.idx))
return result

View File

@@ -2,7 +2,12 @@
# For license information, please see license.txt
import frappe
from frappe.tests.utils import whitelist_for_tests
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import (
FormulaValidator,
get_valid_api_method,
)
from erpnext.tests.utils import ERPNextTestSuite
@@ -72,3 +77,90 @@ class FinancialReportTemplateTestCase(ERPNextTestSuite):
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
)
return template
def not_whitelisted_method(**kwargs):
return [42.0]
@whitelist_for_tests(methods=["POST"])
def whitelisted_post_only_method(**kwargs):
return [42.0]
@whitelist_for_tests(methods=["GET"])
def whitelisted_get_method(**kwargs):
return [42.0]
class TestCustomAPIValidation(FinancialReportTemplateTestCase):
"""Custom API rows must point to whitelisted methods that permit GET"""
TEST_MODULE = "erpnext.accounts.doctype.financial_report_template.test_financial_report_template"
NOT_WHITELISTED = f"{TEST_MODULE}.not_whitelisted_method"
WHITELISTED_POST_ONLY = f"{TEST_MODULE}.whitelisted_post_only_method"
WHITELISTED_GET = f"{TEST_MODULE}.whitelisted_get_method"
def create_api_template(self, api_path):
template = self.create_test_template_with_rows(
[
{
"reference_code": "API001",
"display_name": "API Row",
"data_source": "Custom API",
"calculation_formula": api_path,
}
]
)
template.report_type = "Profit and Loss Statement"
return template
def test_get_valid_api_method(self):
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.NOT_WHITELISTED)
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.WHITELISTED_POST_ONLY)
self.assertEqual(get_valid_api_method(self.WHITELISTED_GET), frappe.get_attr(self.WHITELISTED_GET))
def test_save_rejects_invalid_api_methods(self):
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
template = self.create_api_template(api_path)
self.assertRaises(frappe.ValidationError, template.insert)
def test_save_allows_get_whitelisted_method(self):
template = self.create_api_template(self.WHITELISTED_GET)
template.insert()
template.delete()
def test_engine_rejects_invalid_api_methods(self):
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
ReportContext,
RowProcessor,
)
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
template = self.create_api_template(api_path)
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
processor = RowProcessor(context)
self.assertRaises(frappe.PermissionError, processor._process_api_row, template.rows[0])
def test_engine_calls_valid_api_method(self):
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
ReportContext,
RowProcessor,
)
template = self.create_api_template(self.WHITELISTED_GET)
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
processor = RowProcessor(context)
row_data = processor._process_api_row(template.rows[0])
self.assertEqual(row_data.values, [42.0])
def test_validation_keeps_message_log_clean(self):
validator = FormulaValidator(frappe._dict(rows=[]))
message_count = len(frappe.local.message_log)
# last path raises AppNotInstalledError, which also logs a message via frappe.throw
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY, "missing_app.api.method"):
row = frappe._dict(data_source="Custom API", calculation_formula=api_path, idx=1)
result = validator.validate(row)
self.assertFalse(result.is_valid)
self.assertEqual(len(frappe.local.message_log), message_count)

View File

@@ -82,7 +82,7 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2024-05-27 17:29:55.560840",
"modified": "2026-08-21 23:11:42.509102",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
@@ -131,10 +131,15 @@
{
"read": 1,
"role": "Auditor"
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "name",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -134,7 +134,7 @@ class GLEntry(Document):
mandatory = ["account", "voucher_type", "voucher_no", "company"]
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
frappe.throw(_("{0} is required").format(self.meta.get_translated_label(k)))
if not self.is_cancelled and not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")

View File

@@ -57,7 +57,7 @@
}
],
"links": [],
"modified": "2024-03-27 13:09:55.573483",
"modified": "2026-08-21 23:11:43.571355",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -95,12 +95,61 @@
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Item Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
},
{
"role": "Stock User",
"select": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -8,6 +8,6 @@ def get_data():
{"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
{"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
{"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
{"label": _("Stock"), "items": ["Item Groups", "Item"]},
{"label": _("Stock"), "items": ["Item Group", "Item"]},
],
}

View File

@@ -249,7 +249,7 @@ Object.assign(erpnext.journal_entry, {
);
}
if (frm.doc.docstatus == 1) {
if (frm.doc.docstatus == 1 && !frm.doc.reversal_of) {
frm.add_custom_button(
__("Reverse Journal Entry"),
() => erpnext.journal_entry.reverse_journal_entry(frm),
@@ -677,6 +677,6 @@ Object.assign(erpnext.journal_entry, {
} else {
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, cdt, cdn);
}
frm.refresh_field("accounts");
frm.get_field("accounts").grid.refresh_row(cdn);
},
});

View File

@@ -27,6 +27,7 @@ def get_payment_entry_against_order(
) -> dict | Document:
"""Build an advance-payment Journal Entry against an unbilled Sales/Purchase Order."""
ref_doc = frappe.get_doc(dt, dn)
ref_doc.check_permission()
if flt(ref_doc.per_billed, 2) > 0:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
@@ -78,6 +79,8 @@ def get_payment_entry_against_invoice(
) -> dict | Document:
"""Build a payment Journal Entry against a Sales/Purchase Invoice's outstanding amount."""
ref_doc = frappe.get_doc(dt, dn)
ref_doc.check_permission()
if dt == "Sales Invoice":
party_type = "Customer"
party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to
@@ -118,6 +121,8 @@ def get_payment_entry(ref_doc, args: dict) -> dict | Document:
Returns the Journal Entry document when `args["journal_entry"]` is truthy, otherwise its
dict (for client calls).
"""
frappe.has_permission("Journal Entry", ptype="create", throw=True)
je = frappe.new_doc("Journal Entry")
je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")})
@@ -222,6 +227,20 @@ def make_inter_company_journal_entry(name: str, voucher_type: str, company: str)
@frappe.whitelist()
def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Document | None = None) -> Document:
"""Map a submitted Journal Entry to a reversing one (debits and credits swapped)."""
# `get_mapped_doc` checks this as well, but the guards below disclose which entry
# reverses which, so read access has to be settled before they run
if not frappe.has_permission("Journal Entry", doc=source_name):
frappe.throw(_("Not permitted"), frappe.PermissionError)
reversal_of = frappe.db.get_value("Journal Entry", source_name, "reversal_of")
if reversal_of:
frappe.throw(
_("{0} is already a Reverse Journal Entry of {1}. Cancel it instead of reversing it.").format(
get_link_to_form("Journal Entry", source_name),
get_link_to_form("Journal Entry", reversal_of),
)
)
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
if existing_reverse:
frappe.throw(

View File

@@ -301,6 +301,26 @@ class TestJournalEntry(ERPNextTestSuite):
self.check_gl_entries()
def test_disallow_reversal_of_a_reversal_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.mapper import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank - _TC", "Sales - _TC", 100, submit=True)
rjv = make_reverse_journal_entry(jv.name)
rjv.posting_date = nowdate()
rjv.submit()
self.assertRaisesRegex(
frappe.ValidationError,
"is already a Reverse Journal Entry",
make_reverse_journal_entry,
rjv.name,
)
# the guard must not disclose the reversal to a user who cannot read the entry
with self.set_user("Guest"):
self.assertRaises(frappe.PermissionError, make_reverse_journal_entry, rjv.name)
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)

View File

@@ -56,7 +56,9 @@ class LedgerMerge(Document):
@frappe.whitelist()
def form_start_merge(docname: str):
return frappe.get_doc("Ledger Merge", docname).start_merge()
lm_doc = frappe.get_doc("Ledger Merge", docname)
lm_doc.check_permission("write")
return lm_doc.start_merge()
def start_merge(docname):

View File

@@ -147,14 +147,14 @@
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"links": [],
"modified": "2024-03-27 13:10:03.361383",
"modified": "2026-08-21 23:11:44.144864",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Loyalty Program",
@@ -171,11 +171,20 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -1,6 +1,6 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from unittest.mock import patch
import frappe
from frappe.query_builder.functions import Sum
@@ -196,7 +196,7 @@ class TestLoyaltyProgram(ERPNextTestSuite):
for d in company_wise_info:
self.assertTrue(d.get("loyalty_points"))
@unittest.mock.patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
@patch("erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_details")
def test_tier_selection(self, mock_get_loyalty_details):
# Create a new loyalty program with multiple tiers
loyalty_program = frappe.get_doc(

View File

@@ -48,7 +48,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-14 18:16:47.795986",
"modified": "2026-08-21 23:11:44.763131",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Mode of Payment",
@@ -76,6 +76,30 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -46,7 +46,7 @@
"icon": "fa fa-bar-chart",
"idx": 1,
"links": [],
"modified": "2024-03-27 13:10:05.873547",
"modified": "2026-08-21 23:11:44.908490",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
@@ -69,9 +69,14 @@
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -297,6 +297,9 @@ def start_import(invoices):
invoice_number = d.invoice_number
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
# the outstanding amount is entered inclusive of tax, so taxes must not
# be added on top of it
doc.flags.dont_auto_add_taxes = True
doc.insert(set_name=invoice_number)
doc.submit()
if not frappe.in_test:

View File

@@ -4,9 +4,11 @@
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
from erpnext.accounts.doctype.tax_rule.test_tax_rule import make_tax_rule
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.tests.utils import ERPNextTestSuite
@@ -126,6 +128,55 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
for invoice in invoices:
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"add_taxes_from_taxes_and_charges_template": 1, "add_taxes_from_item_tax_template": 0},
)
def test_opening_invoice_creation_without_taxes(self):
company = "_Test Opening Invoice Company"
template = frappe.get_doc(
{
"doctype": "Sales Taxes and Charges Template",
"company": company,
"title": "_Test Opening Invoice Tax",
"taxes": [
{
"charge_type": "On Net Total",
"account_head": create_account(
account_name="_Test Opening Tax Account",
parent_account="Duties and Taxes - _TOIC",
account_type="Tax",
company=company,
),
"description": "Test taxes",
"rate": 9,
}
],
}
).insert()
# makes the template the default for the party, as it would be on a live site
make_tax_rule(tax_type="Sales", company=company, sales_tax_template=template.name, save=1)
tool = self.make_invoices(company=company, return_doc=True)
invoices = tool.make_invoices()
self.assertEqual(len(invoices), 2)
# outstanding amount is entered inclusive of tax, so taxes must not be added on top of it
for invoice in invoices:
si = frappe.get_doc("Sales Invoice", invoice)
self.assertFalse(si.taxes)
self.assertEqual(si.grand_total, 200)
self.assertEqual(si.outstanding_amount, 200)
# the same invoice created outside the tool keeps the default taxes,
# since adding them there is the user's decision
si = frappe.get_doc(tool.get_invoices()[0])
si.flags.ignore_mandatory = True
si.insert()
self.assertTrue(si.taxes)
self.assertEqual(si.grand_total, 218)
def test_opening_entry_project_linking(self):
doc = self.make_invoices(
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True

View File

@@ -476,6 +476,8 @@ frappe.ui.form.on("Payment Entry", {
return {
query: "erpnext.controllers.queries.employee_query",
};
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
return erpnext.queries.party(frm.doc);
} else if (frm.doc.party_type == "Shareholder") {
return {
filters: {
@@ -1277,8 +1279,14 @@ frappe.ui.form.on("Payment Entry", {
await frappe.after_ajax();
const base_paid_amount = frm.doc.base_paid_amount || 0;
const base_received_amount = frm.doc.base_received_amount || 0;
let other_deductions = 0;
if (frm.doc.payment_type === "Internal Transfer") {
other_deductions = (frm.doc.deductions || [])
.filter((row) => !row.is_exchange_gain_loss)
.reduce((sum, row) => sum + flt(row.amount), 0);
}
const exchange_gain_loss = flt(
base_paid_amount - base_received_amount,
base_paid_amount - base_received_amount - other_deductions,
get_deduction_amount_precision()
);
@@ -1855,11 +1863,19 @@ frappe.ui.form.on("Payment Entry Deduction", {
},
amount: function (frm) {
frm.events.set_unallocated_amount(frm);
if (frm.doc.payment_type === "Internal Transfer") {
frm.events.set_exchange_gain_loss_deduction(frm);
} else {
frm.events.set_unallocated_amount(frm);
}
},
deductions_remove: function (frm) {
frm.events.set_unallocated_amount(frm);
if (frm.doc.payment_type === "Internal Transfer") {
frm.events.set_exchange_gain_loss_deduction(frm);
} else {
frm.events.set_unallocated_amount(frm);
}
},
});

View File

@@ -40,6 +40,7 @@ from erpnext.accounts.party import (
complete_contact_details,
get_default_contact,
get_party_account,
validate_party_company,
)
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
@@ -277,7 +278,8 @@ class PaymentEntry(AccountsController):
if not liability_account:
throw(
_("Please set default {0} in Company {1}").format(
frappe.bold(frappe.get_meta("Company").get_label(fieldname)), frappe.bold(self.company)
frappe.bold(frappe.get_meta("Company").get_translated_label(fieldname)),
frappe.bold(self.company),
)
)
@@ -660,7 +662,7 @@ class PaymentEntry(AccountsController):
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
if not self.get(field):
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
frappe.throw(_("{0} is mandatory").format(self.meta.get_translated_label(field)))
def validate_reference_documents(self):
valid_reference_doctypes = self.get_valid_reference_doctypes()
@@ -1116,8 +1118,14 @@ class PaymentEntry(AccountsController):
)
def set_exchange_gain_loss(self):
other_deductions = 0
if self.payment_type == "Internal Transfer":
other_deductions = sum(
flt(row.amount) for row in self.get("deductions") if not row.is_exchange_gain_loss
)
exchange_gain_loss = flt(
self.base_paid_amount - self.base_received_amount,
self.base_paid_amount - self.base_received_amount - other_deductions,
self.precision("amount", "deductions"),
)
@@ -1150,7 +1158,7 @@ class PaymentEntry(AccountsController):
if value:
continue
label = _(frappe.get_meta("Company").get_label(fieldname))
label = frappe.get_meta("Company").get_translated_label(fieldname)
return frappe.msgprint(
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
label, get_link_to_form("Company", self.company)
@@ -2434,6 +2442,7 @@ def get_party_details(company: str, party_type: str, party: str, date: str, cost
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
validate_party_company(party_type, party, company)
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)
@@ -2617,7 +2626,11 @@ def get_payment_entry(
reference_date: str | date | None = None,
created_from_payment_request: bool | None = None,
):
frappe.has_permission("Payment Entry", ptype="create", throw=True)
doc = frappe.get_doc(dt, dn)
doc.check_permission()
over_billing_allowance = frappe.get_single_value("Accounts Settings", "over_billing_allowance")
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))

View File

@@ -789,7 +789,6 @@ class TestPaymentEntry(ERPNextTestSuite):
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "bank_charges_account", bank_charges_account)
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "bank_charges_account", "")
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Internal Transfer"
@@ -834,7 +833,6 @@ class TestPaymentEntry(ERPNextTestSuite):
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "bank_charges_account", bank_charges_account)
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "bank_charges_account", "")
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Internal Transfer"
@@ -870,6 +868,64 @@ class TestPaymentEntry(ERPNextTestSuite):
self.validate_gl_entries(pe.name, expected_gle)
def test_cross_currency_transfer_splits_bank_charge_and_exchange_gain_loss(self):
exchange_gain_loss_account = frappe.db.get_value(
"Company", "_Test Company", "exchange_gain_loss_account"
)
bank_charges_account = create_account(
parent_account="Indirect Expenses - _TC",
account_name="_Test Bank Charges",
company="_Test Company",
)
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Internal Transfer"
pe.company = "_Test Company"
pe.paid_from = "_Test Bank USD - _TC"
pe.paid_to = "_Test Bank - _TC"
pe.paid_amount = 100
pe.source_exchange_rate = 50
pe.received_amount = 4500
pe.reference_no = "6"
pe.reference_date = nowdate()
pe.append(
"deductions",
{
"account": bank_charges_account,
"cost_center": "_Test Cost Center - _TC",
"amount": 100,
},
)
pe.setup_party_account_field()
pe.set_missing_values()
pe.set_exchange_rate()
pe.set_amounts()
deductions = {d.account: d for d in pe.deductions}
self.assertEqual(deductions[bank_charges_account].amount, 100)
self.assertEqual(deductions[exchange_gain_loss_account].amount, 400)
self.assertTrue(deductions[exchange_gain_loss_account].is_exchange_gain_loss)
self.assertEqual(pe.difference_amount, 0)
for d in pe.deductions:
d.cost_center = "_Test Cost Center - _TC"
pe.insert()
pe.submit()
expected_gle = dict(
(d[0], d)
for d in [
["_Test Bank USD - _TC", 0, 5000, None],
["_Test Bank - _TC", 4500, 0, None],
[exchange_gain_loss_account, 400.0, 0, None],
[bank_charges_account, 100.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
def test_payment_against_negative_sales_invoice(self):
si1 = create_sales_invoice()
@@ -1051,8 +1107,6 @@ class TestPaymentEntry(ERPNextTestSuite):
)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_account", gain_account)
frappe.db.set_value("Company", "_Test Company", "exchange_loss_account", loss_account)
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_gain_account", "")
self.addCleanup(frappe.db.set_value, "Company", "_Test Company", "exchange_loss_account", "")
si_gain = create_sales_invoice(
customer="_Test Customer USD",

View File

@@ -92,6 +92,7 @@ def get_supplier_query(doctype: str, txt: str, searchfield: str, start: int, pag
@frappe.whitelist()
def make_payment_records(name: str, supplier: str, mode_of_payment: str | None = None):
doc = frappe.get_doc("Payment Order", name)
doc.check_permission()
make_journal_entry(doc, supplier, mode_of_payment)

View File

@@ -647,7 +647,7 @@ class PaymentReconciliation(Document):
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname):
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
frappe.throw(_("Please select {0} first").format(self.meta.get_translated_label(fieldname)))
def validate_entries(self):
if not self.get("invoices"):

View File

@@ -201,8 +201,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
)
frappe.db.set_value("Company", self.company, "exchange_gain_account", gain_account)
frappe.db.set_value("Company", self.company, "exchange_loss_account", loss_account)
self.addCleanup(frappe.db.set_value, "Company", self.company, "exchange_gain_account", "")
self.addCleanup(frappe.db.set_value, "Company", self.company, "exchange_loss_account", "")
return gain_account, loss_account
def create_foreign_currency_sales_invoice(self, conversion_rate):
@@ -1331,15 +1329,6 @@ class TestPaymentReconciliation(ERPNextTestSuite):
test_user = "test@example.com"
permitted_ccs = ["_Test Cost Center - _TC", "_Test Cost Center 2 - _TC"]
restricted_cc = "_Test Write Off Cost Center - _TC"
existing_apply_strict_user_permissions = cint(
frappe.db.get_single_value("System Settings", "apply_strict_user_permissions")
)
self.addCleanup(
frappe.db.set_single_value,
"System Settings",
"apply_strict_user_permissions",
existing_apply_strict_user_permissions,
)
transaction_date = nowdate()
rate = 100

View File

@@ -92,6 +92,7 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
freeze: true,
callback: function (data) {
if (!data.exc) {
frm.clear_table("subscription_plans");
$.each(data.message || [], function (i, v) {
var d = frappe.model.add_child(
frm.doc,

View File

@@ -875,6 +875,7 @@ def make_payment_request(**args):
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
party_account_currency = get_account_currency(party_account)
subscription_plans = get_subscription_details(ref_doc.doctype, ref_doc.name)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -906,12 +907,24 @@ def make_payment_request(**args):
or gateway_account.get("payment_channel", "Email") != "Email"
),
"phone_number": args.get("phone_number") if args.get("phone_number") else None,
"is_a_subscription": 1 if subscription_plans else 0,
}
)
if selected_payment_schedules:
apply_payment_references(pr, payment_reference)
if subscription_plans:
pr.set(
"subscription_plans",
[
{
"plan": row.plan,
"qty": row.qty,
}
for row in subscription_plans
],
)
# Dimensions
pr.update(
{
@@ -1225,20 +1238,25 @@ def get_dummy_message(doc):
@frappe.whitelist()
def get_subscription_details(reference_doctype: str, reference_name: str):
if reference_doctype == "Sales Invoice":
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:
plans = frappe.get_doc("Subscription", subscription.sub_name).plans
for plan in plans:
subscription_plans.append(plan)
return subscription_plans
def get_subscription_details(reference_doctype: str, reference_name: str) -> list[dict]:
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
if not frappe.get_meta(reference_doctype).has_field("subscription"):
return []
subscription = frappe.db.get_value(reference_doctype, reference_name, "subscription")
if not subscription:
return []
return frappe.get_all(
"Subscription Plan Detail",
filters={"parent": subscription, "parenttype": "Subscription", "parentfield": "plans"},
fields=[
"plan",
"qty",
],
)
@frappe.whitelist()
@@ -1341,6 +1359,7 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
@frappe.whitelist()
def get_available_payment_schedules(reference_doctype: str, reference_name: str):
ref_doc = frappe.get_doc(reference_doctype, reference_name)
ref_doc.check_permission()
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
return []

View File

@@ -11,15 +11,27 @@ from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.payment_request.payment_request import (
get_subscription_details,
make_payment_request,
)
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.subscription.test_subscription import (
create_plan,
create_subscription,
make_plans,
)
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestSuite
PAYMENT_URL = "https://example.com/payment"
SEND_EMAIL_MOCK = MagicMock(return_value=None)
GET_PAYMENT_URL_MOCK = MagicMock(return_value=PAYMENT_URL)
GET_PAYMENT_GATEWAY_CONTROLLER_MOCK = MagicMock()
payment_gateways = [
{"doctype": "Payment Gateway", "gateway": "_Test Gateway"},
@@ -62,6 +74,18 @@ payment_method = [
]
@patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
new=SEND_EMAIL_MOCK,
)
@patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
new=GET_PAYMENT_URL_MOCK,
)
@patch(
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
new=GET_PAYMENT_GATEWAY_CONTROLLER_MOCK,
)
class TestPaymentRequest(ERPNextTestSuite):
def setUp(self):
for payment_gateway in payment_gateways:
@@ -80,24 +104,11 @@ class TestPaymentRequest(ERPNextTestSuite):
):
frappe.get_doc(method).insert(ignore_permissions=True)
send_email = patch(
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.send_email",
return_value=None,
)
self.send_email = send_email.start()
self.addCleanup(send_email.stop)
get_payment_url = patch(
# this also shadows one (1) call to _get_payment_gateway_controller
"erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url",
return_value=PAYMENT_URL,
)
self.get_payment_url = get_payment_url.start()
self.addCleanup(get_payment_url.stop)
_get_payment_gateway_controller = patch(
"erpnext.accounts.doctype.payment_request.payment_request._get_payment_gateway_controller",
)
self._get_payment_gateway_controller = _get_payment_gateway_controller.start()
self.addCleanup(_get_payment_gateway_controller.stop)
for mock in (SEND_EMAIL_MOCK, GET_PAYMENT_URL_MOCK, GET_PAYMENT_GATEWAY_CONTROLLER_MOCK):
mock.reset_mock()
self.send_email = SEND_EMAIL_MOCK
self.get_payment_url = GET_PAYMENT_URL_MOCK
self._get_payment_gateway_controller = GET_PAYMENT_GATEWAY_CONTROLLER_MOCK
def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR", do_not_save=True)
@@ -2009,3 +2020,140 @@ class TestPaymentRequestV2Gateway(ERPNextTestSuite):
call_kwargs = mock_log_error.call_args
self.assertIn("Payment Initialization Failed", str(call_kwargs))
self.assertIn("_Test Gateway", str(call_kwargs))
def test_payment_request_with_subscription(self):
make_plans()
subscription_plan = frappe.get_doc("Subscription Plan", "_Test Plan Name")
subscription_plan.payment_gateway = "_Test Gateway - INR - _TC"
subscription_plan.save()
subscription = create_subscription(
plans=[{"plan": "_Test Plan Name", "qty": 1}],
start_date=nowdate(),
generate_invoice_at="Prepaid (bill at period start)",
submit_invoice=1,
)
invoice_name = frappe.get_value(
"Sales Invoice",
{
"subscription": subscription.name,
"docstatus": 1,
"is_return": 0,
},
"name",
order_by="from_date asc",
)
payment_request = make_payment_request(
dt="Sales Invoice",
dn=invoice_name,
recipient_id="test@example.com",
)
self.assertEqual(payment_request.is_a_subscription, 1)
self.assertEqual(len(payment_request.subscription_plans), 1)
subscription_plan = payment_request.subscription_plans[0]
self.assertEqual(subscription_plan.plan, "_Test Plan Name")
self.assertEqual(subscription_plan.qty, 1)
self.assertEqual(payment_request.reference_doctype, "Sales Invoice")
self.assertEqual(payment_request.reference_name, invoice_name)
def test_payment_request_without_subscription(self):
si = create_sales_invoice()
payment_request = make_payment_request(
dt="Sales Invoice",
dn=si.name,
recipient_id="test@example.com",
)
self.assertEqual(payment_request.is_a_subscription, 0)
self.assertEqual(len(payment_request.subscription_plans), 0)
self.assertEqual(payment_request.reference_doctype, "Sales Invoice")
self.assertEqual(payment_request.reference_name, si.name)
def test_payment_request_with_subscription_for_purchase_invoice(self):
make_plans()
subscription_plan = frappe.get_doc("Subscription Plan", "_Test Plan Name")
subscription_plan.payment_gateway = "_Test Gateway - INR - _TC"
subscription_plan.save()
subscription = create_subscription(
party_type="Supplier",
party="_Test Supplier",
plans=[{"plan": "_Test Plan Name", "qty": 1}],
start_date=nowdate(),
generate_invoice_at="Prepaid (bill at period start)",
submit_invoice=1,
)
invoice_name = frappe.get_value(
"Purchase Invoice",
{
"subscription": subscription.name,
"docstatus": 1,
"is_return": 0,
},
"name",
order_by="from_date asc",
)
payment_request = make_payment_request(
dt="Purchase Invoice",
dn=invoice_name,
party_type="Supplier",
party="_Test Supplier",
recipient_id="test@example.com",
)
self.assertEqual(payment_request.is_a_subscription, 1)
self.assertEqual(len(payment_request.subscription_plans), 1)
subscription_plan = payment_request.subscription_plans[0]
self.assertEqual(subscription_plan.plan, "_Test Plan Name")
self.assertEqual(subscription_plan.qty, 1)
self.assertEqual(payment_request.reference_doctype, "Purchase Invoice")
self.assertEqual(payment_request.reference_name, invoice_name)
def test_payment_request_without_subscription_for_purchase_invoice(self):
pi = make_purchase_invoice()
payment_request = make_payment_request(
dt="Purchase Invoice",
dn=pi.name,
party_type="Supplier",
party=pi.supplier,
recipient_id="test@example.com",
)
self.assertEqual(payment_request.is_a_subscription, 0)
self.assertEqual(len(payment_request.subscription_plans), 0)
self.assertEqual(payment_request.reference_doctype, "Purchase Invoice")
self.assertEqual(payment_request.reference_name, pi.name)
def test_get_subscription_details_returns_empty_for_doctype_without_subscription_field(self):
so = make_sales_order()
self.assertEqual(get_subscription_details("Sales Order", so.name), [])
def test_get_subscription_details_requires_read_permission_on_reference(self):
si = create_sales_invoice()
restricted_user = "no-roles@example.com"
if not frappe.db.exists("User", restricted_user):
user = frappe.new_doc("User")
user.email = restricted_user
user.first_name = "No Roles"
user.send_welcome_email = 0
user.insert()
accounts_user = "accounts-user@example.com"
if not frappe.db.exists("User", accounts_user):
user = frappe.new_doc("User")
user.email = accounts_user
user.first_name = "Accounts"
user.send_welcome_email = 0
user.add_roles("Accounts User")
with self.set_user(restricted_user):
self.assertRaises(frappe.PermissionError, get_subscription_details, "Sales Invoice", si.name)
with self.set_user(accounts_user):
self.assertEqual(get_subscription_details("Sales Invoice", si.name), [])

View File

@@ -116,7 +116,7 @@
}
],
"links": [],
"modified": "2024-03-27 13:10:11.511137",
"modified": "2026-08-21 23:11:45.693762",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Term",
@@ -157,11 +157,36 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -263,12 +263,15 @@ def get_cashiers(doctype: str, txt: str, searchfield: str, start: int, page_len:
@frappe.whitelist()
def get_invoices(start: str | datetime, end: str | datetime, pos_profile: str, user: str):
invoice_doctype = frappe.db.get_single_value("POS Settings", "invoice_type")
frappe.has_permission("POS Profile", doc=pos_profile, throw=True)
frappe.has_permission("Sales Invoice", throw=True)
sales_inv_query = build_invoice_query("Sales Invoice", user, pos_profile, start, end)
query = sales_inv_query
if invoice_doctype == "POS Invoice":
frappe.has_permission("POS Invoice", throw=True)
pos_inv_query = build_invoice_query("POS Invoice", user, pos_profile, start, end)
query = query + pos_inv_query

View File

@@ -21,13 +21,12 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPOSClosingEntry(ERPNextTestSuite):
def setUp(self):
init_user_and_profile()
self.test_user, self.pos_profile = init_user_and_profile()
make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
def test_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -59,8 +58,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
Test if POS Closing Entry is created without item code
"""
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -79,10 +77,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
test_item_qty = get_test_item_qty(pos_profile)
test_item_qty = get_test_item_qty(self.pos_profile)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -104,13 +101,11 @@ class TestPOSClosingEntry(ERPNextTestSuite):
pcv_doc.flags.in_test = True
pcv_doc.submit()
opening_entry = create_opening_entry(pos_profile, test_user.name)
test_item_qty_after_sales = get_test_item_qty(pos_profile)
test_item_qty_after_sales = get_test_item_qty(self.pos_profile)
self.assertEqual(test_item_qty_after_sales, test_item_qty - 1)
def test_cancelling_of_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
@@ -169,9 +164,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
pos_profile.insert()
self.assertTrue(frappe.db.exists("POS Profile", pos_profile.name))
test_user = init_user_and_profile(do_not_create_pos_profile=1)
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(pos_profile, self.test_user.name)
pos_inv1 = create_pos_invoice(rate=350, do_not_submit=1, pos_profile=pos_profile.name)
pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
pos_inv1.save()
@@ -195,9 +188,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
def test_merging_into_sales_invoice_for_batched_item(self):
frappe.flags.print_message = False
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.stock.doctype.batch.batch import get_batch_qty
item_doc = make_item(
@@ -220,8 +210,7 @@ class TestPOSClosingEntry(ERPNextTestSuite):
)
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
pos_inv = create_pos_invoice(
item_code=item_code,
@@ -291,18 +280,17 @@ class TestPOSClosingEntry(ERPNextTestSuite):
@ERPNextTestSuite.change_settings("POS Settings", {"invoice_type": "Sales Invoice"})
def test_closing_entries_with_sales_invoice(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
opening_entry = create_opening_entry(self.pos_profile, self.test_user.name)
pos_si = create_sales_invoice(
qty=10, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
qty=10, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
)
pos_si.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
pos_si.save()
pos_si.submit()
pos_si2 = create_sales_invoice(
qty=5, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=11
qty=5, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=11
)
pos_si2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000})
pos_si2.save()
@@ -332,12 +320,10 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.sales_invoice.mapper import make_sales_return
test_user, pos_profile = init_user_and_profile()
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
pos_si1, pos_si2 = create_multiple_sales_invoices(self.pos_profile)
pos_inv = create_pos_invoice(rate=100, do_not_save=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
@@ -357,13 +343,13 @@ class TestPOSClosingEntry(ERPNextTestSuite):
self.assertEqual(pos_si2.pos_closing_entry, pcv_doc1.name)
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
opening_entry2 = create_opening_entry(pos_profile, test_user.name)
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.pos_profile)
# Trying to create Sales Invoice when invoice_type is set to POS Invoice.
pos_si3 = create_sales_invoice(
qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
qty=1, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
)
pos_si3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
self.assertRaises(frappe.ValidationError, pos_si3.save)
@@ -394,16 +380,14 @@ class TestPOSClosingEntry(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
test_user, pos_profile = init_user_and_profile()
with self.change_settings("POS Settings", {"invoice_type": "POS Invoice"}):
opening_entry1 = create_opening_entry(pos_profile, test_user.name)
opening_entry1 = create_opening_entry(self.pos_profile, self.test_user.name)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(pos_profile)
pos_inv1, pos_inv2 = create_multiple_pos_invoices(self.pos_profile)
# Trying to create Sales Invoice when invoice_type is set to POS Invoice.
pos_sinv = create_sales_invoice(
qty=1, is_created_using_pos=1, pos_profile=pos_profile.name, do_not_save=1
qty=1, is_created_using_pos=1, pos_profile=self.pos_profile.name, do_not_save=1
)
pos_sinv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})
self.assertRaises(frappe.ValidationError, pos_sinv.save)
@@ -421,9 +405,9 @@ class TestPOSClosingEntry(ERPNextTestSuite):
self.assertEqual(pcv_doc1.grand_total, 300)
with self.change_settings("POS Settings", {"invoice_type": "Sales Invoice"}):
opening_entry2 = create_opening_entry(pos_profile, test_user.name)
opening_entry2 = create_opening_entry(self.pos_profile, self.test_user.name)
pos_si1, pos_si2 = create_multiple_sales_invoices(pos_profile)
pos_si1, pos_si2 = create_multiple_sales_invoices(self.pos_profile)
pos_inv3 = create_pos_invoice(rate=100, do_not_save=1)
pos_inv3.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 100})

View File

@@ -497,6 +497,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
"depends_on": "customer",
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
@@ -1642,7 +1643,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-06-21 12:46:13.250145",
"modified": "2026-08-21 23:11:45.029925",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1685,6 +1686,14 @@
"permlevel": 1,
"read": 1,
"role": "All"
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -4,6 +4,7 @@ import copy
import frappe
from frappe import _
from frappe.utils import add_to_date
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
@@ -53,14 +54,14 @@ class TestPOSInvoice(POSInvoiceTestMixin):
w2 = frappe.get_doc(w.doctype, w.name)
import time
time.sleep(1)
w.save()
import time
time.sleep(1)
frappe.db.set_value(
w.doctype,
w.name,
"modified",
add_to_date(w.modified, seconds=1),
update_modified=False,
)
self.assertRaises(frappe.TimestampMismatchError, w2.save)
def test_change_naming_series(self):
@@ -902,9 +903,6 @@ class TestPOSInvoice(POSInvoiceTestMixin):
self.assertEqual(pos_inv.items[0].rate, 300)
def test_delivered_serial_no_case(self):
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@@ -916,8 +914,6 @@ class TestPOSInvoice(POSInvoiceTestMixin):
self.assertEqual(serial_no, delivered_serial_no)
init_user_and_profile()
pos_inv = create_pos_invoice(
item_code="_Test Serialized Item With Series",
serial_no=[serial_no],
@@ -931,13 +927,9 @@ class TestPOSInvoice(POSInvoiceTestMixin):
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import create_item
init_user_and_profile()
from erpnext.stock.utils import get_stock_balance
frappe.set_user("Administrator")
@@ -959,9 +951,18 @@ class TestPOSInvoice(POSInvoiceTestMixin):
is_stock_item=1,
)
# Add initial stock: SubA=5, SubB=2
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
# Set initial stock to SubA=5 and SubB=2, even when this test is rerun on the same site.
for item_code, target_qty in ((sub_item_a, 5), (sub_item_b, 2)):
balance = get_stock_balance(item_code, warehouse)
difference = target_qty - balance
if difference:
make_stock_entry(
item_code=item_code,
to_warehouse=warehouse if difference > 0 else None,
from_warehouse=warehouse if difference < 0 else None,
qty=abs(difference),
company=company,
)
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
bundle_item = "_Test Bundle"
@@ -1010,16 +1011,19 @@ class TestPOSInvoice(POSInvoiceTestMixin):
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
if not args.pos_profile:
pos_profile = make_pos_profile()
pos_profile.save()
pos_profile_name = args.pos_profile
if not pos_profile_name:
pos_profile_name = frappe.db.exists("POS Profile", "_Test POS Profile")
if not pos_profile_name:
pos_profile = make_pos_profile()
pos_profile.save()
pos_profile_name = pos_profile.name
pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1
pos_inv.is_pos = 1
pos_inv.pos_profile = args.pos_profile or pos_profile.name
pos_inv.pos_profile = pos_profile_name
if args.posting_date:
pos_inv.set_posting_time = 1

View File

@@ -26,14 +26,10 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
pos_inv.save()
@@ -55,14 +51,10 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
pos_inv.append(
@@ -107,9 +99,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import (
init_user_and_profile,
)
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
@@ -121,7 +110,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
make_item(item, {"is_stock_item": 1})
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
pos_inv.append(

View File

@@ -582,7 +582,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2026-02-10 14:24:48.597412",
"modified": "2026-08-21 23:11:45.419667",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@@ -606,6 +606,10 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"role": "Sales Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -289,6 +289,11 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
user = frappe.session["user"]
company = filters.get("company") or frappe.defaults.get_user_default("company")
allowed_pos_profiles = frappe.get_list("POS Profile", pluck="name")
if not allowed_pos_profiles:
return {}
pf = frappe.qb.DocType("POS Profile")
pfu = frappe.qb.DocType("POS Profile User")
@@ -298,6 +303,7 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
.on(pfu.parent == pf.name)
.select(pf.name)
.where((pfu.user == user) & (pf.company == company) & pf.name.like(f"%{txt}%") & (pf.disabled == 0))
.where(pf.name.isin(allowed_pos_profiles))
.limit(page_len)
.offset(start)
.run()
@@ -314,6 +320,7 @@ def pos_profile_query(doctype: str, txt: str, searchfield: str, start: int, page
& (pf.company == company)
& pf.name.like(f"%{txt}%")
& (pf.disabled == 0)
& (pf.name.isin(allowed_pos_profiles))
)
.run()
)

View File

@@ -12,8 +12,9 @@
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
"in_list_view": 1,
"label": "Fieldname",
"read_only": 1
},
{
"fieldname": "field",
@@ -26,7 +27,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-29 18:08:40.323579",
"modified": "2026-08-31 20:41:12.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Search Fields",

View File

@@ -1,40 +1,9 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
let search_fields_datatypes = [
"Data",
"Link",
"Dynamic Link",
"Long Text",
"Select",
"Small Text",
"Text",
"Text Editor",
];
let do_not_include_fields = [
"naming_series",
"item_code",
"item_name",
"stock_uom",
"asset_naming_series",
"default_material_request_type",
"valuation_method",
"warranty_period",
"weight_uom",
"batch_number_series",
"serial_no_series",
"purchase_uom",
"customs_tariff_number",
"sales_uom",
"deferred_revenue_account",
"deferred_expense_account",
"quality_inspection_template",
"route",
"slideshow",
"website_image_alt",
"thumbnail",
"web_long_description",
];
function is_valid_invoice_field(df) {
return frappe.model.no_value_type.indexOf(df.fieldtype) === -1 || df.fieldtype === "Button";
}
frappe.ui.form.on("POS Settings", {
onload: function (frm) {
@@ -42,74 +11,97 @@ frappe.ui.form.on("POS Settings", {
frm.trigger("add_search_options");
},
invoice_type: function (frm) {
frm.trigger("get_invoice_fields");
},
get_invoice_fields: function (frm) {
frappe.model.with_doctype("POS Invoice", () => {
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
if (
frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
["Button"].includes(d.fieldtype)
) {
return { label: d.label + " (" + d.fieldtype + ")", value: d.fieldname };
} else {
return null;
}
});
const invoice_type = frm.doc.invoice_type;
if (!invoice_type) return;
frappe.model.with_doctype(invoice_type, () => {
// the invoice type can change again while the meta loads
if (frm.doc.invoice_type !== invoice_type) return;
const fields = frappe.get_doc("DocType", invoice_type).fields.filter(is_valid_invoice_field);
frm.fields_dict.invoice_fields.grid.update_docfield_property(
"fieldname",
"options",
[""].concat(fields)
[""].concat(
fields.map((df) => {
return { label: `${df.label} (${df.fieldtype})`, value: df.fieldname };
})
)
);
frm.trigger("validate_invoice_fields");
});
},
validate_invoice_fields: function (frm) {
const valid_fieldnames = frappe
.get_doc("DocType", frm.doc.invoice_type)
.fields.filter(is_valid_invoice_field)
.map((df) => df.fieldname);
const invalid_fields = (frm.doc.invoice_fields || [])
.filter((row) => row.fieldname && !valid_fieldnames.includes(row.fieldname))
.map((row) => `#${row.idx} ${row.fieldname}`);
if (!invalid_fields.length) return;
frappe.msgprint({
title: __("Invalid POS Fields"),
indicator: "orange",
message: __("The following rows are not valid fields of {0} and have to be removed: {1}", [
frm.doc.invoice_type.bold(),
invalid_fields.join(", "),
]),
});
},
add_search_options: function (frm) {
frappe.model.with_doctype("Item", () => {
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
if (
search_fields_datatypes.includes(d.fieldtype) &&
!do_not_include_fields.includes(d.fieldname)
) {
return [d.label];
} else {
return null;
}
});
frappe.call({
method: "erpnext.accounts.doctype.pos_settings.pos_settings.get_pos_search_field_options",
callback: ({ message }) => {
const fields = message || [];
fields.unshift("");
frm.fields_dict.pos_search_fields.grid.update_docfield_property("field", "options", fields);
frm.searchable_item_fields = Object.fromEntries(
fields.map((df) => [df.option, df.fieldname])
);
frm.fields_dict.pos_search_fields.grid.update_docfield_property(
"field",
"options",
[""].concat(fields.map((df) => df.option))
);
},
});
},
});
frappe.ui.form.on("POS Search Fields", {
field: function (frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
return d;
} else {
return null;
}
})[0];
const doc = frappe.get_doc(doctype, name);
doc.fieldname = df.fieldname;
frm.refresh_field("fields");
doc.fieldname = frm.searchable_item_fields?.[doc.field] || "";
frm.refresh_field("pos_search_fields");
},
});
frappe.ui.form.on("POS Field", {
fieldname: function (frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
return doc.fieldname == d.fieldname ? d : null;
})[0];
const doc = frappe.get_doc(doctype, name);
const invoice_meta = frappe.get_doc("DocType", frm.doc.invoice_type);
const df = invoice_meta?.fields.find((d) => d.fieldname === doc.fieldname);
if (!df) return;
doc.label = df.label;
doc.reqd = df.reqd;
doc.options = df.options;
doc.fieldtype = df.fieldtype;
doc.default_value = df.default;
frm.refresh_field("fields");
frm.refresh_field("invoice_fields");
},
});

View File

@@ -5,8 +5,46 @@ from collections import Counter
import frappe
from frappe import _
from frappe.model import no_value_fields
from frappe.model.document import Document
SEARCH_FIELD_TYPES = (
"Data",
"Link",
"Dynamic Link",
"Long Text",
"Select",
"Small Text",
"Text",
"Text Editor",
)
# Item fields that are of a searchable fieldtype, but are not meaningful to search a POS item by
DO_NOT_INCLUDE_FIELDS = (
"naming_series",
"item_code",
"item_name",
"stock_uom",
"asset_naming_series",
"default_material_request_type",
"valuation_method",
"warranty_period",
"weight_uom",
"batch_number_series",
"serial_no_series",
"purchase_uom",
"customs_tariff_number",
"sales_uom",
"deferred_revenue_account",
"deferred_expense_account",
"quality_inspection_template",
"route",
"slideshow",
"website_image_alt",
"thumbnail",
"web_long_description",
)
class POSSettings(Document):
# begin: auto-generated types
@@ -32,17 +70,10 @@ class POSSettings(Document):
if old_doc.invoice_type != self.invoice_type:
self.validate_invoice_type()
self.validate_duplicate_invoice_fields()
self.validate_invoice_fields()
def validate_invoice_fields(self):
invoice_fields = [field.fieldname for field in self.invoice_fields]
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
if len(duplicate_invoice_fields):
for field in duplicate_invoice_fields:
frappe.throw(
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
)
self.validate_duplicate_pos_search_fields()
self.validate_pos_search_fields()
def validate_invoice_type(self):
pos_opening_entries_count = frappe.db.count(
@@ -55,3 +86,94 @@ class POSSettings(Document):
),
title=_("Invoice Document Type Selection Error"),
)
def validate_duplicate_invoice_fields(self):
invoice_fields = [field.fieldname for field in self.invoice_fields]
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
if len(duplicate_invoice_fields):
for field in duplicate_invoice_fields:
frappe.throw(
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
)
def validate_invoice_fields(self):
if not self.invoice_type:
return
meta = frappe.get_meta(self.invoice_type)
for field in self.invoice_fields:
df = meta.get_field(field.fieldname)
if not df or not is_valid_invoice_field(df):
frappe.throw(
title=_("Invalid POS Field"),
msg=_("Row #{0}: '{1}' is not a valid field of {2}.").format(
field.idx, frappe.bold(field.fieldname or ""), frappe.bold(_(self.invoice_type))
),
)
# read only in the form, so keep them in sync with the invoice
field.label = df.label
field.fieldtype = df.fieldtype
field.options = df.options
def validate_duplicate_pos_search_fields(self):
fieldnames = [field.fieldname for field in self.pos_search_fields]
duplicate_fieldnames = {key for key, value in Counter(fieldnames).items() if value > 1}
for fieldname in duplicate_fieldnames:
frappe.throw(
title=_("Duplicate POS Search Fields"),
msg=_("'{0}' has been already added.").format(fieldname),
)
def validate_pos_search_fields(self):
searchable_fields = {df.fieldname: df for df in get_searchable_item_fields()}
for field in self.pos_search_fields:
df = searchable_fields.get(field.fieldname)
if not df:
frappe.throw(
title=_("Invalid POS Search Field"),
msg=_("Row #{0}: '{1}' cannot be used to search items.").format(
field.idx, frappe.bold(field.fieldname or "")
),
)
if field.field != get_search_field_option(df):
frappe.throw(
title=_("Invalid POS Search Field"),
msg=_("Row #{0}: '{1}' does not match {2}.").format(
field.idx, frappe.bold(field.field or ""), frappe.bold(df.fieldname)
),
)
def is_valid_invoice_field(df):
return df.fieldtype not in no_value_fields or df.fieldtype == "Button"
def get_searchable_item_fields():
return [
df
for df in frappe.get_meta("Item").fields
if df.fieldtype in SEARCH_FIELD_TYPES and df.fieldname not in DO_NOT_INCLUDE_FIELDS
]
def get_search_field_option(df):
# the fieldname keeps the option unique, two Item fields can share a label
return f"{df.label} ({df.fieldname})"
@frappe.whitelist()
def get_pos_search_field_options():
frappe.has_permission("POS Settings", throw=True)
return [
{"option": get_search_field_option(df), "fieldname": df.fieldname}
for df in get_searchable_item_fields()
]

View File

@@ -1,8 +1,135 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.patches.v16_0.append_fieldname_to_pos_search_fields import execute as append_fieldname
from erpnext.tests.utils import ERPNextTestSuite
class TestPOSSettings(ERPNextTestSuite):
pass
def setUp(self):
self.settings = frappe.get_single("POS Settings")
self.settings.invoice_fields = []
self.settings.pos_search_fields = []
def assertInvalid(self, message):
with self.assertRaises(frappe.ValidationError) as context:
self.settings.save()
self.assertIn(message, str(context.exception))
def test_invoice_field_is_validated_against_invoice_type(self):
# consolidated_invoice exists on POS Invoice only
self.settings.invoice_type = "POS Invoice"
self.settings.append("invoice_fields", {"fieldname": "consolidated_invoice"})
self.settings.save()
self.settings.invoice_type = "Sales Invoice"
self.assertInvalid("is not a valid field of")
def test_field_common_to_both_invoice_types_is_allowed(self):
for invoice_type in ("POS Invoice", "Sales Invoice"):
self.settings.invoice_type = invoice_type
self.settings.invoice_fields = []
self.settings.append("invoice_fields", {"fieldname": "po_no"})
self.settings.save()
def test_unknown_invoice_field_is_not_allowed(self):
self.settings.append("invoice_fields", {"fieldname": "not_a_field"})
self.assertInvalid("is not a valid field of")
def test_layout_invoice_field_is_not_allowed(self):
self.settings.append("invoice_fields", {"fieldname": "accounting_dimensions_section"})
self.assertInvalid("is not a valid field of")
def test_invoice_field_properties_are_set_from_the_invoice(self):
self.settings.append(
"invoice_fields", {"fieldname": "customer", "label": "Tampered", "fieldtype": "Data"}
)
self.settings.save()
field = self.settings.invoice_fields[0]
self.assertEqual(field.label, "Customer")
self.assertEqual(field.fieldtype, "Link")
self.assertEqual(field.options, "Customer")
def test_searchable_item_field_is_allowed(self):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.settings.save()
self.assertEqual(self.settings.pos_search_fields[0].fieldname, "description")
def test_excluded_search_field_is_not_allowed(self):
self.settings.append(
"pos_search_fields", {"field": "Item Name (item_name)", "fieldname": "item_name"}
)
self.assertInvalid("cannot be used to search items")
def test_search_field_of_unsearchable_type_is_not_allowed(self):
# maintain stock is a Check field
self.settings.append(
"pos_search_fields", {"field": "Maintain Stock (is_stock_item)", "fieldname": "is_stock_item"}
)
self.assertInvalid("cannot be used to search items")
def test_unknown_search_field_is_not_allowed(self):
self.settings.append(
"pos_search_fields", {"field": "Nope (not_an_item_field)", "fieldname": "not_an_item_field"}
)
self.assertInvalid("cannot be used to search items")
def test_search_field_without_a_fieldname_is_not_allowed(self):
# the form fills the fieldname in, it cannot be picked on its own
self.settings.append("pos_search_fields", {"field": "Description (description)"})
self.assertInvalid("cannot be used to search items")
def test_search_field_option_must_match_its_fieldname(self):
self.settings.append("pos_search_fields", {"field": "Brand (brand)", "fieldname": "description"})
self.assertInvalid("does not match")
def test_bare_label_is_not_accepted_as_a_search_field(self):
# the stored option carries the fieldname, the patch backfills older rows
self.settings.append("pos_search_fields", {"field": "Description", "fieldname": "description"})
self.assertInvalid("does not match")
def test_duplicate_search_fields_are_not_allowed(self):
for _ in range(2):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.assertInvalid("has been already added")
def test_patch_appends_the_fieldname_to_a_legacy_search_field(self):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.settings.save()
row = self.settings.pos_search_fields[0].name
frappe.db.set_value("POS Search Fields", row, "field", "Description", update_modified=False)
append_fieldname()
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
def test_patch_leaves_an_already_migrated_search_field_alone(self):
self.settings.append(
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
)
self.settings.save()
append_fieldname()
row = self.settings.pos_search_fields[0].name
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
def test_invoice_fields_are_skipped_when_no_invoice_type_is_selected(self):
self.settings.invoice_type = None
self.settings.append("invoice_fields", {"fieldname": "customer"})
self.settings.save()
self.assertEqual(self.settings.invoice_fields[0].fieldname, "customer")

View File

@@ -187,7 +187,10 @@ class PricingRule(Document):
tocheck = frappe.scrub(self.get("applicable_for", ""))
if tocheck and not self.get(tocheck):
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
throw(
_("{0} is required").format(self.meta.get_translated_label(tocheck)),
frappe.MandatoryError,
)
if self.apply_rule_on_other:
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)

View File

@@ -142,6 +142,8 @@ def start_pcv_processing(docname: str):
@frappe.whitelist()
def pause_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
@@ -157,6 +159,8 @@ def pause_pcv_processing(docname: str):
@frappe.whitelist()
def cancel_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="cancel", doc=docname, throw=True)
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
@@ -171,6 +175,8 @@ def cancel_pcv_processing(docname: str):
@frappe.whitelist()
def resume_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()

View File

@@ -10,6 +10,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.mapper import get_qty_already_mapped
@frappe.whitelist()
@@ -52,6 +53,11 @@ def make_purchase_receipt(
args = {}
args = frappe.parse_json(args)
mapped_qty_by_item = get_qty_already_mapped(target_doc, "purchase_invoice_item")
def received_and_mapped_qty(obj):
return flt(obj.received_qty) + flt(mapped_qty_by_item.get(obj.name, 0))
def post_parent_process(source_parent, target_parent):
remove_items_with_zero_qty(target_parent)
set_missing_values(source_parent, target_parent)
@@ -75,15 +81,13 @@ def make_purchase_receipt(
or {}
)
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
obj.conversion_factor
)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
)
pending_qty = flt(obj.qty) - received_and_mapped_qty(obj)
target.qty = pending_qty - flt(returned_qty_map.get("qty"))
target.received_qty = pending_qty
target.stock_qty = (pending_qty - flt(returned_qty_map.get("qty"))) * flt(obj.conversion_factor)
target.amount = pending_qty * flt(obj.rate)
target.base_amount = pending_qty * flt(obj.rate) * flt(source_parent.conversion_rate)
def select_item(d):
filtered_items = args.get("filtered_children", [])
@@ -113,7 +117,8 @@ def make_purchase_receipt(
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
"condition": lambda doc: abs(received_and_mapped_qty(doc)) < abs(doc.qty)
and select_item(doc),
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges",

View File

@@ -512,6 +512,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
"label": "Currency and Price List",
@@ -1695,7 +1696,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-08-05 15:40:16.519774",
"modified": "2026-08-21 23:11:46.733125",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1748,6 +1749,18 @@
"read": 1,
"role": "Accounts Manager",
"write": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -235,6 +235,9 @@ class PurchaseInvoice(BuyingController):
"overflow_type": "billing",
}
]
self.closed_source_links = [
("Purchase Invoice Item", "pr_detail", "Purchase Receipt Item", "Purchase Receipt")
]
def onload(self):
super().onload()
@@ -279,9 +282,7 @@ class PurchaseInvoice(BuyingController):
self.check_conversion_rate()
self.validate_credit_to_acc()
self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
self.check_for_on_hold_or_closed_status(
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
)
self.check_purchase_order_on_hold_or_close("purchase_order", exclude_if_field="purchase_receipt")
self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
@@ -297,6 +298,7 @@ class PurchaseInvoice(BuyingController):
BillingValidationService(self).validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.set_status()
self.validate_purchase_receipt_if_update_stock()
self.validate_exchange_rate_with_purchase_receipt()
validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_invoice_reference
)
@@ -320,6 +322,47 @@ class PurchaseInvoice(BuyingController):
if total_billed_qty and total_received_qty:
self.per_received = total_received_qty / total_billed_qty * 100
def validate_exchange_rate_with_purchase_receipt(self):
if self.is_internal_transfer() or not erpnext.is_perpetual_inventory_enabled(self.company):
return
stock_items = self.get_stock_items()
receipts = {
item.purchase_receipt
for item in self.items
if item.purchase_receipt and item.item_code in stock_items
}
if not receipts:
return
if frappe.db.get_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"):
return
mismatched = [
f"{frappe.bold(row.name)} ({row.conversion_rate})"
for row in frappe.get_all(
"Purchase Receipt",
filters={"name": ("in", list(receipts))},
fields=["name", "currency", "conversion_rate"],
)
if row.currency == self.currency
and flt(row.conversion_rate)
and flt(row.conversion_rate) != flt(self.conversion_rate)
]
if not mismatched:
return
frappe.throw(
_(
"Exchange rate {0} does not match the exchange rate of Purchase Receipt {1}. Use the same exchange rate as the Purchase Receipt or enable {2} in {3} to adjust the landed cost based on this invoice."
).format(
frappe.bold(self.conversion_rate),
", ".join(mismatched),
frappe.bold(_("Set Landed Cost Based on Purchase Invoice Rate")),
get_link_to_form("Buying Settings", "Buying Settings", _("Buying Settings")),
)
)
def validate_invoice_hold(self):
if self.is_return:
frappe.throw(_("Return Purchase Invoice cannot be held."))
@@ -732,9 +775,7 @@ class PurchaseInvoice(BuyingController):
super().on_cancel()
PurchaseTaxWithholding(self).on_cancel()
self.check_for_on_hold_or_closed_status(
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
)
self.check_purchase_order_on_hold_or_close("purchase_order", exclude_if_field="purchase_receipt")
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return

View File

@@ -130,6 +130,9 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
get_purchase_document_details,
)
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
get_custom_dimension_overrides,
)
doc = self.doc
tax_service = TaxService(doc)
@@ -270,25 +273,34 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
# Amount added through landed-cost-voucher
if landed_cost_entries:
if (item.item_code, item.name) in landed_cost_entries:
for account, base_amount in landed_cost_entries[
(item.item_code, item.name)
].items():
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(base_amount["base_amount"]),
"credit_in_account_currency": flt(base_amount["amount"]),
"credit_in_transaction_currency": item.net_amount,
"project": item.project or doc.project,
},
item=item,
)
for entry in landed_cost_entries.get((item.item_code, item.name), []):
if not (entry.amount or entry.base_amount):
continue
lcv_account_currency = get_account_currency(entry.expense_account)
credit_in_transaction_currency = (
flt(entry.amount)
if lcv_account_currency == doc.currency
else flt(
entry.base_amount / doc.conversion_rate, item.precision("net_amount")
)
)
gl_dict = self.get_gl_dict(
{
"account": entry.expense_account,
"against": item.expense_account,
"cost_center": entry.dimensions.cost_center or item.cost_center,
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(entry.base_amount),
"credit_in_account_currency": flt(entry.amount),
"credit_in_transaction_currency": credit_in_transaction_currency,
"project": entry.dimensions.project or item.project or doc.project,
},
item=item,
)
gl_dict.update(get_custom_dimension_overrides(entry))
gl_entries.append(gl_dict)
# sub-contracting warehouse
if flt(item.rm_supp_cost):

View File

@@ -578,10 +578,6 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
original_value = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
pr = make_purchase_receipt(
@@ -594,25 +590,15 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
pi = create_purchase_invoice(pr.name)
pi.conversion_rate = 80
self.assertRaises(frappe.ValidationError, pi.insert)
pi.conversion_rate = 70
pi.insert()
pi.submit()
# Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
# fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[0].base_net_amount - pr.items[0].base_net_amount
)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", original_value
self.assertFalse(
frappe.db.exists("GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name})
)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
@@ -620,7 +606,8 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
make_purchase_invoice as create_purchase_invoice,
)
# Creating Purchase Invoice with USD currency
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
pr = frappe.new_doc("Purchase Receipt")
pr.currency = "USD"
pr.company = "_Test Company with perpetual inventory"
@@ -634,34 +621,20 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
"rate": 100,
},
)
pr.append(
"items",
{"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"},
)
pr.insert()
pr.submit()
# Createing purchase invoice against Purchase Receipt
pi = create_purchase_invoice(pr.name)
pi.conversion_rate = 70
pi.credit_to = "_Test Payable USD - TCP1"
pi.insert()
pi.submit()
# Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
# fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "credit"
self.assertFalse(
frappe.db.exists("GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name})
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[1].base_net_amount - pr.items[1].base_net_amount
)
self.assertEqual(flt(discrepancy_caused_by_exchange_rate_diff, 2), amount)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(self.globalTestRecords["Purchase Invoice"][1])
pi.insert()
@@ -3553,7 +3526,6 @@ def make_purchase_invoice_against_cost_center(**args):
def setup_provisional_accounting(**args):
args = frappe._dict(args)
create_item("_Test Non Stock Item", is_stock_item=0)
company = args.company or "_Test Company"
provisional_account = create_account(
account_name=args.account_name or "Provision Account",

View File

@@ -77,7 +77,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2024-03-27 13:10:26.945131",
"modified": "2026-08-21 23:11:47.506282",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
@@ -104,12 +104,29 @@
{
"read": 1,
"role": "Purchase User"
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -27,6 +27,7 @@ def start_payment_ledger_repost(docname: str | None = None):
"""
if docname:
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
repost_doc.check_permission("submit")
if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
try:
for entry in repost_doc.repost_vouchers:

View File

@@ -170,6 +170,10 @@
"shipping_address_section",
"shipping_address_name",
"shipping_address",
"shipping_contact_person",
"shipping_contact_display",
"shipping_contact_mobile",
"shipping_contact_email",
"shipping_addr_col_break",
"dispatch_address_name",
"dispatch_address",
@@ -589,6 +593,46 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "shipping_contact_person",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
"label": "Shipping Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"fetch_from": "shipping_contact_person.full_name",
"fieldname": "shipping_contact_display",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "Shipping Contact",
"read_only": 1
},
{
"fetch_from": "shipping_contact_person.mobile_no",
"fieldname": "shipping_contact_mobile",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "Shipping Contact Mobile No",
"options": "Phone",
"read_only": 1
},
{
"fetch_from": "shipping_contact_person.email_id",
"fieldname": "shipping_contact_email",
"fieldtype": "Data",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Shipping Contact Email",
"options": "Email",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company_address",
"fieldtype": "Link",
@@ -609,6 +653,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
"depends_on": "customer",
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
@@ -2360,7 +2405,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-08-11 12:00:00.000000",
"modified": "2026-08-14 12:43:19.480555",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -202,6 +202,10 @@ class SalesInvoice(SellingController):
set_warehouse: DF.Link | None
shipping_address: DF.TextEditor | None
shipping_address_name: DF.Link | None
shipping_contact_display: DF.SmallText | None
shipping_contact_email: DF.Data | None
shipping_contact_mobile: DF.SmallText | None
shipping_contact_person: DF.Link | None
shipping_rule: DF.Link | None
status: DF.Literal[
"",
@@ -274,6 +278,9 @@ class SalesInvoice(SellingController):
"overflow_type": "billing",
}
]
self.closed_source_links = [
("Sales Invoice Item", "dn_detail", "Delivery Note Item", "Delivery Note")
]
def set_indicator(self):
"""Set indicator for portal"""
@@ -610,6 +617,7 @@ class SalesInvoice(SellingController):
"percent_join_field": "sales_order",
"status_field": "delivery_status",
"keyword": "Delivered",
"exclude_field": "skip_delivery",
"second_source_dt": "Delivery Note Item",
"second_source_field": "qty",
"second_join_field": "so_detail",

View File

@@ -476,7 +476,7 @@ class SalesInvoiceGLComposer(BaseGLComposer):
for payment_mode in doc.payments:
if skip_change_gl_entries and payment_mode.account == doc.account_for_change_amount:
payment_mode.base_amount -= flt(doc.change_amount)
payment_mode.base_amount -= flt(doc.base_change_amount)
if payment_mode.base_amount:
# POS, make payment entries

View File

@@ -7,7 +7,7 @@ import json
import frappe
from frappe import qb
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.utils import add_days, cint, flt, format_date, getdate, nowdate, today
from frappe.utils import add_days, add_to_date, cint, flt, format_date, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
@@ -129,14 +129,14 @@ class TestSalesInvoice(ERPNextTestSuite):
w2 = frappe.get_doc(w.doctype, w.name)
import time
time.sleep(1)
w.save()
import time
time.sleep(1)
frappe.db.set_value(
w.doctype,
w.name,
"modified",
add_to_date(w.modified, seconds=1),
update_modified=False,
)
self.assertRaises(frappe.TimestampMismatchError, w2.save)
def test_sales_invoice_change_naming_series(self):
@@ -1583,6 +1583,35 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
def test_pos_change_amount_multi_currency_gl_entry(self):
from erpnext.accounts.doctype.sales_invoice.services.gl_composer import SalesInvoiceGLComposer
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 0)
si = create_sales_invoice(do_not_save=True)
si.is_pos = 1
si.currency = "USD"
si.conversion_rate = 50
si.party_account_currency = "USD"
si.account_for_change_amount = "Cash - _TC"
si.change_amount = 50
si.base_change_amount = 2500
si.append(
"payments",
{"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 150, "base_amount": 7500},
)
gl_entries = []
SalesInvoiceGLComposer(si).make_pos_gl_entries(gl_entries)
debtors_entry = next(entry for entry in gl_entries if entry["account"] == si.debit_to)
cash_entry = next(entry for entry in gl_entries if entry["account"] == "Cash - _TC")
self.assertEqual(flt(debtors_entry["credit"]), 5000.0)
self.assertEqual(flt(cash_entry["debit"]), 5000.0)
frappe.db.set_single_value("POS Settings", "post_change_gl_entries", 1)
def test_stock_delivered_but_not_billed_gl_on_invoice(self):
company = "_Test SDBNB Company"
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -3817,25 +3846,12 @@ class TestSalesInvoice(ERPNextTestSuite):
# enable common party accounting
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
# create a dimension and make it mandatory
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
dim = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
}
)
dim.save()
else:
dim = frappe.get_doc(
"Accounting Dimension",
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
)
dim.disabled = False
dim.dimension_defaults = []
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
dim.save()
# make the shared department dimension mandatory
dim = frappe.get_doc("Accounting Dimension", {"document_type": "Department"})
dim.disabled = False
dim.dimension_defaults = []
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
dim.save()
# create a sales invoice
si = create_sales_invoice(
@@ -5789,12 +5805,6 @@ def create_internal_parties():
allowed_to_interact_with="Wind Power LLC",
)
create_internal_customer(
customer_name="_Test Internal Customer 2",
represents_company="_Test Company with perpetual inventory",
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_customer(
customer_name="_Test Internal Customer 3",
represents_company="_Test Company",
@@ -5815,12 +5825,6 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company 1",
)
create_internal_supplier(
supplier_name="_Test Internal Supplier 2",
represents_company="_Test Company with perpetual inventory",
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_supplier(
supplier_name="_Test Internal Customer 3",
represents_company="_Test Company",

View File

@@ -79,7 +79,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2024-03-27 13:10:38.343481",
"modified": "2026-08-21 23:11:48.797423",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
@@ -113,12 +113,33 @@
"role": "Sales Master Manager",
"share": 1,
"write": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -7,10 +7,9 @@ def get_data():
"non_standard_fieldnames": {
"Tax Rule": "sales_tax_template",
"Subscription": "sales_tax_template",
"Restaurant": "default_tax_template",
},
"transactions": [
{"label": _("Transactions"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
{"label": _("References"), "items": ["POS Profile", "Subscription", "Restaurant", "Tax Rule"]},
{"label": _("References"), "items": ["POS Profile", "Subscription", "Tax Rule"]},
],
}

View File

@@ -149,7 +149,7 @@
"icon": "fa fa-truck",
"idx": 1,
"links": [],
"modified": "2026-07-22 14:53:27.315435",
"modified": "2026-08-21 23:11:49.532098",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shipping Rule",
@@ -195,6 +195,38 @@
"role": "Sales Master Manager",
"share": 1,
"write": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import patch
import frappe
from frappe.utils.data import (
@@ -658,23 +659,15 @@ class TestSubscription(ERPNextTestSuite):
sub2 = create_subscription(start_date="2018-01-02")
processed = []
original_process = Subscription.process
original_rollback = frappe.db.rollback
def patched(self, posting_date=None):
processed.append(self.name)
if self.name == sub1.name:
raise frappe.ValidationError("forced failure")
Subscription.process = patched
# process_all calls frappe.db.rollback() on error which would otherwise wipe
# the test transaction; stub it so we can observe the iteration in isolation.
frappe.db.rollback = lambda *a, **kw: None
try:
# Stub transaction recovery so the test can observe the complete iteration in isolation.
with patch.object(Subscription, "process", patched), patch.object(frappe.db, "rollback"):
process_all([sub1.name, sub2.name])
finally:
Subscription.process = original_process
frappe.db.rollback = original_rollback
self.assertEqual(processed, [sub1.name, sub2.name])
@@ -1073,12 +1066,6 @@ def create_plan(**kwargs):
def create_parties():
if not frappe.db.exists("Supplier", "_Test Supplier"):
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = "_Test Supplier"
supplier.supplier_group = "All Supplier Groups"
supplier.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer"

View File

@@ -29,7 +29,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:10:51.976600",
"modified": "2026-08-21 23:11:50.740962",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",
@@ -68,11 +68,68 @@
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Item Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
},
{
"role": "Stock User",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -63,25 +63,6 @@ class TestTaxRule(ERPNextTestSuite):
def test_for_parent_supplier_group(self):
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template",
"title": "_Test Purchase Taxes and Charges Template",
"company": "_Test Company",
"taxes": [
{
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Purchase Taxes and Charges",
"cost_center": "Main - _TC",
"rate": 6,
}
],
}
).insert()
make_tax_rule(
supplier_group="All Supplier Groups",
tax_type="Purchase",

View File

@@ -100,7 +100,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-30 07:13:51.785735",
"modified": "2026-08-21 23:11:50.979676",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
@@ -142,6 +142,26 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Item Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -21,7 +21,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-06-29 05:25:50.243710",
"modified": "2026-08-21 23:11:51.176158",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Group",
@@ -39,6 +39,30 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -140,6 +140,7 @@ def _get_party_details(
if not ignore_permissions:
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
validate_party_company(party_type, party.name, company)
currency = party.get("default_currency") or currency or get_company_currency(company)
@@ -155,7 +156,7 @@ def _get_party_details(
dispatch_address,
ignore_permissions=ignore_permissions,
)
set_contact_details(party_details, party, party_type)
set_contact_details(party_details, party, party_type, doctype)
set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile)
@@ -197,6 +198,17 @@ def _get_party_details(
return party_details
def validate_party_company(party_type, party, company):
if not company or party_type not in ("Customer", "Supplier"):
return
from erpnext.stock.doctype.company_restriction.company_restriction import (
validate_masters_for_company,
)
validate_masters_for_company(party_type, [party], company)
def set_address_details(
party_details,
party,
@@ -346,10 +358,22 @@ def complete_contact_details(party_details):
party_details.update(contact_details)
def set_contact_details(party_details, party, party_type):
def set_contact_details(party_details, party, party_type, doctype=None):
party_details.contact_person = get_default_contact(party_type, party.name)
complete_contact_details(party_details)
# the shipping contact is picked by the user, so it has no default to fall back on;
# blank it instead of carrying the previous party's contact over
if doctype and frappe.get_meta(doctype).has_field("shipping_contact_person"):
party_details.update(
{
"shipping_contact_person": None,
"shipping_contact_display": None,
"shipping_contact_mobile": None,
"shipping_contact_email": None,
}
)
def set_other_values(party_details, party, party_type):
# copy

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