Compare commits

...

1076 Commits

Author SHA1 Message Date
Mihir Kandoi
14fec2c154 test(postgres): probe whether MAX() over text agrees across engines
DO NOT MERGE -- this exists to make CI answer a question.

The parity effort wrapped many descriptive text columns in Max() to satisfy
strict GROUP BY, justified as "Max() returns the value MariaDB picked
arbitrarily". Where the column genuinely varies within its group that does not
hold: Max() over text is a sort, and the engines sort text differently.

MariaDB's utf8mb4 collations fold case but treat punctuation and spaces as
significant. glibc's en_US.UTF-8 -- what the Linux CI Postgres runs -- ignores
punctuation at the primary level, so max('ITEM-C', 'ITEMB') should be 'ITEMB'
on MariaDB and 'ITEM-C' on Postgres.

These assertions encode MariaDB's answers. They pass on macOS (BSD/ICU
collation, which happens to agree). If the Linux Postgres job fails them, the
Max()-over-varying-text sites are a live parity gap and not only a
row-coherence one.
2026-08-02 20:16:14 +05:30
MochaMind
78f9be257b chore: update POT file (#57707) 2026-08-02 14:31:32 +02:00
Shllokkk
61b80050d1 Merge pull request #57703 from Shllokkk/create-payment-entries-from-payable-report
feat: validate selection and improve Create Payment Entries dialog
2026-08-02 17:43:17 +05:30
Shllokkk
bb5b71643b feat: show PE count, grand total and draft note in payment dialog 2026-08-02 17:28:51 +05:30
Mihir Kandoi
0b9dd11115 Merge pull request #57699 from mihir-kandoi/codex/fix-shipping-rule-duplicate-taxes
fix: prevent duplicate shipping charges without cost center
2026-08-02 12:18:17 +05:30
Mihir Kandoi
106ecd7120 chore: remove shipping rule comments 2026-08-02 12:03:14 +05:30
Mihir Kandoi
a4134af30b fix: prevent duplicate shipping charges without cost center 2026-08-02 12:01:35 +05:30
Mihir Kandoi
c3cd4f18f2 Merge pull request #57676 from mihir-kandoi/feat/mr-supplier-selection-dialog
feat: select a supplier per item when creating Purchase Orders from a Material Request
2026-08-02 11:55:49 +05:30
Mihir Kandoi
6477709f7c Merge pull request #57674 from mihir-kandoi/fix/uom-conversion-factor-precision
fix: preserve UOM conversion factor precision in transactions
2026-08-02 11:55:13 +05:30
Shllokkk
7fdb768259 feat: validate selection and create draft payment entries synchronously 2026-08-01 19:40:39 +05:30
Mihir Kandoi
07ac4d83ef feat(job_card): print quantities with their stock uom (#57689)
* feat(job_card): carry the stock uom on the job card

Every quantity the job card reports belongs to the item it produces, but the
document had no unit of its own, so messages could only print bare numbers.

Add the Stock UOM field, set from the finished good or the final product, and
backfill the job cards that already exist.

* fix(job_card): print quantities with their unit

A bare 5 in an error says nothing about what was counted. Every message that
reports a quantity now names its unit, taking it from the job card's stock uom,
from the previous operation's finished good when the message compares two
operations, and from the item itself for a raw material transfer.

The completion dialogs read the same unit off the job card.

* refactor(job_card): move the stock uom next to the qty it measures

* fix(job_card): keep the stock uom backfill atomic

Drop the auto commit toggle so the backfill is a single transaction with no
connection flag left behind when it raises, and select the job cards to fill
with an explicit unset filter instead of a value list.
2026-08-01 18:34:31 +05:30
Mihir Kandoi
0ddf72dae9 refactor(job_card): make the completion dialog say what it asks for (#57688)
* refactor(job_card): drop the unused make_finished_good handler

Nothing triggered it and Job Card has no make_finished_good method to call.

* refactor(job_card): make the completion dialog say what it asks for

The dialog qty shares the Qty to Manufacture label with the field on the form
while it means the current cycle only, its title fell back to the generic Enter
Value because frappe.prompt takes four arguments and it was passed five, and
nothing on it stated that the three quantities have to add up.

Name the cycle in the label, title the dialog after the button that opens it,
and describe the split on the fields. Same wording in the shop floor dialog.
2026-08-01 18:34:31 +05:30
Mihir Kandoi
7bffd84482 fix(job_card): reject a completion split that cannot add up (#57687)
* fix(job_card): reject a completion split that cannot add up

The completion dialogs silently dropped a recalculation whose result went
negative, so entering a pending qty larger than what is left of the qty to
manufacture kept the contradiction (3 to manufacture, 3 completed, 2 pending)
and the job card only failed much later, on submission.

Keep the split consistent while it is entered: reset the pending qty when the
qty to manufacture changes, and refuse a completed, pending or process loss qty
that leaves the others negative. complete_job_card validates the same rule, so
the shop floor and the API cannot store a split that will never submit.

Also name the three parts in the submission error instead of calling their sum
the Total Completed Qty, which read as a contradiction of the field itself.

* test(job_card): cover the completion qty split guard
2026-08-01 18:34:30 +05:30
Mihir Kandoi
970039d8ec fix(job_card): leave the pending qty out of the job card's own output (#57686)
* fix(job_card): leave the pending qty out of the job card's own output

Pending qty is the part of a job card handed over to another job card, but the
status and the manufacturing entry still measured the card against its full
for_quantity. A card submitted with 3 completed and 2 pending was stuck at Work
In Progress with no way to change it, and its manufacturing entry was built for
the full 5.

Measure both against for_quantity minus pending qty, so the card reaches To
Manufacture on submission, its manufacturing entry covers the completed qty, and
it is Completed once that qty is manufactured.

* test(job_card): cover a job card completed with a pending qty
2026-08-01 18:34:30 +05:30
Mihir Kandoi
0e1bc58b2e fix(job_card): apply the completion dialog's qty to manufacture (#57685)
* fix(job_card): apply the completion dialog's qty to manufacture

Both the desk dialog and the shop floor session dialog send for_quantity when
completing a job card, but complete_job_card dropped it. Reducing Qty to
Manufacture to 3 on a job card of 5 left for_quantity at 5, so set_process_loss
turned the untouched 2 into process loss on the next save.

The dialog qty covers the current cycle, so add it to the qty already completed
by the earlier cycles of the job card instead of overwriting for_quantity, and
validate the pending qty against the result.

* test(job_card): cover qty to manufacture from the completion dialog

Reducing the dialog qty resizes the job card without inventing process loss, and
a pending qty split across two cycles leaves for_quantity untouched.
2026-08-01 18:34:30 +05:30
Mihir Kandoi
3bd3354152 fix(job_card): require the previous operation to be manufactured (#57684)
* fix(job_card): block next operation until previous operation is manufactured

With track semi finished goods, Work Order Operation completed_qty is set from
the submitted job cards' total completed qty, so a job card of the next
operation could be started and completed even when no Manufacture entry existed
for the previous operation. The semi-finished goods it consumes were never
produced.

Validate the sequence against the qty actually manufactured against the previous
operations' job cards (Manufacture entries / Subcontracting Receipts) when the
work order tracks semi finished goods.

* test(job_card): cover manufactured qty check across previous operations

Work order with operations A and B at sequence 1 and C at sequence 2, tracking
semi finished goods. C stays blocked while A's job card is submitted but its
Manufacture entry is missing, and once A is manufactured for 3, C can only be
completed for 3.
2026-08-01 18:34:29 +05:30
Mihir Kandoi
1a49e73c85 Merge pull request #57681 from Shllokkk/sre-voucher-qty-use-demand
fix: set reservation voucher_qty to voucher demand not reserved qty
2026-08-01 15:42:12 +05:30
Shllokkk
7a97dc3361 test: partial work order reservation records full voucher_qty 2026-08-01 15:29:20 +05:30
Shllokkk
7995bb9960 fix: set reservation voucher_qty to voucher demand not reserved qty 2026-08-01 15:29:20 +05:30
Mihir Kandoi
6be6bf2929 ci: fall back to develop when frappe has no matching branch (#57693)
* ci: fall back to develop when frappe has no matching branch

The frappe branch to install is taken from the pull request's base branch. A
stacked pull request targets another erpnext branch, so the clone fails with
"couldn't find remote ref", no bench is installed, and every job that needs one
fails with it.

Fall back to develop when the base branch does not exist in frappe. An explicit
FRAPPE_BRANCH is left alone, since it can be a commit sha rather than a branch.

* ci: only fall back when frappe is known to lack the branch

git ls-remote --exit-code reports 2 for a branch that is not there and 128 for a
remote it could not reach. Treating both as absence let a transient network or
DNS failure install develop over the branch the pull request was built against.

Fall back on 2 alone and log anything else, so a flaky probe leaves the branch
as it was.
2026-08-01 09:38:14 +00:00
Diptanil Saha
ceefd4add7 Merge pull request #57201 from diptanilsaha/fix/perms_whitelisted_methods
fix: permission checks on various whitelisted methods
2026-08-01 14:46:34 +05:30
Mihir Kandoi
e694d45ed4 Merge pull request #57679 from Shllokkk/reserved-stock-dashboard-net-transferred
fix: exclude transferred and consumed qty from dashboard reserved stock
2026-08-01 14:33:32 +05:30
diptanilsaha
0659bd7049 fix(payment_request): added permission checks on resend_payment_email 2026-08-01 14:28:29 +05:30
diptanilsaha
3b0cbc972e fix(item_variant): added permission checks on enqueue_multiple_variant_creation 2026-08-01 14:28:29 +05:30
diptanilsaha
09d721d1be fix(assets): add permission checks on whitelisted methods on asset_capitalization 2026-08-01 14:28:24 +05:30
Shllokkk
6c36624d91 fix: exclude transferred and consumed qty from dashboard reserved stock 2026-08-01 13:24:46 +05:30
Mihir Kandoi
2e72846670 fix: label the items table in the supplier selection dialog
The grid template always renders its label line, so leaving the table unlabelled
left an empty line hanging above the description.
2026-08-01 09:23:09 +05:30
Mihir Kandoi
44fdf7bea9 fix: keep the bulk supplier field to half the supplier selection dialog
A lone Link field stretched the full width of the dialog, which reads as a
search bar rather than a field. A column break holds it to half.
2026-08-01 09:21:16 +05:30
Mihir Kandoi
e84bf44e51 feat: set one supplier across every item in the supplier selection dialog
A Material Request where few items carry a default supplier meant picking the
same supplier row by row. A Supplier field above the table copies its value
into every row, leaving the exceptions to be corrected by hand.

Both pickers skip suppliers that are disabled or barred from Purchase Orders by
their scorecard standing.
2026-08-01 09:19:49 +05:30
Mihir Kandoi
f0bb70539d fix: warn about existing draft orders before the supplier selection creates more
Creating through the dialog calls the endpoint directly instead of going
through open_mapped_doc, so the draft link guard that every other Create action
runs never fired, and a repeated dialog quietly produced a second set of draft
orders for the same quantity.
2026-08-01 09:16:13 +05:30
Mihir Kandoi
8ffe5ba420 test: reject the same Material Request item twice in one supplier selection 2026-08-01 09:11:37 +05:30
Mihir Kandoi
99d56cc850 fix: reject the same Material Request item twice in one supplier selection
Each row was checked against the pending quantity on its own, so a payload that
listed one item under two suppliers passed both checks and ordered the pending
quantity twice. The dialog cannot produce that, a direct call to the endpoint
can.
2026-08-01 09:11:37 +05:30
Mihir Kandoi
21c6d10ad3 fix: escape item code and UOM in the supplier dialog errors
Desk renders a client side message as HTML, so an Item or UOM whose name holds
markup ran as markup in the buyer's session.
2026-08-01 09:11:37 +05:30
Mihir Kandoi
3856eaa35e fix: open the Purchase Order when the supplier selection creates only one
Naming a single order in a message and leaving the buyer to click it is a step
for nothing. The form opens directly when there is one order; the message stays
for the case it was meant for, several orders at once.
2026-08-01 09:02:05 +05:30
Mihir Kandoi
d233fdf198 test: reject a supplier selection without items 2026-08-01 09:00:12 +05:30
Mihir Kandoi
07445b3675 feat: order only the items ticked in the supplier selection dialog
Every row is ticked when the dialog opens, so the common case of ordering
everything is unchanged, and a buyer who wants a partial order unticks what
should wait. Creating with nothing ticked is rejected.
2026-08-01 09:00:12 +05:30
Mihir Kandoi
5a78e2290a fix: link the item and spell out the unit in the supplier dialog errors
A bare item code left the buyer to find the item themselves, and a bare number
gave no clue what the limit was counted in. Both messages now link the item and
state the pending quantity in bold with its UOM.
2026-08-01 08:57:04 +05:30
Mihir Kandoi
671c289303 test: alert when Required By falls back to today 2026-08-01 08:52:23 +05:30
Mihir Kandoi
53e09dfdd6 feat: alert when Required By falls back to today
Items whose requested date has passed silently got today as Required By, which
is a date the buyer never asked for. A toast now says so.
2026-08-01 08:52:23 +05:30
Mihir Kandoi
d0cae2eb9c feat: show the UOM alongside the quantity in the supplier selection dialog
The quantity is meaningless without the unit it is counted in, which the buyer
had to look up on the Material Request itself.
2026-08-01 08:52:23 +05:30
Mihir Kandoi
6f22551aae fix: list the Purchase Orders created per supplier instead of opening one
Opening one of several created orders hid the rest and moved the buyer off the
Material Request. The created orders are now reported the way Production Plan
reports its documents, as links in a message, and the form stays put.
2026-08-01 08:39:03 +05:30
Mihir Kandoi
15d10bbaf1 test: Required By on Purchase Orders created per supplier
Backdates the Material Request item so the mapper drops its schedule date, and
asserts the created order still saves with today as Required By.
2026-08-01 08:38:42 +05:30
Mihir Kandoi
d05bd80b1e fix: set Required By on Purchase Orders created per supplier
Mapping drops a schedule date that already passed, leaving the buyer to pick a
new one on the Purchase Order form. Nothing fills it in when the orders are
created straight from the supplier selection dialog, so a Material Request
whose required date has gone by failed to save with "Please enter the Required
By".

Items that lose their date now fall back to today, which is the earliest date a
Purchase Order raised today accepts.
2026-08-01 08:38:26 +05:30
Mihir Kandoi
09cfd1fe91 test: quantity handling in the supplier selection dialog
Asserts the requested quantity reaches the Purchase Order item and that rows
without a supplier, or with a quantity that is zero, negative or beyond the
pending quantity, are rejected.
2026-08-01 08:38:15 +05:30
Mihir Kandoi
da83370c5c feat: adjust the ordered quantity in the supplier selection dialog
The dialog prefilled the pending quantity of each Material Request item but
kept it read only, so ordering less than what was requested meant editing the
Purchase Order afterwards.

The quantity is now editable and is validated against the pending quantity of
its Material Request item, both in the dialog and on the server. The requested
quantity is handed to the mapper as the pending quantity of the source row, so
the existing mapping - including the subcontracting conversions - derives the
Purchase Order quantities from it unchanged.
2026-08-01 08:38:10 +05:30
Mihir Kandoi
65be201ed6 test: supplier selection when creating Purchase Orders from Material Request
Covers the default supplier lookup for pending items, the supplier passed
through to a single mapped order, the grouping of items into one order per
supplier, and the failure when an item is sent without a supplier.
2026-07-31 22:22:55 +05:30
Mihir Kandoi
e8df7b4a90 feat: select a supplier per item when creating Purchase Orders from Material Request
Creating a Purchase Order from a Material Request mapped every pending item
into a single order, leaving the buyer to split it by hand whenever the items
came from different vendors.

The Create action now reads the default supplier of each pending item (item,
item group, then brand defaults). When the items resolve to more than one
distinct supplier - including the case where only some of them have a default -
a dialog lists the items with their default supplier prefilled and editable.
Submitting it groups the items by the chosen supplier and creates one draft
Purchase Order per group.

When every item resolves to the same supplier the order is mapped straight
away with that supplier set, and when none of them has a default supplier the
previous behaviour is unchanged.
2026-07-31 22:22:49 +05:30
Mihir Kandoi
f4d70c2d60 test: fractional conversion factor survives Material Request to Purchase Order
Fails before the fix with 0.45 != 0.453592292 on a site with Float
Precision 2, and 0.454 on the default of 3.
2026-07-31 21:54:05 +05:30
Mihir Kandoi
269cc6ee3b fix: preserve UOM conversion factor precision in transactions
calculate_item_values rounds every Float field on an item row to the
site's Float Precision (3 by default), and conversion_factor was one of
them. The factor is a ratio, not a rate: UOM Conversion Factor.value is
stored at precision 9, and Material Request keeps the full value because
it has no currency field and so never runs the calculation.

Mapping a Material Request to a Purchase Order therefore truncated the
factor - 0.453592292 for Pound -> Kg became 0.454 - and stock_qty, which
is recomputed as qty * conversion_factor, drifted from the quantity that
was requested, leaving the Material Request unable to close.

Exclude conversion_factor from the rounded fields on the server and on
the client. Factors below the site precision would otherwise round to
zero outright.
2026-07-31 21:53:59 +05:30
Shllokkk
ebb5d933ea Merge pull request #57668 from Shllokkk/reserve-stock-accept-dict-doc
fix: allow reserving stock from work order dialog
2026-07-31 21:15:49 +05:30
Diptanil Saha
0a047b410a fix(plant_floor): add missing perm check on get_stock_summary (#57667) 2026-07-31 14:52:17 +00:00
Shllokkk
517053bc25 fix: drop row prefix in reserve stock message when row is unknown 2026-07-31 20:12:15 +05:30
Shllokkk
f29c7de0ef fix: accept dict doc when reserving stock for work order 2026-07-31 20:12:15 +05:30
Diptanil Saha
0fdca37506 fix: use payment entry posting date for received amount exchange rate (#57660) 2026-07-31 17:47:43 +05:30
Sudharsanan Ashok
9a4594ac06 fix: resolve default expense account fallback in gl composer (#57433)
fix: update stock variance account logic which defaults to default expense account set in company

Co-authored-by: Afsal Syed <afsalsyed12@gmail.com>
2026-07-31 14:02:09 +05:30
Mihir Kandoi
a6bdf7905e Merge pull request #57650 from aerele/fix/material-transfer-qty-precision
fix: respect quantity precision in material transfer validation
2026-07-31 12:40:15 +05:30
Sudharsanan11
cf72e03f39 test: cover material transfer quantity precision 2026-07-31 12:09:43 +05:30
Sudharsanan11
1ff8bf7971 fix: respect quantity precision in material transfer validation 2026-07-31 12:09:36 +05:30
Jatin3128
fd7765ac02 fix: filter Accounts Receivable by invoice sales partner (#57628)
Filter Accounts Receivable and AR Summary on the Sales Invoice's own
sales_partner instead of the customer's default_sales_partner, and read
the Sales Partner column from the invoice. Returns are attributed to the
invoice they settle, matching how the Sales Person filter works.
2026-07-31 11:42:17 +05:30
Diptanil Saha
9e659938d7 fix(quotation): carry forward communications from opportunity at after_insert (#57639) 2026-07-31 05:05:26 +00:00
ruthra kumar
caac1468b7 Merge pull request #57434 from aerele/pcv-status-update
fix: update doc status in period closing voucher
2026-07-31 10:34:36 +05:30
Mihir Kandoi
41cc4ffeb6 Merge pull request #57606 from aerele/fix/stock-entry-items-add-scio-guard
fix: guard scio row lookup in stock entry items_add
2026-07-31 08:56:06 +05:30
pandiyan
6e444a1832 fix: guard scio row lookup in stock entry items_add
check the result of find() before reading t_warehouse off it. on a
'receive from customer' entry with no row carrying scio_detail, find()
returns undefined and items_add throws a typeerror.

the throw rejects the serially-run handler chain, so the stock entry
controller's own items_add never runs and the new row silently loses
its target warehouse, expense account, cost center and serial/batch
field defaults.

leave t_warehouse unset when no reference row exists, so the rest of
the chain still runs.
2026-07-31 08:54:33 +05:30
rohitwaghchaure
d59c5e36bc feat: status based bar colors in Work Order gantt view (#57634) 2026-07-30 23:24:59 +05:30
rohitwaghchaure
386a4ac1f0 fix: do not fetch a random inventory account when multiple inventory accounts exist (#57626) 2026-07-30 14:13:45 +00:00
nareshkannasln
b3c2ba5381 fix: validate account frozen date 2026-07-30 17:14:44 +05:30
Jatin3128
7febc28ed6 feat: auto-fill subscription accounting dimensions from plan with item fallback (#57615)
When a plan is selected in the Subscription's Plans table, the Subscription's
accounting dimensions (cost center and any custom dimensions) auto-fill from the
plan, falling back to the plan item's company default (selling cost center for a
Customer, buying for a Supplier). Only empty fields are filled. Stale async
responses are ignored so a quick re-pick of the plan can't be overwritten.
2026-07-30 15:07:15 +05:30
Shllokkk
956105579d Merge pull request #57618 from Shllokkk/asset-manual-create-valuation-rate
fix: source manually created asset value from valuation rate
2026-07-30 14:52:50 +05:30
Shllokkk
46e01c2d92 fix: source manually created asset value from valuation rate 2026-07-30 14:25:41 +05:30
Mihir Kandoi
e65e1d3c96 Merge pull request #57616 from mihir-kandoi/fix/item-group-root-seeding
fix: seed standard Item Groups under the existing tree root
2026-07-30 13:24:52 +05:30
Mihir Kandoi
e7088d8981 fix: seed standard Item Groups under the existing tree root
install_fixtures always inserted "All Item Groups" as a parentless group.
On a site where another app had already created the root, ItemGroup.validate
re-parented it, leaving a second group-root that held the standard groups
while the real root held everything else.

This is reproducible with the healthcare app on a non-English site: its
after_install seeds the root as _("All Item Groups"), so a pt-BR site gets
"Todos os Grupos de Itens" as the root before the setup wizard runs. The
split predates #57390 -- the old translated-name lookup resolved to the same
root and produced an identical tree.

Resolve the root once with get_root_of (falling back to the canonical English
name on fresh installs) and use it for the root record's exists-guard and the
standard groups' parent, matching Company.create_default_departments.

Patch merges an already-seeded "All Item Groups" into the root it sits under,
lifting its children and repointing every link.

Closes #57581
2026-07-30 13:08:28 +05:30
Soham Kulkarni
ec02b5fa64 Merge pull request #57614 from sokumon/item-default
fix: unchecking default workspace
2026-07-30 12:39:16 +05:30
sokumon
85fa6596b8 fix: unchecking default workspace 2026-07-30 12:23:00 +05:30
Mihir Kandoi
0a7c8504e6 Merge pull request #57609 from mihir-kandoi/fix/title-field-parity
fix(projects): read the Timesheet label from the employee field
2026-07-30 08:36:31 +05:30
Mihir Kandoi
38e5674ea4 chore(stock): drop the dead title template on Material Request
`set_title()` runs in validate and always fills `title` first, so
`set_title_field()` never renders `{material_request_type}`, and
create_new.js skips defaults for the field `title_field` names. Titles
stay "<type> Request for <items>".
2026-07-30 08:22:01 +05:30
Mihir Kandoi
03d84430b6 fix(projects): read the Timesheet label from the employee field
Timesheet was the only doctype where the {...} template on `title`
actually rendered: `title_field` was `title`, so `set_title_field()`
seeded it from `{employee_name}` on insert. A `default` renders once, so
reassigning a draft left the stored title — and every label derived from
it — on the previous employee, with no way to correct it from the form
because the field is hidden.

Point `title_field` at `employee_name` so the label reads the live field
instead of a copy that drifts. Existing rows need no backfill.
2026-07-30 08:22:00 +05:30
Mihir Kandoi
f71946def7 Merge pull request #57419 from kaulith/fix/update-items-row-removal-permission
fix: don't require cancel and delete perms to remove items via Update Items
2026-07-29 16:42:26 +05:30
Khushi Rawat
4f1adb8a94 Merge pull request #57520 from khushi8112/feature/default-modern-print-formats
feat: default new sites to the Modern with Images print formats
2026-07-29 15:47:08 +05:30
Jatin3128
cfe18e8427 fix: let Purchase Receipt cancel defer to Frappe's linked-document check (#57592)
on_cancel pre-blocked cancellation with its own "Purchase Invoice is
already submitted" guard, duplicating the check Frappe already runs for any
submitted linked document. Drop the guard and the unused check_next_docstatus()
method it mirrored so the receipt defers to the framework: the Cancel All
Documents flow cancels the invoice first and then the receipt, and a direct
cancel is still rejected by Frappe's linked-document check.

Add a regression test that a direct cancel of a receipt with a submitted
invoice is rejected and rolls back, leaving no stray stock or GL entries.
2026-07-29 14:44:32 +05:30
Jatin3128
6b8b9d3644 test: isolate accounts settings mutation in overdue threshold test (#57441)
* test: isolate accounts settings mutation in overdue threshold test

test_overdue_billing_threshold_on_submit mutated the Accounts Settings singleton
without restoring it, so a failed assertion mid-test leaked
enable_overdue_billing_threshold and the bypass role into later tests that submit
sales invoices. Wrap the mutations in try/finally and restore the originals. Also
assert that a 0 overdue limit on the customer inherits the customer group's limit.

* test: restore credit limits in overdue threshold fallback test
2026-07-29 12:38:33 +05:30
Nabin Hait
b86ad21444 Merge pull request #57314 from krishna-254/fix/skip-italy-einvoice-opening-invoices
fix(italy): skip e-invoicing for opening invoices
2026-07-29 11:08:59 +05:30
Diptanil Saha
372dff2ffa refactor(accounts): repost accounting ledger (#56442)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 22:51:06 +00:00
Vishnu Priya Baskaran
5125d64b7f fix: clear deferred revenue/expense fields on uncheck (#57140) 2026-07-29 04:18:45 +05:30
Krishna Pramod Shirsath
e0c31f1745 fix: recover failed POS closings (#57203)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-28 22:29:47 +05:30
Mihir Kandoi
6d748c5523 Merge pull request #57571 from mihir-kandoi/move-warehouse-defaults-to-company
refactor: move warehouse defaults from Stock Settings to Company
2026-07-28 20:28:55 +05:30
Mihir Kandoi
2095073688 fix: check company permission before reading sample retention stock
validate_sample_quantity and move_sample_to_retention_warehouse are
whitelisted and take company from the caller, which selects whose retention
warehouse gets read. The retained batch qty then reaches the return value and
the max-retained warning, so an authenticated user could probe another
company's stock with a known item and batch.

Gate the shared company -> warehouse resolution on read permission for the
Company, which respects User Permissions. validate_sample_quantity only grew
a company argument in this branch; move_sample_to_retention_warehouse already
took one, so this closes that path too.
2026-07-28 20:15:19 +05:30
Mihir Kandoi
ac477bb33c test: stop relying on companies having no default warehouse
Companies now get their Stores warehouse as Default Warehouse, so item
warehouse resolution succeeds where it previously came back empty.

test_internal_pr_reference cleared inter_company_reference and asserted a
ValidationError, but no validation covers that field - the mapper already
sets it. It was incidentally catching 'Row #1: Warehouse is mandatory for
stock Item' from the blank target warehouse, so the assertion never tested
what it claimed. Dropped it; the delivery_note_item assertion below still
covers the reference linkage.

test_inter_company_transaction_without_default_warehouse now establishes its
own premise by clearing the company's default warehouse instead of relying
on it being unset. Its failure previously skipped the teardown that restores
frappe.local.enable_perpetual_inventory, which db rollback cannot undo, which
in turn broke two later inter-company tests.
2026-07-28 20:06:47 +05:30
Mihir Kandoi
455251abe1 fix: throw when the transaction company has no sample retention warehouse
Item.retain_sample is validated against any company having one configured,
since Item is not company-scoped. The transaction company may still not be,
in which case get_batch_qty received warehouse=None and returned its batch
list, which was then compared numerically -> TypeError.

Resolve company -> retention warehouse through one helper that throws a
clear message instead, covering both the sample stock entry and the
whitelisted quantity validation.
2026-07-28 19:41:29 +05:30
Mihir Kandoi
26613d258e refactor: move warehouse defaults from Stock Settings to Company
Default Warehouse and Sample Retention Warehouse were global singles, so every
consumer had to re-check that the warehouse belonged to the transaction's
company before using it. Both now live on Company, under a new Warehouse
Defaults section that also collects the warehouse fields Company already had.

The company check moves to Company.validate_warehouses, which also rejects
group warehouses for all seven fields — a group or cross-company value there
already failed at SLE time, this just surfaces it at the source.

New companies get their Stores warehouse as Default Warehouse via
create_default_warehouses, replacing the setup-wizard and test-fixture code
that seeded the global.
2026-07-28 19:29:18 +05:30
Mihir Kandoi
c80a848216 Merge pull request #57567 from aerele/fix/stock-balance-for-none-row
fix: guard against None row in get_stock_balance_for
2026-07-28 19:26:26 +05:30
Shllokkk
3f0b894cdf Merge pull request #57566 from Shllokkk/item-def-accounting-desc
fix(item): correct description on deferred revenue/expense
2026-07-28 19:22:27 +05:30
Shllokkk
fa75aa08ab fix(item): correct description on deferred revenue/expense 2026-07-28 19:00:24 +05:30
R-Jayaraman
f9d25bc3d3 fix: guard against None row in get_stock_balance_for
row defaults to None but was dereferenced unconditionally when computing
the incoming rate for batch-tracked items, causing an AttributeError for
any caller (e.g. direct API calls) that omits row while still passing
batch_no. The other two row accesses in this function already guard
against None; this brings the incoming-rate check in line with them.
2026-07-28 18:57:06 +05:30
Smit Vora
986cea2331 feat: taxable-base resolver hook for custom charge types (#56175) 2026-07-28 17:21:55 +05:30
Mihir Kandoi
0cd95e1995 fix(manufacturing): fall back to UOM Conversion Factor in Production Plan (#57553)
Production Plan read the conversion factor straight off the item's own
UOM child table, so an item with a purchase UOM but no matching row threw
"UOM Conversion factor not found" while Stock Entry silently resolved it
from the item's variant template or the UOM Conversion Factor doctype.
Resolve it the same way, and keep returning None when nothing is
configured anywhere so the missing-setup error still fires.
2026-07-28 11:41:51 +00:00
Shllokkk
5fc20d6b8e fix: respect child warehouse account override in Stock and Account Value Comparison (#57552)
fix: respect child warehouse account override in stock vs account value comparison
2026-07-28 16:51:53 +05:30
Diptanil Saha
5835709402 fix: add permission check for get_item_details (#57515) 2026-07-28 10:38:40 +00:00
Mihir Kandoi
a051a12d9b Merge pull request #57540 from mihir-kandoi/fix-company-mfg-warehouse-filters
fix(setup): scope manufacturing warehouse filters to company
2026-07-28 14:16:44 +05:30
Mihir Kandoi
632113c309 fix(setup): scope manufacturing warehouse filters to company
Default WIP, Finished Goods and Scrap Warehouse fields on Company listed
warehouses of every company. Filter them by the current company and
exclude group warehouses, matching the other warehouse fields.
2026-07-28 14:10:50 +05:30
Mihir Kandoi
91e4a753b3 Merge pull request #57535 from frappe/fix/silently-swallowed-exceptions
fix: stop swallowing exceptions silently in six places
2026-07-28 13:06:46 +05:30
rohitwaghchaure
4a0a1db17e fix: skip stock expense GL entries for non-stock items (#57519)
* fix: skip stock expense gl entries for non stock items

* test: use a leaf expense account for the service item invoice
2026-07-28 13:00:32 +05:30
Mihir Kandoi
85a04772f6 fix: stop swallowing exceptions silently in six places
- Pricing Rule: log a broken condition instead of dropping the rule
- Payment Request: log gateway validation failures
- Supplier Quotation from RFQ portal: let errors surface instead of
  returning None to a dead submit button
- Bank Transaction Rule: drop the pointless try/except around the
  on_trash unlink; a failed write already aborts the delete
- Statement of Accounts: the CC handler was unreachable
  (frappe.get_value returns None, it does not raise); drop it and
  filter out users without an email
- Bank Statement Import: 'Bank Account' at column 0 is falsy, so the
  column was appended instead of filled
2026-07-28 12:49:08 +05:30
Mihir Kandoi
2f07dfc474 Merge pull request #57532 from mihir-kandoi/fix-update-cost-bom-creator-boms
fix(manufacturing): update cost of BOMs created via BOM Creator
2026-07-28 12:46:47 +05:30
Mihir Kandoi
d269c90838 fix(manufacturing): update cost of BOMs created via BOM Creator
`calculate_rm_cost` skipped rate refresh whenever `bom_creator` was set,
so neither the Update Cost button nor the BOM Update Tool could ever
refresh those BOMs. Every BOM in a multi-level tree carries the field, so
whole trees stayed frozen at their creation rates.

The guard replaced the removed `rm_cost_as_per == "Manual"` check in
0b63dbf, on the assumption that BOM Creator rows hold manual rates. They
do not: BOM Creator recomputes every row from `rm_cost_as_per` on save.
2026-07-28 12:33:12 +05:30
Mihir Kandoi
7d44bdbdc6 Merge pull request #57522 from mihir-kandoi/fix-subcontracting-receipt-title
fix(subcontracting): stop the Subcontracting Receipt title from going stale
2026-07-28 12:19:21 +05:30
Mihir Kandoi
8d7ac9f51d Merge pull request #57528 from mihir-kandoi/fix-bom-creator-dup-subassembly
fix(manufacturing): scope BOM Creator tree children to the parent row
2026-07-28 12:18:46 +05:30
Mihir Kandoi
15b7626eed Merge pull request #57521 from mihir-kandoi/fix-semi-fg-produced-qty
fix(manufacturing): sum semi-FG qty across split job cards
2026-07-28 12:14:09 +05:30
Mihir Kandoi
b37152752f fix(manufacturing): scope BOM Creator tree children to the parent row
The BOM Creator tree identified a node by the parent's item code
(fg_item) instead of the specific BOM Creator Item row, so every
occurrence of a repeated sub-assembly shared one child set: expanding
any one of them listed the raw materials of all of them, and deleting
one wiped the raw materials of its siblings.

Key the tree on fg_reference_id and make the node value the row name,
matching the framework convention that a tree node's value is its
docname. Item code now travels as its own field for the label and for
the fg_item argument sent back on add/convert.

Fixes #57311
2026-07-28 11:25:29 +05:30
Mihir Kandoi
bde118e7cf fix(manufacturing): exclude corrective job cards from semi-FG aggregate 2026-07-28 11:09:36 +05:30
Khushi Rawat
56a9ca334b Merge pull request #57451 from khushi8112/feature/request-for-quotation-print-formats
feat: add four Request for Quotation print formats built with the print format builder
2026-07-28 11:06:33 +05:30
Khushi Rawat
aacccfb958 Merge pull request #57450 from khushi8112/feature/quotation-print-formats
feat: add four Quotation print formats built with the print format builder
2026-07-28 11:06:20 +05:30
Khushi Rawat
bd5d6fb9d8 Merge pull request #57449 from khushi8112/feature/pos-invoice-print-formats
feat: add four POS Invoice print formats built with the print format builder
2026-07-28 11:06:06 +05:30
Khushi Rawat
22334def56 Merge pull request #57447 from khushi8112/feature/purchase-invoice-print-formats
feat: add four Purchase Invoice print formats built with the print format builder
2026-07-28 11:05:51 +05:30
Khushi Rawat
5631acc39b Merge pull request #57446 from khushi8112/feature/purchase-order-print-formats
feat: add four Purchase Order print formats built with the print format builder
2026-07-28 11:05:38 +05:30
Khushi Rawat
dd0f763613 Merge pull request #57445 from khushi8112/feature/delivery-note-print-formats
feat: add four Delivery Note print formats built with the print format builder
2026-07-28 11:05:25 +05:30
Khushi Rawat
b71a9b8273 Merge pull request #57437 from khushi8112/feature/sales-order-print-formats
feat: add four Sales Order print formats built with the print format builder
2026-07-28 11:05:12 +05:30
Khushi Rawat
7a76c5f268 Merge pull request #57430 from khushi8112/feature/sales-invoice-print-formats
feat: add four Sales Invoice print formats
2026-07-28 11:04:59 +05:30
Mihir Kandoi
5548f0726a fix(manufacturing): sum semi-FG qty across split job cards
update_semi_finished_good_details assigned the current job card's
manufactured_qty to Work Order.produced_qty instead of accumulating it,
so a second job card on the same operation overwrote the first. Nothing
corrected it afterwards because StatusService.update_work_order_qty
returns early for track_semi_finished_goods work orders, leaving the
work order stuck below its planned qty with no way to progress.

Aggregate manufactured_qty and completed_qty over the operation's
submitted job cards instead.
2026-07-28 11:04:17 +05:30
Mihir Kandoi
543301701e fix(subcontracting): stop storing "{supplier_name}" as the Subcontracting Receipt title
Subcontracting Receipt was left out of #57493. Its title_field is "title",
so the template does get rendered on insert, but never again — the title
goes stale as soon as the supplier changes.

Point title_field at supplier_name like Purchase Receipt, and give the
title field the same shape as its subcontracting siblings. Existing rows
already hold a rendered name, so no data patch is needed.

Also guard the whole class of bug: a "{...}" default on a title field is
only ever rendered when title_field is "title".
2026-07-28 11:00:57 +05:30
khushi8112
3eb924d052 feat: default new sites to the Modern with Images print formats
Point set_default_print_formats() at the new builder-made
"<Doctype> Modern with Images" formats, falling back to the existing
"with Item Image" formats when the new ones aren't present. Only fresh
sites are affected (the existing default-print-format guard is kept).
2026-07-28 11:00:31 +05:30
Shllokkk
14fd045555 Merge pull request #57471 from Shllokkk/routing-hour-rate-operating-cost
fix: update operating cost when propagating workstation hour rate to routing
2026-07-28 10:50:06 +05:30
Mihir Kandoi
43acbae5a1 Merge pull request #57493 from aerele/fix-subcontracting-title-template
fix: stop storing raw title template on subcontracting orders
2026-07-28 10:41:59 +05:30
Shllokkk
a5250d8e80 Merge branch 'develop' into routing-hour-rate-operating-cost 2026-07-28 01:33:50 +05:30
Sudharsanan Ashok
73224d3650 fix(stock): keep manufactured item rate at zero when inputs are free (#57334)
* fix(stock): keep manufactured item rate at zero when inputs are free

when a finished item is produced from raw materials consumed at zero
valuation, the incoming rate fell back to the item's own valuation
rate (or BOM cost), valuing free inputs as output and inflating the fg
value on every production run.

add has_consumption_basis() to detect when the consumed cost is known
even if it is zero (consumed rows present, or a consumption entry
exists for the work order). when it is, skip the get_valuation_rate and
BOM-cost fallbacks so a real cost of zero is preserved.

* test(stock): cover manufacture rate for zero-valued raw materials

- manufacture from a free input keeps fg basic_rate and sle
  incoming_rate/stock_value_difference at zero even when the fg already
  carries a valuation in the target warehouse
- material consumption on with no consumption entry does not fall back
  to bom/price-list rate for free inputs
- zero-valued consumption entry keeps the manufacture entry's fg rate
  at zero
2026-07-27 23:35:00 +05:30
Sudharsanan Ashok
d37e905322 fix(stock): value batched packed-item returns from the original bundle (#57327)
* fix(stock): value batched packed-item returns from the original bundle

when a return delivery note or sales invoice bundle is built via the
use_serial_batch_fields / sle-driven path, its voucher_detail_no keeps the
packed item instead of being remapped to the parent dn/si item. the return
valuation lookup then misses and the bundle values at zero, so the sle
stock_value_difference stays wrong even after a repost.

resolve the original dn/si item via the packed item's parent_detail_docname
when the direct lookup fails, so the return values from the original outward
bundle on both submit and repost.

* test(stock): cover batched packed-item return valuation on repost
2026-07-27 23:33:41 +05:30
Diptanil Saha
28c96999e2 fix(quotation): carry forward communications from opportunity (#57507) 2026-07-27 23:29:57 +05:30
Sudharsanan Ashok
425191e57e fix(stock): narrow legacy serial ledger lookup by item (#57499)
Filter legacy Stock Ledger Entry lookups by item code so the existing
item and warehouse index can reduce rows scanned during serial valuation.
2026-07-27 21:40:44 +05:30
ruthra kumar
05a15dff71 Merge pull request #57500 from frappe/mergify/bp/develop/pr-57484
fix(test): flaky test in exchange rate revaluation (backport #57484)
2026-07-27 17:21:41 +05:30
ruthra kumar
4e77c9d6e6 fix(test): flaky test in exchange rate revaluation
- remove redundant setup on system settings

(cherry picked from commit 484ff8e349)
2026-07-27 11:40:37 +00:00
Raffael Meyer
bdf586c670 fix: error message wording (#57495) 2026-07-27 11:15:18 +00:00
Mihir Kandoi
3f93887ec8 Merge pull request #57492 from frappe/production-plan-recalculate-bin
fix: recalculate whole bin in Production Plan reservation patch
2026-07-27 16:01:04 +05:30
Mihir Kandoi
88b02130e7 fix: recalculate whole bin for Production Plan raw material items
projected_qty is derived from every bin quantity, so refreshing only
reserved_qty_for_production_plan leaves it stale wherever another field
had drifted. Call Bin.recalculate_values() instead.

Renamed so the patch re-runs on sites that already applied
recompute_production_plan_reserved_qty.
2026-07-27 15:26:25 +05:30
Raffael Meyer
8b37c52187 fix(crm): align Opportunity status checks with Quotation statuses (#57489) 2026-07-27 09:20:22 +00:00
Mihir Kandoi
70fa8c0c2a Merge pull request #57485 from frappe/stock-ageing-batch-slot-pooling
fix: pool batch slot values on every run, not only when negative
2026-07-27 13:53:23 +05:30
Mihir Kandoi
545262c5d4 test: assert batch pooling preserves the group total on a repeating rate 2026-07-27 13:42:25 +05:30
Mihir Kandoi
cedaaa3a00 fix: pool batch slot values on every run, not only when negative
A batch is one valuation pool, so any per-slot value difference within a
batch is stale detail from the report's own age slots, not real valuation.
The rebalance only ran when consumption had already driven a slot negative,
so a batch whose receipts landed at different rates kept a skewed split
across age buckets (one bucket free, another double-priced) while the total
stayed correct.

Drop the negative-slot precondition and always spread a batch's pooled value
over its slots in proportion to qty. Redistribution preserves group totals,
so buckets still sum to Stock Balance; only the split across ages changes.
2026-07-27 13:37:08 +05:30
pandiyan
5008e6126f fix: stop storing raw title template on subcontracting orders
subcontracting order and subcontracting inward order carry a hidden
title field defaulting to "{supplier_name}" / "{customer_name}", while
their title_field points at supplier_name / customer_name. document.
set_title_field() substitutes the template only when title_field is
"title", so every record stores the placeholder verbatim.

drop the dead default and hidden flags, move title into the other info
tab to match purchase order, and add a patch to repair existing rows.
2026-07-27 13:18:31 +05:30
Mihir Kandoi
273e9f2431 Merge pull request #57463 from aerele/fix/sco-closed-reserved-qty
fix(subcontracting): release raw-material reservation when closing a subcontracting order
2026-07-27 12:08:37 +05:30
ruthra kumar
d3448ef9a2 Merge pull request #57476 from ruthra-kumar/date_configurable_err_reversal
refactor: configurable date in reverse ERR journals
2026-07-27 11:40:32 +05:30
ruthra kumar
1a558ce641 refactor(test): manually submit reverse err journal 2026-07-27 11:18:41 +05:30
Sudharsanan11
e4b8065a69 test(subcontracting): cover reservation release on closing a subcontracting order
close a partially-received sco with a reserve warehouse and assert the
raw-material reservation is released and projected qty recovers.
2026-07-27 11:13:17 +05:30
Sudharsanan11
db91a79d31 fix(subcontracting): release raw-material reservation when closing a subcontracting order
the bin reserved-qty recalc filtered out closed purchase orders but not
closed subcontracting orders, so closing a partially-received sco kept the
reservation for the unreceived qty and left projected qty understated.
apply the same closed-status filter to the subcontracting order path.
2026-07-27 11:13:17 +05:30
ruthra kumar
0be33e4132 refactor: configurable date in reverse ERR journals 2026-07-27 11:11:38 +05:30
rohitwaghchaure
f077d2edc0 fix: GL entries for purchase expense with LCV (#57475) 2026-07-26 23:08:35 +05:30
MochaMind
fd37bc3ff8 chore: update POT file (#57469) 2026-07-26 16:10:52 +02:00
Shllokkk
371ab1db61 Merge pull request #57443 from Shllokkk/rename-ar-ap-report-filters
fix: rename misleading filter labels in AR/AP reports
2026-07-26 17:16:42 +05:30
Shllokkk
be27a918e6 Merge pull request #57320 from Shllokkk/create-payment-entries-from-payable-report
fix: show create payment entries as an inner button on row selection
2026-07-26 17:14:35 +05:30
Shllokkk
e08e119739 test: assert operating cost is propagated to routing operations 2026-07-26 16:15:53 +05:30
Shllokkk
eb9afa40ea fix: update operating cost when propagating workstation hour rate to routing 2026-07-26 16:03:09 +05:30
Shllokkk
c6a16495c0 Merge pull request #57466 from Shllokkk/routing-hour-rate-operating-cost
fix: recalculate operating cost on hour rate change in routing
2026-07-26 14:06:28 +05:30
Shllokkk
598f6f0f4e fix: recalculate operating cost on hour rate change in routing 2026-07-26 13:11:11 +05:30
mergify[bot]
8c24c5bd68 fix: enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report (backport #57458) (#57459)
fix: enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report (#57458)

(cherry picked from commit 4e8f5de5cb)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-07-25 08:27:19 +05:30
Nabin Hait
208a07e19e Merge pull request #57452 from nabinhait/fix-child-qty-rate-type-hint
fix: accept list payload for trans_items in update_child_qty_rate
2026-07-24 21:54:18 +05:30
Shllokkk
f13cd00494 fix: migrate stored AR/AP ageing filter to renamed field 2026-07-24 18:45:07 +05:30
Nabin Hait
d73ff0a2bf fix: accept list payload for trans_items in update_child_qty_rate
The whitelisted endpoint declared trans_items as str, so Frappe's typing
validation raised FrappeTypeError when the client sent the items as a
JSON list. ChildItemUpdater.update already handles both via
frappe.parse_json, so widen the wrapper's hint to str | list.
2026-07-24 17:33:58 +05:30
khushi8112
2d731b3232 fix: remove duplicate supplier name in Bordered format
The right-column Custom HTML block re-displayed the vendor name a
second time. In the source Purchase Order design that block was part
of an address card (name + mailing address together); Request for
Quotation has no per-supplier address field, so after dropping the
address the block became a bare, purposeless repeat of the name
already shown at the top of the left column. Classic, Modern, and
Modern with Images each show it once - Bordered now matches.

Addresses the Greptile review comment on this PR.
2026-07-24 17:26:39 +05:30
khushi8112
fe11252a4d feat: add four Request for Quotation print formats built with the print format builder
Request for Quotation has no pricing fields at all (no rate, amount,
grand_total, in_words), so this is a structurally simplified version
of the Bordered/Classic/Modern/Modern with Images designs rather than
a straight field-swap: Sub Total/Discount/Tax/Grand Total/In Words
sections dropped entirely, item table shows Item/Code/Quantity only.
Supplier fields point at the doctype's own `vendor` field (matching
the existing request_for_quotation_with_item_image standard format's
convention), items table bound to Request for Quotation Item, status
field carries the real RFQ status list, no letter head embedded.
2026-07-24 17:20:28 +05:30
khushi8112
8c3e773f89 feat: add four Quotation print formats built with the print format builder
Adapted from the Sales Order formats: customer_name relabeled to
"Party Name" (Quotation supports both Customer and Lead via
quotation_to), delivery_date mapped to valid_till, items table bound
to Quotation Item, status field carries the real Quotation status
list, no letter head embedded.
2026-07-24 17:09:11 +05:30
khushi8112
f9fe407d42 fix: correct supplier_name/in_words field type across all four formats
Both are Data on the Purchase Invoice doctype, not Small Text
(carried over from the Sales Invoice formats these were adapted from,
where Small Text is correct - Sales Invoice's own doctype defines it
that way). No rendering impact: Data.html and Field.vue never branch
on Small Text vs Data. Addresses the Greptile review comment on
purchase_invoice_bordered.json, and applies the same fix to the other
three formats that had the identical issue.
2026-07-24 16:57:38 +05:30
khushi8112
dc5f445bae fix: correct supplier_name/in_words field type on Purchase Order formats
Both are Data on the Purchase Order doctype; carried over as Small
Text from the Sales Order formats these were adapted from. No
rendering impact - caught via a proactive fieldtype audit after a
Greptile comment on a downstream PR flagged the same pattern on
Purchase Invoice.
2026-07-24 16:57:19 +05:30
khushi8112
8ef7e11332 fix: correct customer_name/in_words field type on Delivery Note formats
Both are Data on the Delivery Note doctype; carried over as Small Text
from the Sales Order formats these were adapted from. No rendering
impact - caught via a proactive fieldtype audit after a Greptile
comment on a downstream PR flagged the same pattern on Purchase
Invoice.
2026-07-24 16:56:39 +05:30
khushi8112
f81a70baff fix: correct customer_name/in_words field type on Sales Order formats
Both are Data on the Sales Order doctype; they were carried over as
Small Text from the Sales Invoice formats they were adapted from,
where that type is correct (Sales Invoice defines them as Small Text).
No rendering impact — caught via a proactive fieldtype audit after a
Greptile comment on a downstream PR flagged the same pattern on
Purchase Invoice.
2026-07-24 16:55:23 +05:30
khushi8112
b025a5f8c2 feat: add four POS Invoice print formats built with the print format builder
Adapted from the Sales Invoice formats: nearly identical field shape
(customer_name, address_display, company_address_display, posting_date,
due_date, total, in_words all exist natively on POS Invoice), so only
the items table (bound to POS Invoice Item), status field options, and
title text needed changing. Each format correctly relabels as "Credit
Note" instead of "POS Invoice" when is_return is set. Field types for
customer_name/in_words corrected to Data to match POS Invoice's own
doctype definition (Sales Invoice defines these as Small Text; POS
Invoice does not). No letter head embedded.
2026-07-24 16:54:16 +05:30
khushi8112
a6829f64da feat: add four Purchase Invoice print formats built with the print format builder
Adapted from the Sales Invoice formats for the buying side: customer
fields swapped for supplier fields, items table bound to Purchase
Invoice Item, status field carries the real Purchase Invoice status
list. Each format correctly relabels as "Debit Note" instead of
"Purchase Invoice" when is_return is set, mirroring the Credit Note
labeling on the Sales Invoice formats. No letter head embedded.
2026-07-24 16:44:40 +05:30
khushi8112
273eea8034 feat: add four Purchase Order print formats built with the print format builder
Adapted from the Sales Order formats for the buying side: customer
fields swapped for supplier fields, delivery_date mapped to
schedule_date (Required By), items table bound to Purchase Order
Item, status field carries the real Purchase Order status list, no
letter head embedded.
2026-07-24 16:33:21 +05:30
khushi8112
0dbedbfb25 feat: add four Delivery Note print formats built with the print format builder
Adapted from the Sales Order formats: transaction_date/delivery_date
mapped to Delivery Note's own posting_date and po_no (customer's PO
number), items table bound to Delivery Note Item, status field carries
the real Delivery Note status list, no letter head embedded.
2026-07-24 16:18:35 +05:30
khushi8112
ef50991d18 fix: correct Sales Order print format bindings and tidy the Modern footer
- items table options: Sales Invoice Item -> Sales Order Item (all four formats)
- status field options: replace the Sales Invoice status list with the actual
  Sales Order statuses (Modern, Modern with Images)
- drop the redundant document-number field from the Modern footer; the header
  already shows it
2026-07-24 15:38:21 +05:30
Shllokkk
e99425b7c4 fix: rename misleading filter labels in AR/AP reports 2026-07-24 15:32:51 +05:30
Mihir Kandoi
3e15fe0dfd Merge pull request #57435 from aerele/fix-party-dashboard-user-permission
fix(accounts): respect user permissions in party dashboard company list
2026-07-24 15:00:00 +05:30
khushi8112
df6c98a187 feat: add four Sales Order print formats built with the print format builder
Adapted from the new Sales Invoice Bordered/Classic/Modern/Modern with
Images formats: invoice-specific fields (posting_date, due_date, the
Sales Invoice/Credit Note title) mapped to their Sales Order
equivalents, no letter head embedded.
2026-07-24 13:08:05 +05:30
Deepesh Garg
a04725eb04 Merge pull request #57296 from deepeshgarg007/user_change
fix: Ignore permission while deleting user permission
2026-07-24 12:29:58 +05:30
pandiyan
903c87bcaa fix: respect user permissions in party dashboard company list
use frappe.get_list instead of frappe.get_all in get_dashboard_info so
the company list honors user permissions. previously, a party with
invoices across multiple companies would raise "User don't have
permissions to select/read this account" for users restricted to a
subset of companies, since get_party_account was called for companies
the user could not access.

fixes frappe/erpnext#57428
2026-07-24 12:22:08 +05:30
Deepesh Garg
314796633f Merge branch 'develop' into user_change 2026-07-24 12:14:34 +05:30
khushi8112
15dee35adc fix: address review feedback on new Sales Invoice print formats
- drop dev-site-specific letter_head references so a fresh install picks its own default
- add missing row_condition on Modern with Images' tax repeater
- standardize Sub Total on total (matches summed item amounts) and In Words on the transaction-currency field across all four formats
2026-07-24 11:37:18 +05:30
khushi8112
4c83b7c5f1 feat: add four Sales Invoice print formats built with the print format builder 2026-07-24 10:52:40 +05:30
Khushi Rawat
5bf1c506a5 Merge pull request #57382 from khushi8112/fix/mt940-per-transaction-reference
fix: map MT940 per-transaction reference from :61: customer_reference
2026-07-24 10:36:35 +05:30
Kaushal Shriwas
87a403ae01 test(selling): deactivate leaked sales order workflow before item removal test 2026-07-23 21:19:52 +05:30
Kaushal Shriwas
5b1c4d22e0 test(selling): cover item removal without cancel and delete perms 2026-07-23 20:42:56 +05:30
Kaushal Shriwas
86ba4395fb fix(selling): don't require cancel and delete perms to remove items via Update Items 2026-07-23 20:42:45 +05:30
Mihir Kandoi
ba593f918a Merge pull request #57335 from aerele/fix/report-date-validation-dry
refactor: reuse shared date range validation across reports
2026-07-23 19:54:34 +05:30
Nabin Hait
50e361e10d Merge pull request #57263 from nabinhait/proforma-invoice
feat(selling): Proforma Invoice against Sales Order
2026-07-23 19:48:44 +05:30
Mihir Kandoi
e6ddb91ce5 Merge pull request #57410 from mihir-kandoi/revert-default-warehouse-names
revert: canonical default warehouse names (#57392, #57409)
2026-07-23 19:41:05 +05:30
Nabin Hait
0b71c943c1 fix(selling): don't email cancelled proformas or copy their PDF
Address review findings:
- send_proforma_email rejects non-issued proformas, and the tab suppresses the
  action for cancelled rows, so a voided document can't be sent to a customer
- mark proforma_pdf as no_copy and disable amendment (a proforma is created
  only from a Sales Order), so a copied proforma can't carry the original's PDF
  and number
2026-07-23 19:17:21 +05:30
Nabin Hait
e4fb5ed3c4 fix(selling): guard proforma against unsubmitted SO and missing PDF
Address review findings:
- make_proforma_invoice: reject a non-submitted Sales Order (the whitelisted
  endpoint was previously only JS-gated on docstatus)
- send_proforma_email: throw a clear error when the attached PDF File is missing
  instead of passing a null fid to sendmail
2026-07-23 18:59:08 +05:30
ervishnucs
170803b041 refactor: reuse shared date range validation across reports 2026-07-23 18:31:50 +05:30
Mihir Kandoi
e4561b1431 Merge pull request #57412 from aerele/customer-pick-list
fix: map pick list customer to delivery note when no sales order
2026-07-23 18:24:19 +05:30
Mihir Kandoi
bf309f65d4 Merge pull request #57413 from aerele/fix-batch-oldest-expiry-typeerror
fix: typeerror in get_batches_by_oldest for mixed batch expiry
2026-07-23 18:23:10 +05:30
pandiyan
62c9f8ee3e fix: typeerror in get_batches_by_oldest for mixed batch expiry
sort on (expiry is none, expiry) so a null expiry_date is never
order-compared against a datetime.date, which raised typeerror in
python 3 when a warehouse held both dated and never-expiring batches.
2026-07-23 17:48:22 +05:30
R-Jayaraman
aff10d3a37 fix: map pick list customer to delivery note when no sales order 2026-07-23 17:41:03 +05:30
Mihir Kandoi
63f6f1808f Revert "Merge pull request #57392 from mihir-kandoi/fix/canonical-default-warehouse-names"
This reverts commit 5fa242ec93, reversing
changes made to a81524ea14.
2026-07-23 17:09:53 +05:30
Mihir Kandoi
a8e74df6f7 Revert "Merge pull request #57409 from mihir-kandoi/chore/drop-warehouse-name-translation-marking"
This reverts commit ab16fd0a98, reversing
changes made to 5fa242ec93.
2026-07-23 17:09:53 +05:30
Mihir Kandoi
ab16fd0a98 Merge pull request #57409 from mihir-kandoi/chore/drop-warehouse-name-translation-marking
refactor: drop translation marking from default warehouse names
2026-07-23 16:56:50 +05:30
Mihir Kandoi
2ea63ce709 refactor: drop translation marking from default warehouse names
warehouse_name is a Data field shown as stored, and everywhere else
users see the docname, which carries the company abbr ("Stores - F")
and is never translated - a translated bare "Stores" has no display
use, so the identity _ marking from #57392 serves nothing. The
exists-guard keeps the runtime session-translation match that protects
legacy sites from duplicate English warehouses.
2026-07-23 16:45:57 +05:30
Mihir Kandoi
5fa242ec93 Merge pull request #57392 from mihir-kandoi/fix/canonical-default-warehouse-names
fix: create default warehouses with untranslated names
2026-07-23 16:17:04 +05:30
Mihir Kandoi
a81524ea14 Merge pull request #57400 from aerele/fix-address-missing-company-address-field
fix: guard against missing is_your_company_address custom field on ad…
2026-07-23 16:10:24 +05:30
Mihir Kandoi
2f008ea846 Merge pull request #57403 from mihir-kandoi/fix-stock-ageing-batch-pool-rebalance
fix: rebalance batch slot values at the pooled rate when driven negative
2026-07-23 16:09:19 +05:30
Mihir Kandoi
194e1df5cc fix: rebalance batch slot values at the pooled rate when driven negative
A batch is one valuation pool, so consumption is valued at the pooled
rate while slots may carry stale intra-batch detail (e.g. units
reconciled at zero and later merged). Consuming such a slot leaves a
negative value on positive qty. Spread the pool value across the
batch's slots when that happens; non-batchwise slots pool per
warehouse.
2026-07-23 15:56:42 +05:30
Mihir Kandoi
4911484349 Merge pull request #57399 from mihir-kandoi/production-plan-reserve-bom-qty
fix: Production Plan raw material qty calculation and bin reservation
2026-07-23 15:27:21 +05:30
Mihir Kandoi
a370c9a348 fix: AttributeError in sufficient sub assembly warning 2026-07-23 15:16:13 +05:30
Mihir Kandoi
276f69498a fix: safety stock and MOQ over-ordering in Production Plan raw materials 2026-07-23 15:16:11 +05:30
Mihir Kandoi
c0cb783603 fix: reserve full BOM consumption for Production Plan raw materials 2026-07-23 15:16:11 +05:30
Mihir Kandoi
9ec2262d09 Merge pull request #57398 from mihir-kandoi/item-group-get-root-of
refactor: use get_root_of for Item Group root resolution
2026-07-23 15:01:16 +05:30
Mihir Kandoi
3b3e7b5ea4 refactor: use get_root_of for Item Group root resolution
Replaces the hand-rolled parentless-group query from #57390 with the
framework helper, matching sibling tree doctypes like Customer Group.
2026-07-23 14:48:37 +05:30
Mihir Kandoi
da8bf368da Merge pull request #57387 from mihir-kandoi/drop-job-card-operation-row-number
refactor: drop unused operation_row_number field from Job Card
2026-07-23 14:42:07 +05:30
rohitwaghchaure
8c0ec3c179 fix: Incorrect creation time at the time cancelling an entry causing an issue especially same posting datetime (#57380)
* fix: shift same-timestamp sibling SLEs when cancelling an entry

update_qty_in_future_sle compared against the reversal SLE's own
creation and skipped same-posting_datetime siblings on cancel, leaving
their qty_after_transaction stale and causing false negative stock
errors.

* fix: revert update_qty_in_future_sle cancel tie-break, it double-counted
2026-07-23 09:07:33 +00:00
Mihir Kandoi
53ae349b5a Merge pull request #57390 from mihir-kandoi/fix-item-group-root-lookup
fix: do not translate root Item Group lookup key
2026-07-23 14:22:31 +05:30
Mihir Kandoi
002ca6d5c1 Merge pull request #57388 from mihir-kandoi/mapper-dict-target-doc
fix: accept dict target_doc in mapper endpoints
2026-07-23 14:22:06 +05:30
pandiyan
ea3ed8b836 fix: guard against missing is_your_company_address custom field on address 2026-07-23 14:06:23 +05:30
Mihir Kandoi
861c6b16be fix: create default warehouses with untranslated names
Company.create_default_warehouses stored warehouse_name through the
session _(), so a site set up in a non-English language keeps its
default warehouses under translated names. The opening-stock fallback
in item.py then looks up {"warehouse_name": _("Stores")} from an
arbitrary session and misses the warehouse whenever the lookup
session's language differs from the creation session's.

Create the default warehouses with canonical English names, matching
the identity-_ convention install_fixtures uses for other default
records. The exists-guard also matches the session translation so a
re-run on a legacy site does not insert English duplicates next to
translated warehouses.

The "Stores" fallback lookup moves to get_stores_warehouse, which
tries the canonical name first and falls back to the session
translation so legacy sites keep resolving their translated warehouse.
The setup-wizard and test-bootstrap lookups drop _() since they now
run after English creation (install_fixtures already used identity _,
so its lookup silently missed translated warehouses before this
change).

Same bug class as #57345.
2026-07-23 14:05:22 +05:30
Mihir Kandoi
d197d5685e fix: resolve Item Group tree root structurally
A name lookup misses roots created under a translated name (pre-#49875
setups) or renamed roots. Resolve the first parentless group by lft and
guard against self-parenting when the root itself is saved.
2026-07-23 14:03:54 +05:30
Shllokkk
f2496d2e2c fix: respect selected BOM when creating work order for variant item (#57358) 2026-07-23 13:49:34 +05:30
Mihir Kandoi
9c8d9ac42b fix: validate supplied operation_id belongs to work order operation 2026-07-23 13:23:15 +05:30
Mihir Kandoi
2551543703 fix: accept dict target_doc in mapper endpoints
Native JSON request bodies deliver target_doc as a parsed dict, which the
str | Document type hints rejected, breaking every Get Items From button.
Unify all whitelisted mapper endpoint hints to str | dict | Document | None.
Needs frappe#41190 so get_mapped_doc converts the dict target.
2026-07-23 13:20:40 +05:30
Mihir Kandoi
553336fa6f fix: resolve or require operation_id server-side 2026-07-23 13:17:49 +05:30
Mihir Kandoi
7239e6ed8c fix: do not translate root Item Group lookup key
_("All Item Groups") resolves in the session language, so for
non-English users the db.exists lookup missed the root (stored in
English) and new groups were saved parentless, becoming uneditable
second roots.

Closes #57345
2026-07-23 13:17:48 +05:30
Mihir Kandoi
cff05687d9 Merge pull request #57389 from mihir-kandoi/fix/translated-default-record-lookups
fix: do not translate default record lookup keys
2026-07-23 13:06:35 +05:30
Mihir Kandoi
50de87c9ea fix: clear stale operation_id and guard async row selection 2026-07-23 13:00:10 +05:30
Mihir Kandoi
8cd36f60ab fix: prompt for operation row when operation repeats in work order 2026-07-23 12:52:27 +05:30
Mihir Kandoi
9b7c36f3d9 fix: do not translate default record lookup keys
Company.create_default_departments named and looked up the root
Department via _("All Departments"), which resolves in the session
language. A site set up in a non-English language stores the root
translated, and a company created later from a session in another
language misses it and inserts a second root, corrupting the tree.
Resolve the root once with get_root_of (falling back to the canonical
English name on fresh installs) and reuse it for the root record, the
exists-guard and the child departments' parent.

Also stop translating lookups of records install_fixtures stores under
English names: Price List "Standard Selling" and Print Headings
"Credit Note" / "Debit Note".

Same class as the root Item Group fix (#57386, issue #57345).
2026-07-23 12:51:30 +05:30
Mihir Kandoi
c93b63d757 Merge pull request #57383 from mihir-kandoi/company-restriction-permlevel
fix: gate company restriction fields behind permlevel 1
2026-07-23 12:40:16 +05:30
Mihir Kandoi
d71681a99e refactor: drop unused operation_row_number field from Job Card 2026-07-23 12:37:29 +05:30
Mihir Kandoi
92b62402be Merge pull request #57384 from pratheep-bit/fix/fr-corr-001-reorder-item
fix: log exception instead of swallowing in notify_errors
2026-07-23 12:34:24 +05:30
Jatin3128
a47f25896b feat: make Shipping Rule Cost Center optional with company default fallback (#57355)
feat(shipping-rule): make cost center optional with company default fallback

Cost Center on Shipping Rule is no longer mandatory. When left blank, the
applied shipping tax row falls back to the company default cost center,
avoiding the 'Cost Center is required for Profit and Loss account' error on
submit. The rule's project is also applied to the tax row.
2026-07-23 07:00:23 +00:00
Mihir Kandoi
4bb63cca10 test: probe permlevel visibility via a value field only
get_permitted_fieldnames never lists Table fields, so allowed_companies
cannot be asserted through it; the write-reset assertions already cover
that field.
2026-07-23 12:26:59 +05:30
Mihir Kandoi
41fc23b9b2 fix: drop Customer's unused level-1 Sales User read grant
Makes visibility uniform across the three masters: only the
master-manager role can see or edit company restriction fields.
2026-07-23 12:12:49 +05:30
Khushi Rawat
45553e46ec Merge branch 'develop' into fix/mt940-per-transaction-reference 2026-07-23 12:10:38 +05:30
srujan00123
551d709c64 fix: map MT940 per-transaction reference from :61: customer_reference
The mt940 library exposes ``transaction_reference`` from the :20: tag,
which is the statement-level reference and identical for every
transaction in a statement. The bank-statement-to-CSV conversion was
using it verbatim, so every imported row ended up with the same
reference, making reconciliation impossible.

Read the per-transaction reference from ``customer_reference`` on the
:61: tag instead. Handle two edge cases:

- **Overflow >16 chars.** When a bank emits a single-line :61: whose
  reference exceeds 16 characters, the mt940 regex splits the tail into
  ``extra_details``. Gate the rejoin to cases where
  ``customer_reference`` is exactly at the 16-char MT940 cap; below
  that, ``extra_details`` is genuine supplementary information and must
  not be appended.
- **``NONREF`` sentinel.** The MT940 standard marker for "no customer
  reference". Check it against the un-concatenated value so that a
  ``NONREF`` customer reference with populated ``extra_details`` still
  falls back to ``bank_reference`` instead of returning a junk
  ``NONREFsomething`` value.

Also switch the Description column to ``transaction_details`` (the :86:
tag content) so rows carry their real narrative instead of the mostly
empty :61: supplementary field.
2026-07-23 12:10:20 +05:30
Mihir Kandoi
565220ebe5 fix: gate company restriction fields behind permlevel 1
Only the master-manager role of each doctype (Item Manager, Sales Master
Manager, Purchase Master Manager) can view and edit restrict_to_companies
and allowed_companies. Customer reuses its existing level-1 permission
rows; Item and Supplier get a new level-1 row.
2026-07-23 12:07:16 +05:30
Nabin Hait
7b93252621 Merge pull request #57299 from nabinhait/feat/warn-existing-draft-links
feat: warn when a draft linked document already exists
2026-07-23 10:38:45 +05:30
Nabin Hait
7e0c81391b test: use a role-less user for the permission check
test@example.com carries System Manager in the frappe fixtures, so on a
fresh CI site it can read Delivery Note and the lookup legitimately
returns the draft. test1@example.com has no roles, making the
no-permission assertion environment-independent.
2026-07-23 10:27:41 +05:30
Pratheep Selvam
4830bc51c1 fix: log exception instead of swallowing in notify_errors 2026-07-23 09:23:39 +05:30
Mihir Kandoi
5dab7b9928 Merge pull request #57361 from mihir-kandoi/batch-prefill-stock-controller
refactor: move new-doc route options to StockController
2026-07-22 18:01:56 +05:30
Mihir Kandoi
551559e804 refactor: move new-doc route options to StockController
set_route_options_for_new_doc lived in TransactionController, so doctypes
extending StockController directly (Stock Reconciliation, Stock Entry) missed
the Batch/SABB prefill or duplicated it locally. Move it to StockController
and call it from onload_post_render so all descendants inherit it.

- Batch quick entry from Stock Reconciliation items now prefills Item
- SABB route options unified: warehouse || s_warehouse || t_warehouse,
  so transaction doctypes now also prefill warehouse
- Stock Entry's duplicate handler removed; its onload_post_render now
  calls super
2026-07-22 17:58:38 +05:30
Mihir Kandoi
3d46f7eba2 Merge pull request #57357 from mihir-kandoi/posting-datetime-desc
chore: better description for posting datetime for naming series field
2026-07-22 15:40:24 +05:30
Mihir Kandoi
dcfe1706db Merge pull request #57356 from mihir-kandoi/company-restriction-link-scan-perf
perf: scan link fields once per table in company restriction check
2026-07-22 15:34:49 +05:30
Mihir Kandoi
deafe79dc9 chore: better description for posting datetime for naming series field 2026-07-22 15:28:56 +05:30
Mihir Kandoi
a22b10e5eb perf: scan link fields once per table instead of per row
Meta.get_link_fields rescans the field list on every call; hoisting it
out of the row loop cuts the reference walk on a 100-row invoice from
2.8ms to 0.1ms.
2026-07-22 15:21:51 +05:30
Nabin Hait
01f7e45058 Merge branch 'develop' into feat/warn-existing-draft-links 2026-07-22 15:18:39 +05:30
Mihir Kandoi
ae9bb27436 Merge pull request #57353 from mihir-kandoi/fix/reserved-batch-precision
fix: get reserved batch qty precision from settings
2026-07-22 15:11:11 +05:30
Mihir Kandoi
2401b04090 fix: get reserved batch qty precision from settings 2026-07-22 14:58:02 +05:30
Mihir Kandoi
e147f6be98 Merge pull request #57352 from mihir-kandoi/company-restriction-transaction-enforcement
fix: enforce company restrictions at transaction level
2026-07-22 14:52:54 +05:30
Mihir Kandoi
e3ec8d2975 refactor: exempt system doctypes via in_create flag
Doctypes marked In Create (GL Entry, Stock Ledger Entry, Bin, ledger
entries) are system-created by definition, so derive their exemption
from meta instead of listing them.
2026-07-22 14:40:27 +05:30
Mihir Kandoi
1982816a70 refactor: enforce company restrictions on any doctype with a Company link
Replace the manually maintained transaction allowlist with a wildcard
validate hook: any doctype carrying a Company link field is checked, so
new doctypes are covered automatically. System-managed doctypes (ledger
entries, reposts, bundles, bins, POS consolidation, bank feeds) are
exempted so cancel, repost and reconciliation of documents created
before a restriction changed keep working; that guarantee is pinned by
a cancel-after-restriction test.
2026-07-22 14:36:48 +05:30
Mihir Kandoi
ce01fa0e34 fix: cover manufacturing, logistics, asset and service doctypes
Extend company restriction enforcement to the remaining user-entered
transactions (BOM, Work Order, Job Card, Production Plan, Pick List,
Blanket Order, asset and maintenance documents). Ledger and repost
doctypes stay excluded so cancelling or reposting older documents
keeps working after a restriction changes.
2026-07-22 14:28:13 +05:30
Mihir Kandoi
01892c2e26 refactor: hook validate_allowed_companies instead of calling per master
Item, Customer and Supplier each imported and called it in their
validate; register it once in doc_events instead.
2026-07-22 14:17:32 +05:30
Mihir Kandoi
88b8ce3888 fix: enforce company restrictions at transaction level
Restrict to Companies only filtered list views and document reads, and
only for users with Company user permissions. Any user could still use
a master restricted to Company A in a Company B transaction, and users
without Company user permissions bypassed the feature entirely.

Validate on save of transactions that every linked Item, Customer and
Supplier allows the transaction company, and filter item link queries
by the transaction company so restricted items don't show up in the
item selector.
2026-07-22 14:13:22 +05:30
Nabin Hait
c522a1fdae fix: disable pagination in draft link lookup
frappe.get_list defaults to 20 rows; child-table joins can produce
duplicate parent names that fill the window and hide further drafts.
Also clarify the check-ordering regression test.
2026-07-22 12:06:00 +05:30
Nishka Gosalia
eafd43769b Merge pull request #57333 from nishkagosalia/fix-settings-map
fix: settings map cleanup
2026-07-22 11:09:23 +05:30
Jatin3128
1029cd988a fix(accounts receivable): made territory field multi select (#57322) 2026-07-22 10:53:17 +05:30
Diptanil Saha
8cb96496ec fix(payments): ensure payments app installed on the site in payment_app_import_guard (#57342) 2026-07-21 21:21:40 +00:00
Diptanil Saha
6d31af3a52 chore: remove apiclient (#57339) 2026-07-21 20:15:25 +00:00
nishkagosalia
721fd56013 fix: settings map cleanup 2026-07-21 19:51:05 +05:30
Krishna Shirsath
f328018bfb fix(italy): skip e-invoicing for opening invoices 2026-07-21 16:48:25 +05:30
Mihir Kandoi
a008be7f0f Merge pull request #57316 from mihir-kandoi/fix-stock-ageing-reco-revaluation
fix: rescale stock ageing FIFO slot values on stock reconciliation
2026-07-21 15:45:51 +05:30
Mihir Kandoi
ed855c3823 fix: resolve float precision before streaming stock ledger entries
get_single_value inside _revalue_reconciled_batch_slots runs while
rows stream through the unbuffered cursor on MariaDB, killing the
active iterator. Resolve it once in generate() with the other
prefetches.
2026-07-21 14:34:08 +05:30
Shllokkk
4485179375 fix: show create payment entries as an inner button on row selection 2026-07-21 14:22:10 +05:30
Mihir Kandoi
7cba539cb0 fix: use system float precision for batch qty comparison 2026-07-21 14:18:19 +05:30
Mihir Kandoi
4cd1b6a8bf fix: revalue batch reco slots only when the entry covers the full batch
stock_value_difference / qty equals the new batch rate only when the
reco entry carries the entire batch, as the split out/in reco SLEs and
batches reconciled from zero do. Partial direct-batch_no entries mix a
qty delta with existing stock, so their slots keep prior values.

Plain items need no such guard: the valuation engine collapses the
FIFO stack to qty_after * valuation_rate on every reconciliation, so
rescaling remaining slots at the reco rate matches the ledger. Lock
that with a test.
2026-07-21 14:09:28 +05:30
Mihir Kandoi
3ce31be80a fix: rescale batch FIFO slot values on stock reconciliation
Batch items take the batch-slot path, which mirrors the same value
arithmetic: the reco's incoming entry dumps the revaluation remainder
on one slot. Rescale each reconciled batch's slots at its post-reco
rate (stock_value_difference / qty of the incoming bundle entry).
2026-07-21 13:49:21 +05:30
Mihir Kandoi
7a68e8bf4d fix: rescale stock ageing FIFO slot values on stock reconciliation
A reconciliation's stock_value_difference includes the revaluation of
stock already in the FIFO queue, but the whole amount was attached to
the qty-delta slot while older slots kept pre-revaluation values. A
downward revaluation therefore produced negative bucket values in the
Stock Ageing report, and repeated recos let the queue total drift away
from Stock Balance.

Re-derive every slot value as qty * valuation_rate after processing a
reco SLE, since a reconciliation values the entire balance at its rate.
Covers both single-SLE recos and the zero-out/re-add pair that flows
through the transfer bucket.
2026-07-21 13:44:36 +05:30
Shllokkk
98d58bcd6a fix: sync process loss percentage when fg qty changes (#57063) 2026-07-21 07:32:43 +00:00
Henil Maru
83e04dd773 fix: show transaction currency symbol in Payment Request schedule dialog and reference table (#57050)
* fix: show transaction currency symbol in Payment Request schedule dialog and reference table

When company currency (INR) differs from customer currency (USD), the Amount
column in the Select Payment Schedule dialog and the Payment Reference table on
the Payment Request form incorrectly displayed the company currency symbol (₹)
instead of the transaction currency symbol ($).

- Pass `currency` from the parent document on each schedule row returned by
  `get_available_payment_schedules` so the dialog can resolve the symbol.
- Add a hidden `currency` field to the dialog table and set `options: "currency"`
  on `payment_amount` so Frappe renders the correct symbol.
- Propagate `currency` into Payment Reference rows in `set_payment_references`.
- Add a hidden `currency` Link field to the Payment Reference child DocType and
  set `options: "currency"` on its `amount` field so the table renders correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: preserve currency when serializing payment schedule rows

get_available_payment_schedules set `schedule.currency` directly on
the Payment Schedule Document row, but `currency` isn't a field on
that DocType, so the API response serializer stripped it before it
reached the client. The Select Payment Schedule dialog and the
Payment Reference table therefore always fell back to the company
currency symbol, even with the earlier options="currency" changes in
place.

Convert each row to a plain dict via as_dict() first, then set the
currency key on the dict so it survives serialization.

* refactor: source schedule currency in dialog instead of API serializer

get_available_payment_schedules had to convert each child row with
as_dict() and re-attach currency, because currency is not a field on
Payment Schedule and the response serializer drops attributes set on the
Document itself.

The schedule dialog already has the transaction currency on frm.doc, so
set it there and let the API keep returning the schedule rows unchanged.
Payment Reference still stores currency per row.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jatin3128 <jatinsarna8@gmail.com>
Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-07-21 11:58:38 +05:30
Diptanil Saha
8327f19ebf ci: fix zh two_letters_code mapping (#57307) 2026-07-21 01:06:14 +05:30
MochaMind
ab6931279d fix: sync translations from crowdin (#57259) 2026-07-21 00:41:52 +05:30
Soham Kulkarni
9d5e3be0b3 Merge pull request #57305 from frappe/revert-57292-customer
Revert "fix: mark selling as default workspace for customer"
2026-07-21 00:01:04 +05:30
Soham Kulkarni
58b839eb71 Revert "fix: mark selling as default workspace for customer" 2026-07-20 21:44:49 +05:30
Mihir Kandoi
1b36459d2b Merge pull request #57301 from mihir-kandoi/stock-summary-bin-qty-fields
feat(stock): expose all Bin qty fields in Stock Summary and Stock Projected Qty
2026-07-20 20:17:25 +05:30
Mihir Kandoi
e146c318e5 Merge pull request #57300 from mihir-kandoi/bin-recalculate-values
feat: recalculate valuation rate and stock value from Bin
2026-07-20 20:10:07 +05:30
Mihir Kandoi
59c0c15c2e feat(stock): expose all Bin qty fields in Stock Summary and Stock Projected Qty
Stock Summary's sort selector only offered 5 of Bin's 10 qty fields; add
the rest (ordered, requested, planned, reserved for production plan,
reserved stock) and extend get_data's or_filters so bins whose only
nonzero qty is one of the new fields show up when sorted by it. Sort
labels now mirror Bin field labels.

Stock Projected Qty report had a column for every Bin qty field except
reserved_stock; add it.
2026-07-20 20:05:37 +05:30
Mihir Kandoi
49a43aad81 fix: keep Standard Cost stock value in step with the standard rate
Mirrors update_qty's Standard Cost handling and drops fixed test item
names so reruns start from fresh SLE-less items.
2026-07-20 19:59:13 +05:30
Mihir Kandoi
df79e85f53 feat: recalculate valuation rate and stock value from Bin
Renames the Recalculate Bin Qty button to Recalculate Values and sets
valuation_rate and stock_value from the last SLE (0 when none exists).
2026-07-20 19:48:10 +05:30
Nabin Hait
7d351153bb feat: warn when a draft linked document already exists
When creating a follow-up document (SO->DN, PO->PR, PI->Payment Entry,
etc.), warn the user if a draft of the target doctype already linked to
the source document exists, with links to the drafts and the option to
proceed anyway.

The target doctype comes from the make_mapped_doc response via the new
frappe.model.add_mapped_doc_guard hook, so every open_mapped_doc flow is
covered without per-doctype code or method-name inference. The server
lookup walks parent-level and child-table Link / Dynamic Link fields of
the target doctype and queries through frappe.get_list, so role and user
permissions apply and docstatus filtering happens in the query itself.
Payment Entry creation bypasses open_mapped_doc, so its controller runs
the same guard explicitly.
2026-07-20 18:37:24 +05:30
Jatin3128
b917aca361 refactor: clearer labels for the overdue billing control (#57298)
refactor: clearer labels and messages, drop "threshold" wording

User-facing text only, no field or behaviour changes:

- Accounts Settings toggle label -> "Restrict Customer Over Billing".
- Bypass role label -> "Role Allowed to Bypass Over Billing Restriction".
- Customer Credit Limit field label -> "Overdue Limit".
- Rewrote the descriptions and the block message to match and to stop
  saying "threshold".
2026-07-20 18:14:44 +05:30
Mihir Kandoi
f19216979b Merge pull request #57280 from aerele/timesheet_group_by
fix(report): handle nonetype error in timesheet billing summary group…
2026-07-20 17:43:31 +05:30
Deepesh Garg
3b10ff7df7 fix: Ignore permission while deleting user permission 2026-07-20 17:05:21 +05:30
Diptanil Saha
73004c6e4b refactor: rework appointment booking lifecycle and portal verification (#57270)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:35:58 +05:30
Soham Kulkarni
e728c24b80 Merge pull request #57292 from sokumon/customer
fix: mark selling as default workspace for customer
2026-07-20 16:06:09 +05:30
sokumon
873bce3c46 fix: mark selling as default workspace for customer 2026-07-20 15:53:41 +05:30
Poovetha
9a7209e668 fix(report): handle nonetype error in timesheet billing summary grouping logic 2026-07-20 13:39:07 +05:30
rohitwaghchaure
4cdaa8dba6 fix: block changing Stock account type when stock ledger entries exist (#57283) 2026-07-20 13:20:10 +05:30
Mihir Kandoi
276e688949 Merge pull request #57273 from aerele/typo-fix-allow-negative-stock
fix: correct typo in allow_negative_stock parameter
2026-07-20 12:31:13 +05:30
Nishka Gosalia
ccb9b16378 Merge pull request #57274 from nishkagosalia/gh-57206
fix: project % complete field allowing modification when manual method
2026-07-20 11:46:20 +05:30
nishkagosalia
21009c18c0 fix: project % complete field allowing modification when manual method 2026-07-20 11:34:09 +05:30
Afsal Syed
b3a616c328 fix: correct typo in allow_negative_stock parameter 2026-07-20 10:52:00 +05:30
rohitwaghchaure
2eecdc48bf feat: inline serial and batch entries editor (#57216)
* feat: inline serial and batch entries editor in Purchase Receipt

* feat: grid-style UX, deferred saves, scan and range options for inline serial batch editor

* feat: extend inline serial batch editor to all bundle doctypes with auto fetch

* fix: address review comments on inline serial batch editor

* fix: escape untrusted values in inline editor alerts

* fix: clear child bundle reference only when the row owns the bundle

* fix: keep inline serial batch editor disabled on existing sites via patch
2026-07-19 19:21:09 +05:30
MochaMind
ddb094084e chore: update POT file (#57269) 2026-07-19 14:50:34 +02:00
Nabin Hait
e932105ee3 fix(selling): recompute proforma amount for all rows on qty change
Refreshing a single grid row only updates the active row, so the derived amount
for rows after the edited one went stale. Recompute every row's amount from
qty x rate and re-render the grid.
2026-07-19 12:26:34 +05:30
Nabin Hait
654c9e6ad8 style(selling): form-builder layout tweaks and label rename
- Proforma Invoice form: place Grand Total beside Total Quantity (column break)
  and minor field reorder
- Rename the tab button to "New Proforma Invoice"
- Add the Proforma Invoice client-script scaffold
2026-07-19 11:25:40 +05:30
Nabin Hait
7314cedd53 feat(selling): surface proforma settings in Selling Settings tabs
- Move the Proforma Invoice settings section from the Subcontracting Inward tab
  to the Transaction tab
- List Proforma Invoice in the Document Naming tab so its naming series can be
  configured there
2026-07-19 11:21:41 +05:30
Mihir Kandoi
1cd32b9c73 Merge pull request #57258 from mihir-kandoi/per-master-company-restriction 2026-07-18 20:39:13 +05:30
Mihir Kandoi
ea5c648ab0 refactor: gate company restrictions per master via Restrict to Companies checkbox
Replaces the Global Defaults toggle. Each Item/Customer/Supplier now
carries a Restrict to Companies checkbox: the Allowed Companies table
only shows (and is mandatory) when checked, is cleared on uncheck, and
permission filtering, read denial and write validation apply only to
masters that have the checkbox set.
2026-07-18 19:03:31 +05:30
Mihir Kandoi
4d49cfa1de Merge pull request #57256 from mihir-kandoi/fix-multi-batch-serial-stock-reco
fix: scope current serial nos to the selected batch in stock reconciliation
2026-07-18 18:56:15 +05:30
Mihir Kandoi
f43f8f75d0 fix: scope current serial nos to the selected batch in stock reconciliation
get_stock_balance_for fetched serial nos across every batch in the
warehouse, so reconciling one batch of a serial+batch item compared the
selected serials against the pool of all batches and failed whenever
multiple batches existed.
2026-07-18 16:51:46 +05:30
Mihir Kandoi
71ccc8885e Merge pull request #57253 from aerele/fix-pick-list-work-order-transferred-qty-leak
fix: exclude transferred_qty from work order item to pick list item m…
2026-07-18 14:05:55 +05:30
pandiyan
5b36f12596 fix: exclude transferred_qty from work order item to pick list item mapping
get_mapped_doc copies same-named fields by default. work order item's
transferred_qty (cumulative across the whole work order) was leaking into
the new pick list item's transferred_qty (meant to track how much of
that pick list row has been converted into a stock entry, starting at 0).

the leaked value then got subtracted again in
get_pending_transfer_stock_qty(), so every pick list after the first
under-transferred raw materials by whatever was already recorded on the
work order, driving material_transferred_for_manufacturing towards zero
across repeated partial pick-list/finish cycles.

fixes #57236, related to #56596
2026-07-18 13:36:48 +05:30
Mihir Kandoi
ca5bec2b77 Merge pull request #57249 from mihir-kandoi/ppmr
fix: add fetch from in production plan material request child table
2026-07-17 22:19:58 +05:30
Mihir Kandoi
dfc2a411e1 fix: add fetch from in production plan material request child table 2026-07-17 22:08:15 +05:30
kaulith
1aee0df79a fix: force-delete repost data file during cleanup (#57245)
* fix(stock): force-delete repost data file during cleanup

* test(stock): cover repost data file cleanup with attach guard
2026-07-17 22:07:20 +05:30
Mihir Kandoi
e0896c656c Merge pull request #57244 from mihir-kandoi/fix-clear-old-logs-orphan-references
fix: clear linked comments, versions and attachments with old logs
2026-07-17 22:06:23 +05:30
Mihir Kandoi
6a69237130 Update erpnext/utilities/__init__.py
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-17 21:53:16 +05:30
Mihir Kandoi
334346e0f8 Merge pull request #57241 from mihir-kandoi/fix-material-request-buying-price-list
fix: validate buying price list on material request and update item rates on change
2026-07-17 21:38:10 +05:30
Mihir Kandoi
3a63f61832 chore: remove unneccessary flt 2026-07-17 21:24:40 +05:30
Mihir Kandoi
1887825ce5 fix: clear linked comments, versions and attachments with old logs
Repost Item Valuation and BOM Update Log cleared old logs with a raw
delete on the parent table, orphaning timeline comments, versions,
attachments and other reference records.

Fixes #57237
2026-07-17 21:24:08 +05:30
Mihir Kandoi
1ef3cd1d3f fix: dont overwrite rate with 0 if not found 2026-07-17 21:23:49 +05:30
Mihir Kandoi
a31119353c Merge pull request #57223 from aerele/project_validation
fix(projects): include on hold status in project filters and reports
2026-07-17 20:46:11 +05:30
Mihir Kandoi
6dcc0cab3a fix: pass ctx keys get_price_list_rate_for reads, skip rate update on insert
update_item_rates passed price_not_uom_dependent, a key
get_price_list_rate_for never reads, and omitted conversion_factor, so a
stock-UOM price was never converted to the row UOM. The function's
(historically misnamed) price_list_uom_dependant ctx key carries the
Price List's price_not_uom_dependent value: truthy returns the found
rate as-is, falsy multiplies by conversion_factor.

Also guard on_update with is_new(): has_value_changed returns True when
there is no doc_before_save, so every first save re-wrote item rates.
2026-07-17 20:44:07 +05:30
Mihir Kandoi
18b15f2ca9 fix: validate buying price list on material request and update item rates on change 2026-07-17 20:44:07 +05:30
Nabin Hait
2a9603fcd6 style(selling): organise Proforma Invoice form into sections
Group fields with section and column breaks: Details (two columns), Items with
right-aligned totals, Print Settings (two columns), and Status (two columns).
2026-07-17 20:12:37 +05:30
Nabin Hait
63607e91fd feat(selling): open the Proforma tab after creating a proforma
Flag the form on create and activate the Proforma tab once the reloaded form
has rendered the list, so the new proforma is shown immediately.
2026-07-17 20:04:18 +05:30
Nabin Hait
853c1d8986 feat(selling): add a New button below the proforma listing
Place a "+ New" button under the Proforma tab list to create another proforma
without leaving the tab.
2026-07-17 19:16:28 +05:30
Jatin3128
a33da337ec feat: block sales invoice submit when customer overdue exceeds threshold (#57230)
* feat: block sales invoice submit when customer overdue exceeds threshold

Adds an opt-in, per-customer Overdue Billing Threshold. When enabled in
Accounts Settings, submitting a Sales Invoice is blocked if the customer's
overdue amount exceeds their threshold, unless the current user holds a
configured bypass role. Modeled on the existing credit limit feature.

- Accounts Settings (Credit Limits tab): enable toggle + bypass role.
- Per-customer threshold on the Customer Credit Limit table, shown only
  when the feature is enabled via a property setter (same mechanism as
  subscription / accounting dimension sections). Table relabeled to
  "Credit & Overdue Limits".
- Overdue is read live from the ledger via get_outstanding_invoices
  (payments already netted), summing Sales Invoices past their due date.
- Enforced in Sales Invoice on_submit, after the credit-limit check;
  returns are exempt.
- validate_credit_limit_on_change no longer trips when a row sets only
  the overdue threshold (credit_limit = 0).

Fixes #52960

* fix: compute overdue amount in company currency and format with fmt_money

get_customer_overdue_amount now sums GL Entry debit - credit grouped per
invoice, which is always booked in company currency, instead of using
get_outstanding_invoices which returns the receivable-account currency.
The threshold is in company currency, so the previous comparison could mix
currencies for customers with a foreign-currency receivable account. This
mirrors how get_customer_outstanding computes the figure for the existing
credit-limit check.

The blocking message now formats both amounts with fmt_money using the
company currency.

Adds a test asserting a 100 USD invoice at a conversion rate of 50 is
counted as 5000 in company currency.

* refactor: drop redundant threshold coercion and dead test cleanup

- Coerce the overdue threshold with flt() once when reading it, instead of
  calling flt() on it at each of the three use sites.
- Remove a no-op set_overdue_billing_threshold() call in the feature-disabled
  block (the threshold was already set to that value) and the trailing reset,
  which is dead since each test is rolled back.

No behaviour change.

* fix: compute overdue amount from payment terms, matching the Overdue status

The overdue amount keyed on Sales Invoice.due_date, which set_due_date() sets
to the LAST payment term. An invoice whose first term was past due and unpaid
was therefore counted as zero, even though ERPNext already shows it as Overdue
in the invoice list. The gate and the UI could disagree.

get_customer_overdue_amount now follows the same rule as is_overdue(): per
invoice, the amount that has fallen due (sum of payment schedule terms past
their due date) minus what has been paid, clamped to the outstanding balance.
Invoices without a schedule (POS, opening) still fall back to the invoice due
date, mirroring is_overdue()'s own guard.

The ledger stays the source of truth for what is unpaid: the outstanding per
invoice is still SUM(debit) - SUM(credit) from GL Entry. base_payment_amount is
always stored in company currency, so no currency conversion is needed and the
comparison against the threshold stays consistent.

Adds a test covering a two-term invoice: only the past-due term counts, and
paying it off clears the overdue amount.

* feat: honour the overdue billing threshold set on the customer group

The threshold lives on Customer Credit Limit, which is also rendered on
Customer Group. A threshold set there was stored but never evaluated, so the
configuration was a silent no-op.

get_overdue_billing_threshold now reads the customer's row and falls back to
its customer group, mirroring get_credit_limit. The group's
bypass_credit_limit_check is deliberately not consulted: it is labelled for the
credit limit check at sales order and is unrelated to overdue billing.

get_customer_group_details also dropped the threshold when copying group rows
onto a customer, because it copied a single hardcoded field per table. It now
copies a list of fields per table, so credit_limit and overdue_billing_threshold
both carry over.
2026-07-17 18:58:10 +05:30
Nabin Hait
1da4657530 feat(selling): option to hide item qty on amount-based proforma print
Add a "Hide Item Quantity in Print" option (Amount basis only) that omits the
qty and rate columns from the printed proforma, for a clean value-based
document that shows only item and amount.
2026-07-17 17:44:04 +05:30
Nabin Hait
2242f1b230 feat(selling): make qty editable in amount-based proforma
In Amount basis, both qty and amount are now user-entered and the rate is
derived from them (rate = amount / qty). Previously qty was forced to the
ordered qty, which ignored an edited qty when switching basis.
2026-07-17 17:37:01 +05:30
rohitwaghchaure
40f861c0a0 fix: parallel reposting stalls between scheduler ticks (#57220) 2026-07-17 16:27:27 +05:30
Poovetha
7248961568 fix(projects): add project filter 2026-07-17 15:03:25 +05:30
Poovetha
79e5ccd370 test(projects): add test to ensure on hold project retains status 2026-07-17 15:03:25 +05:30
Poovetha
51a9fc0316 fix(projects): include on hold status in project filters and reports 2026-07-17 15:03:25 +05:30
Nabin Hait
77540403ab feat(selling): prefill remaining proforma qty/amount and reorder Create button
- Pre-fill each line with the remaining (ordered minus already proformed) qty
  and amount, so the default no longer trips the excess warning
- Place the Proforma Invoice action after the standard Create options
2026-07-17 14:11:26 +05:30
Mihir Kandoi
3f53475cad Merge pull request #57233 from mihir-kandoi/fix-stock-entry-pending-work-order-query
fix: replace column-literal work order filter with server-side query
2026-07-17 14:05:05 +05:30
Mihir Kandoi
eed7c98b30 fix: replace column-literal work order filter with server-side query
The work_order link filter in Stock Entry passed the string
`tabWork Order`.produced_qty as a filter value. It was never a real
column comparison: db_query coerces string values on numeric fields
with flt(), so the condition silently degraded to qty > 0, and on
backends that don't coerce text to numeric (postgres) such filters
fail with InvalidTextRepresentation.

Move the condition into a whitelisted search query that compares the
columns properly (qty > produced_qty), mirroring pick_list's
get_pending_work_orders.
2026-07-17 13:54:08 +05:30
Nabin Hait
4177101ac0 feat(selling): warn when total proforma exceeds the ordered qty/amount
Show a non-blocking notice below the item table when a line's total proforma
quantity (or amount) — this proforma plus already-issued ones — exceeds the
Sales Order line's ordered value. It updates live as the qty/amount or the
basis changes, and the user can still create the proforma.

Issued-proforma qty/amount are aggregated per line on demand (cancelled
proformas excluded); nothing is stored on the Sales Order.
2026-07-17 13:42:11 +05:30
Mihir Kandoi
d8eb029284 Merge pull request #57228 from mihir-kandoi/fix-transit-warehouse-company-fallback
fix: fall back to the company in-transit warehouse
2026-07-17 13:06:54 +05:30
Mihir Kandoi
920e64ded0 fix: fall back to the company in-transit warehouse
set_transit_warehouse re-tested from_warehouse inside a guard that
already requires it, so the Company branch of its ternary was
unreachable: with no default on the source warehouse the field stayed
empty and Company.default_in_transit_warehouse was silently ignored.
Try the warehouse's default first, then the company's.
2026-07-17 13:05:29 +05:30
Nabin Hait
f711375885 feat(selling): keep cancelled proformas visible with their PDF
Cancelling a proforma should void it, not erase history.

- Persist the Cancelled status on cancel (db_set) and keep the PDF attached
- Proforma tab now lists cancelled proformas with a red status badge, so the
  voided document and its PDF stay reachable for audit
2026-07-17 12:50:44 +05:30
Nabin Hait
473c655cb2 feat(selling): add amount-based proforma option
Let a proforma be created by editing item amount instead of quantity, for
value/advance-style proformas.

- "Based On" (Quantity | Amount) on the proforma and the create dialog
- Amount basis keeps the ordered qty and derives a rate so the line totals
  the entered amount; the PDF renders the same in-memory Sales Order copy
- Proforma Invoice Item now stores rate and amount
2026-07-17 12:50:44 +05:30
Nabin Hait
f9a09e1b3d refactor(selling): drop proforma quantity tracking
Remove the pending/proforma-qty machinery: it only fit staged, incremental
proformas and misrepresented the common whole-order / re-issued cases.

- Drop proforma_qty from Sales Order Item and its submit/cancel write-back
- Drop pending-qty aggregation and the over-qty soft warning
- The create dialog now pre-fills the ordered qty (editable down)
2026-07-17 11:51:03 +05:30
Nabin Hait
b0c53ac5a4 test(selling): add Proforma Invoice tests
Cover partial proforma being non-blocking on delivery/billing,
pending-qty aggregation with cancelled proformas excluded, tax scaling
to the partial qty, over-qty as a soft warning, and the settings gate.
2026-07-17 11:45:49 +05:30
Nabin Hait
fdc8879ce1 feat(selling): wire Proforma Invoice into Sales Order form
- Create > Proforma Invoice dialog with naming series, print format and
  letter head selectors, and an item-wise pending-qty grid
- Proforma tab listing issued proformas with inline view/email actions,
  shown only once at least one proforma exists
- Register the client script and add the connections dashboard link
2026-07-17 11:45:49 +05:30
Diptanil Saha
9fdb6dd875 Merge pull request #57224 from diptanilsaha/fix/dunning_type_validation
fix: added missing validations for `Dunning Type`
2026-07-17 10:45:40 +05:30
diptanilsaha
2325068b19 test: added tests for Dunning Type validation 2026-07-17 10:35:10 +05:30
diptanilsaha
c8e7674d63 fix: added missing validations for Dunning Type 2026-07-17 10:35:04 +05:30
Mihir Kandoi
0cba26c608 Merge pull request #57225 from mihir-kandoi/fix-duplicate-workspace-links
fix: remove duplicate links from home and projects workspaces
2026-07-17 08:05:53 +05:30
Mihir Kandoi
7fc43aba72 fix: remove duplicate links from home and projects workspaces
Workspace re-export in #56864 duplicated every link in the Home and
Projects workspaces, so desk renders each link twice. Same issue as
55afd95b20. Bumped modified so existing sites re-sync.
2026-07-17 07:55:17 +05:30
Mihir Kandoi
fc50e23daa Merge pull request #57194 from aerele/fix-operation-batch-size-flag-n-plus-one
fix: batch operation batch-size flag lookups to avoid n+1 query in wo…
2026-07-17 07:47:59 +05:30
pandiyan
6500752440 fix: batch operation batch-size flag lookups to avoid n+1 query in work order operations 2026-07-16 23:35:45 +05:30
MochaMind
2b4fc02c81 fix: sync translations from crowdin (#57188) 2026-07-16 18:01:04 +02:00
Shllokkk
76298d9bee Merge pull request #57198 from Shllokkk/strip-account-number-coa-importer
fix: strip account number when building account name in COA importer
2026-07-16 20:50:58 +05:30
Nishka Gosalia
5fc03a116a feat: map settings for DocTypes to show on settings dialog (#57025)
fix: mapping settings for DocType settings
2026-07-16 19:30:27 +05:30
Nabin Hait
65db3e374b feat(selling): add Proforma Invoice print format
Jinja print format on Sales Order, rendered against the in-memory
qty-adjusted copy so taxes and totals reflect the partial quantity.
2026-07-16 16:51:56 +05:30
Nabin Hait
07756f2bec feat(selling): add Proforma Invoice doctype and server API
- Submittable, non-accounting Proforma Invoice + Proforma Invoice Item
  child doctype (in_create; posts no GL/stock, stores item + qty only)
- Server API: pending-qty aggregation per Sales Order line (issued
  proformas only), make_proforma_invoice (sole creation path, gated on
  the settings toggle), PDF rendered from an in-memory qty-adjusted copy
  of the Sales Order and attached, send_proforma_email
- Non-blocking proforma_qty write-back to the Sales Order on submit/cancel
2026-07-16 16:51:48 +05:30
Nabin Hait
fbdc1f5b1f feat(selling): add proforma invoice settings and tracking field
- Selling Settings: "Enable Proforma Invoice" toggle (opt-in) and a
  default proforma print format
- Sales Order Item: non-blocking proforma_qty counter
2026-07-16 16:51:37 +05:30
Diptanil Saha
ac68db3fa6 refactor(dunning): converted get_dunning_letter_text to doc method and restrict_globals on render_template (#57205) 2026-07-16 16:06:13 +05:30
rohitwaghchaure
7b517a4e64 feat: book Expenses Added To Stock GL entries for stock vouchers (configurable) (#57190)
* feat: book Expenses Added To Stock GL entries for Stock Entry, Stock Reconciliation and LCV

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: make stock expense GL booking configurable via Accounts Settings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: skip stock expense booking for unconfigured companies, check flag once per compose

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:04:45 +00:00
Mihir Kandoi
a25acee43f Merge pull request #57179 from aerele/link-portal-users-to-contact-from-relevent-doctype
feat(stock): automatically link portal users to their associated contact profiles for customers and suppliers
2026-07-16 15:23:49 +05:30
Afsal Syed
9ae2069bd9 test(stock): add portal user contact link verification for customer and supplier 2026-07-16 15:11:28 +05:30
Afsal Syed
337a06dfb6 feat(stock): automatically link portal users to their associated contact profiles for customers and suppliers 2026-07-16 15:11:28 +05:30
Mihir Kandoi
103f3e50a7 Merge pull request #57202 from mihir-kandoi/pg-read-committed-gates
fix(stock): serialize postgres stock writes per (item, warehouse); block GL inserts during account rename
2026-07-16 14:10:56 +05:30
Mihir Kandoi
d0010fda68 Merge pull request #57204 from mihir-kandoi/fix-production-plan-min-order-qty
fix: consider min order qty in the purchase/transfer flow of production plan
2026-07-16 13:28:55 +05:30
Mihir Kandoi
448316fe8e test: assert row count in the min order qty split scenario 2026-07-16 12:56:11 +05:30
Mihir Kandoi
2f8d588093 fix: consider min order qty in the purchase/transfer flow of production plan
The transfer flow ignored Consider Minimum Order Qty twice: the JS
handler force-reset the checkbox before fetching items, and the
purchase remainder left after allocating transfers from other
warehouses was never raised to min_order_qty (the check runs on the
total requirement before the split).

Drop the JS reset and apply min order qty to the purchase remainder,
in stock UOM before the purchase UOM conversion.
2026-07-16 12:46:21 +05:30
Mihir Kandoi
b100e6d414 fix(stock): serialize pick list allocation per item on postgres
Two simultaneous allocations for the same item can both claim the same stock
on postgres: the picked-items locking read cannot see the rows another
in-flight creation is inserting, while MariaDB's gap locks make the creations
take turns. Advisory-gate set_item_locations per item (sorted against
deadlocks) so the second allocation waits, then subtracts the first's claim.
MariaDB unchanged.
2026-07-16 09:58:09 +05:30
Mihir Kandoi
897eca895a fix(stock): fall back gracefully when transaction_advisory_lock is unavailable
Same hasattr pattern as repost_gate: an ERPNext ahead of its frappe build keeps
the status-quo serialization-failure retries instead of failing every stock
submission on postgres.
2026-07-16 09:32:03 +05:30
Mihir Kandoi
35a9d7b09c fix(accounts): block GL Entry inserts during account rename on postgres
The for_update read in _ensure_idle_system only blocks new GL inserts on
MariaDB, via the gap lock it takes; a postgres row lock never blocks inserts,
so the guard silently degraded to the 5-minute recency check. LOCK TABLE IN
EXCLUSIVE MODE blocks writers (not readers) until the rename commits and NOWAIT
keeps the wait=False fail-fast, feeding the existing QueryTimeoutError path.
2026-07-16 09:21:17 +05:30
Mihir Kandoi
9cfdb482fc fix(stock): serialize stock writes per (item, warehouse) with a txn advisory lock on postgres
Postgres locking reads never see rows a concurrent transaction is inserting
(MariaDB's gap locks block the insert, then its locking reads return the fresh
row), so two concurrent writers for the same (item, warehouse) compute from the
same stale previous SLE and the loser overwrites Bin with a wrong absolute qty.
Today only the REPEATABLE READ serialization-failure retry catches this; the
gate makes correctness lock-based, covers the empty-history first-transaction
case (nothing exists to row-lock), and keeps negative-stock validation accurate
against concurrently inserted SLEs. Taken at the top of make_sl_entries (sorted
pairs, before the future_sle_exists cache warms) and in
update_entries_after.__init__ for the repost paths; re-entrant, released at
commit. MariaDB paths unchanged.
2026-07-16 09:21:07 +05:30
Shllokkk
6f6ec3072a Merge branch 'develop' into strip-account-number-coa-importer 2026-07-16 00:18:46 +05:30
Shllokkk
cbe406ee2a fix: strip account number when building account name in COA importer 2026-07-16 00:17:35 +05:30
Mihir Kandoi
7fe4dc1367 Merge pull request #56746 from frappe/mergify/configuration-deprecated-update
ci(mergify): upgrade configuration to current format
2026-07-15 12:53:07 +05:30
Mihir Kandoi
d5126dcad5 Merge pull request #56884 from aerele/fix/v16-report-date-guard
fix: validate mandatory date filters in reports
2026-07-15 12:51:29 +05:30
Diptanil Saha
fee3a6e0fd fix(accounts): update AU standard chart of accounts (#57145)
Co-authored-by: Jebajebas <jeba.j@arus.co.in>
2026-07-15 12:22:02 +05:30
Diptanil Saha
72b72a81fa fix(project): improved access control for project users (#56675)
* fix: permission check for `get_task_html` and `get_timesheet_html`

* fix(project): enabled project access control for users without `Projects User` Role

* fix(portal): validate user permissions for project portal

* fix: patch to add docshare for the project users

* fix(patch): selecting correct column on the query

* fix(project): grant access to all the current users for new project

* fix(portal): fixed condition to display timesheets on project

* test(portal): add access control tests for project user

* fix(project): using `frappe.has_permission` instead of `self.has_permission` to validate user permissions

* fix(project): granting docshare access for every ProjectUser

Roles for an User can be removed any time or an User Permission can be added which might restrict the access to the Project.

* fix(patch): create docshare documents for non-cancelled projects and users who have no docshare documents

* test(project): removed `test_control_access_does_not_touch_users_with_real_permission`
2026-07-15 12:20:27 +05:30
rohitwaghchaure
e99966a38e fix: skip redundant reposting of dependent items (#57092)
* fix: skip redundant reposting of dependent items

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: use earliest cascade datetime and batch repost item lookup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 12:09:34 +05:30
Mihir Kandoi
1d6edf9674 fix: name every conflicting voucher in the reserved batch error (#57174)
* fix: name every conflicting voucher in the reserved batch error

* fix: exclude fully-delivered reservations from the conflict message

* fix: round outstanding qty guard consistently with the conflict gate
2026-07-15 06:13:32 +00:00
Mihir Kandoi
67587a79e9 Merge pull request #57169 from mihir-kandoi/fix-shared-reserved-batch-delivery
fix: allow delivery when a batch is reserved across multiple sales orders
2026-07-15 11:10:58 +05:30
Mihir Kandoi
2310c4c005 fix: allow delivery when a batch is reserved across multiple sales orders
validate_reserved_batches compared the voucher's own qty against the
remaining batch qty, so delivering one order's reserved unit threw
Reserved Batch Conflict whenever the remainder exactly matched another
order's reservation. Compare the remaining batch qty against the
aggregated outstanding reserved qty (qty - delivered_qty) of other
vouchers instead, excluding reservations the voucher itself delivers.
2026-07-15 10:59:52 +05:30
Mihir Kandoi
576a5d26df Merge pull request #57137 from aerele/projects_status
feat: add on hold status to project
2026-07-15 10:51:28 +05:30
Mihir Kandoi
b6b19790d2 Merge pull request #57164 from mihir-kandoi/gh57158
fix: set correct currency in supplier quotation net rate field
2026-07-15 10:46:32 +05:30
Mihir Kandoi
117bb912cb Merge pull request #57163 from mihir-kandoi/hide-job-card-poi
fix: hide job card field in purchase order item
2026-07-15 10:42:58 +05:30
Mihir Kandoi
27672851cd fix: set correct currency in supplier quotation net rate field 2026-07-15 10:34:07 +05:30
Mihir Kandoi
f44bcae47d fix: hide job card field in purchase order item 2026-07-15 10:31:07 +05:30
Mihir Kandoi
71f843e5bf Merge pull request #57154 from aerele/fix-production-plan-bom-warehouse-n-plus-one
fix: batch BOM source warehouse lookup in get_production_items to avo…
2026-07-14 22:18:58 +05:30
pandiyan
4705909cee fix: batch BOM source warehouse lookups to avoid n+1 queries in production plan work order creation 2026-07-14 22:07:52 +05:30
Nabin Hait
04f75cc64f Merge pull request #56983 from nabinhait/fix-flaky-usd-exchange-rate-tests
test: seed current-dated USD↔INR exchange rate to fix flaky currency tests
2026-07-14 18:13:28 +05:30
Nabin Hait
5133ba47b7 fix: make currency exchange truly idempotent against any pre-existing state
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-14 18:02:59 +05:30
rohitwaghchaure
1fd2faa68d fix: permission issue (#57112) 2026-07-14 12:22:48 +00:00
Soham Kulkarni
4d2b603ba6 Merge pull request #57134 from sokumon/merge-workspaces
fix: merge erpnext workspaces
2026-07-14 17:10:28 +05:30
Poovitha Palanivelu
672fadaa78 feat: add on hold status to project 2026-07-14 16:43:32 +05:30
Mihir Kandoi
d7f4524cdd refactor: convert Hide Currency Symbol in Global Defaults to a Check field (#57135) 2026-07-14 11:10:07 +00:00
Mihir Kandoi
876adcb535 Merge pull request #57129 from SandraFrappe/fix/purchase-order-duplicate-material-request-item
fix: prevent duplicate material request items in purchase order
2026-07-14 16:31:26 +05:30
Mihir Kandoi
b2ec906ff3 test: remove test 2026-07-14 16:18:53 +05:30
sokumon
f2e8c7b664 fix: add sequence for erpnext 2026-07-14 16:05:44 +05:30
Mihir Kandoi
b6cce627a8 feat: company-wise restriction for Item, Customer and Supplier masters (#57124) 2026-07-14 10:30:47 +00:00
SandraFrappe
2d6f89a7f5 fix: prevent duplicate material request items in purchase order 2026-07-14 14:32:06 +05:30
sokumon
ac99d28100 chore: merge erpnext workspaces 2026-07-14 14:27:39 +05:30
Mihir Kandoi
823dbb7a2b Merge pull request #57127 from mihir-kandoi/naming-posting-date-default-on
feat: make naming series based on posting datetime on by default on n…
2026-07-14 13:49:34 +05:30
Mihir Kandoi
7db93d8b19 feat: make naming series based on posting datetime on by default on new sites 2026-07-14 13:38:41 +05:30
Diptanil Saha
d82e6c4f12 fix(accounts): added permission checks on get_account_balances_coa (#57107) 2026-07-14 07:26:43 +00:00
Smit Vora
7f4be47d43 Merge pull request #56912 from ljain112/refactor-subcon
refactor: move functionality in postprocess for mapped doc
2026-07-14 11:53:58 +05:30
Khushi Rawat
7371df129f Merge pull request #57111 from khushi8112/remove-dead-dashboard-fixtures
chore: remove dead assets dashboard_fixtures with broken imports
2026-07-14 11:53:07 +05:30
khushi8112
14a15cc6f9 chore: remove dead assets dashboard_fixtures with broken imports (#57079)
erpnext.accounts.dashboard_fixtures and erpnext.buying.dashboard_fixtures
were removed in 2020 when dashboards were exported to JSON fixtures.
The assets module's dashboard_fixtures.py was left behind unreferenced;
its dashboard, charts and number cards already exist as exported JSON.
2026-07-14 11:40:34 +05:30
Mihir Kandoi
f4f9030437 Merge pull request #57114 from frappe/mergify/bp/develop/pr-57113
fix(stock): set stock_uom on transferred Stock Reservation Entries (backport #57113)
2026-07-14 11:39:00 +05:30
Mihir Kandoi
1d5ec14530 Merge pull request #57116 from aerele/get_bundle_wise_serial_nos-db-params-limit
fix(stock): fix sqlparse token limit in get_bundle_wise_serial_nos
2026-07-14 11:29:34 +05:30
PranavDarade
e321e95e59 fix(stock): set stock_uom on transferred Stock Reservation Entries
StockReservation.transfer_reservation_entries_to() created the transferred SREs without copying stock_uom, in both the entries_to_reserve dict and the extra-items fallback. get_items_to_reserve() already selects the item's stock_uom, so entry.stock_uom is used.

On sites with a global default stock_uom (e.g. "Nos"), frappe's _set_defaults() backfilled the blank field, so the transfer silently stored the wrong UOM for any item whose stock UOM is not the default. On sites without that default the SRE's validate_mandatory() raised "Stock UOM is required", aborting Work Order submission for the Subcontracting Inward Order / Production Plan flows.

(cherry picked from commit 5991ecfa3d)
2026-07-14 11:27:07 +05:30
S Sakthivel Murugan
3e8784f596 fix: validate mandatory date filters in reports 2026-07-14 11:24:51 +05:30
Afsal Syed
e748bf512b test(stock): add unit test for get_bundle_wise_serial_nos query 2026-07-14 11:18:17 +05:30
Afsal Syed
4544a6c935 fix(stock): fix sqlparse token limit in get_bundle_wise_serial_nos 2026-07-14 11:18:17 +05:30
Mihir Kandoi
336970aa5a Merge pull request #57115 from mihir-kandoi/fix/supplier-scorecard-month-end-idempotency
fix: duplicate scorecard period when supplier is created on a month end
2026-07-14 10:55:59 +05:30
Mihir Kandoi
3aafda331b fix: duplicate scorecard period when supplier is created on a month end
make_all_scorecards' dedup query used strict bounds, so a single-day
period (supplier created on a month's last day -> start == end under
"Per Month") never matched its own window and was re-created on every
call. The daily refresh_scorecards job would insert a duplicate
submitted period each day for such suppliers, and
test_make_all_scorecards_is_idempotent fails on any date where
nowdate() - 75 days lands on a month end — both nightly server suites
went red on 2026-07-14 (75 days after April 30).

Inclusive bounds cannot false-match adjacent periods: each next period
starts at end_date + 1, so closed intervals never touch.
2026-07-14 10:20:41 +05:30
Mihir Kandoi
39b5c6ba2a Merge pull request #57101 from aerele/fix/job-card-work-order-transferred-qty
fix(manufacturing): preserve job card transferred quantity
2026-07-13 21:06:28 +05:30
Mihir Kandoi
3dba2d23b8 Merge pull request #57099 from aerele/fix-stock-entry-bom-query-redundancy
perf: avoid redundant bom cost_allocation_per query per finished item…
2026-07-13 21:05:13 +05:30
Mihir Kandoi
b23224b3d6 Merge pull request #57091 from aerele/fix/so-qty-company-warehouse
fix(stock): show qty (company) and qty (warehouse) in sales transactions
2026-07-13 21:03:07 +05:30
Mihir Kandoi
c0147e5a2e Merge pull request #57089 from aerele/fix-pick-list-barcode-scan-max-qty
fix: allow barcode scan to add and increment items in pick list
2026-07-13 21:02:08 +05:30
Sudharsanan11
51f9b70bfa test(manufacturing): cover transferred quantity across job cards 2026-07-13 19:40:20 +05:30
Sudharsanan11
951e432a1b fix(manufacturing): preserve job card transferred quantity 2026-07-13 19:40:20 +05:30
pandiyan
feb750f082 perf: avoid redundant bom cost_allocation_per query per finished item row in stock entry 2026-07-13 18:29:23 +05:30
Diptanil Saha
9c353741ad fix(tnc): using get_cached_doc to retrieve template for get_terms_and_conditions and permission checks (#57096) 2026-07-13 11:24:56 +00:00
Sudharsanan11
4e5e1f6596 test(stock): assert qty (company) and qty (warehouse) on item details
covers sales order, quotation, sales invoice, delivery note and
purchase order, asserting actual_qty from the row warehouse and
company_total_stock across all warehouses of the company.
2026-07-13 14:37:23 +05:30
Sudharsanan11
ab30bab6cb fix(stock): show qty (company) and qty (warehouse) in sales transactions
company was passed to get_bin_details only for purchase order, so
company_total_stock was never returned for sales order, quotation,
sales invoice and delivery note and the qty (company) column always
read zero. pass ctx.company for every doctype, which also drops the
dependency on doc being supplied.

on the client, set_actual_qty copied only actual_qty out of the
response, so qty (company) never refreshed on a warehouse change. use
frm.call with child so every bin field is applied, pass
include_child_warehouses to match the server, and include quotation.
2026-07-13 14:37:23 +05:30
pandiyan
3ece4a615d fix: allow barcode scan to add and increment items in pick list
- allow new rows on scan when pick manually is enabled, since only
  then are scanned rows not subject to being overridden by
  set_item_locations on save
- stop capping picked qty at the default demand qty (1) for rows
  added by the scanner itself, so repeat scans of the same barcode
  keep incrementing the row instead of failing with "maximum
  quantity scanned"
- ignore barcode uom when matching an existing row if new rows
  aren't allowed, since there's no alternate-uom row to fall back to
2026-07-13 13:54:54 +05:30
ruthra kumar
33abc53d7a Merge pull request #56817 from Soham-ambibuzz/philipinnes_localization_coa_v3
feat: restructure Philippines chart of accounts with amortization sup…
2026-07-13 12:56:31 +05:30
ruthra kumar
47341ff55c Merge pull request #56628 from aerele/multi-currency
fix(journal-entry): fetch outstanding on foreign currency
2026-07-13 12:35:15 +05:30
ruthra kumar
7144e5b29a Merge pull request #56458 from Shllokkk/psoa-restrict-jinja-globals
fix: restrict jinja globals in process statement of accounts templates
2026-07-13 12:34:04 +05:30
ruthra kumar
f0326aa6cf Merge pull request #56902 from frappe/fix-budget-variance-chart-month-shift
fix(budget-variance): correct month shift in comparison chart
2026-07-13 12:31:27 +05:30
Khushi Rawat
2580601937 Merge pull request #57071 from iamejaaz/fix-letterhead-company-guard
fix: guard company logo lookup in default letterheads
2026-07-13 11:58:31 +05:30
Khushi Rawat
14dac6ecc4 Merge pull request #55276 from aerele/fix/asset-repair-fully-depreciated
fix(asset): allow asset repair creation for fully depreciated assets
2026-07-13 02:09:13 +05:30
Mihir Kandoi
e8a61ecfbb Merge pull request #57073 from mihir-kandoi/gh57072
fix: make represents company field in purchase invoice ignore user pe…
2026-07-13 00:08:24 +05:30
Mihir Kandoi
6729a53fee fix: make represents company field in purchase invoice ignore user permissions 2026-07-12 23:56:23 +05:30
MochaMind
612a3f20d8 chore: update POT file (#57067) 2026-07-12 20:25:54 +02:00
Ejaaz Khan
e39ca72997 fix: set explicit table and logo widths in grey letterhead 2026-07-12 22:03:32 +05:30
Ejaaz Khan
23c09fe0f3 fix: guard company logo lookup in default letterheads 2026-07-12 21:17:42 +05:30
Nabin Hait
e1e56b6920 test: adjust currency tests for deterministic seeded exchange rate
Seeding a current-dated USD->INR rate makes get_exchange_rate resolve
62.9 on today() instead of hitting the live API, which exposed three
tests that implicitly relied on a different/undefined current rate:

- customer: dropped its own colliding current-dated seed (ignored via
  ignore_if_duplicate, and its cleanup deleted the shared seed) and now
  asserts the quotation resolves the seeded rate via get_exchange_rate.
- exchange_rate_revaluation: the revalued rate (62.9) is now below the
  booked 80, so the revaluation is a loss (debited) rather than a gain;
  derive the gain/loss column from the sign instead of assuming a gain.
- purchase_invoice: the receipt rate was an accidental tuple (70,) that
  got discarded and recomputed to the seed; set explicit rates with the
  receipt above the invoice so the stock exchange difference is a credit,
  matching the asserted column.
2026-07-12 21:12:29 +05:30
Nabin Hait
7928608b0f Merge pull request #56973 from nabinhait/refactor-stock-gl-composer
refactor(stock): de-conditionalize BaseStockGLComposer via subclass hooks
2026-07-12 20:32:49 +05:30
Mihir Kandoi
4a160c10f8 Merge pull request #57070 from mihir-kandoi/fix-dimension-search-check-field
fix: accounting dimension search matching unrelated records
2026-07-12 20:26:26 +05:30
Mihir Kandoi
38e8c3b1fe fix: accounting dimension search matching unrelated records
#56871 routed any searchfield without a DocField meta — including
"name", which get_search_fields() always appends — into the
`= cint(txt)` branch. For non-numeric search text cint() yields 0, so
`is_group = 0` (Cost Center) matched every leaf record on both
engines, and `name = 0` matched every non-numeric name on MariaDB.

Skip Check fields from or_filters entirely — a checkbox can't match
search text — and keep LIKE for everything else, including "name".
2026-07-12 18:37:51 +05:30
Mihir Kandoi
8db4f82af5 Merge pull request #57069 from mihir-kandoi/fix-job-card-pick-list-project
fix(stock): propagate project from job card to stock entry
2026-07-12 17:51:26 +05:30
Mihir Kandoi
72492f5da2 fix(stock): propagate project from job card to stock entry 2026-07-12 17:32:30 +05:30
mergify[bot]
45102e12cc feat: explain FIFO allocation of fixed Discount Amount on Sales Order (backport #56436) (#57062)
* feat: explain FIFO allocation of fixed Discount Amount on Sales Order (#56436)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
(cherry picked from commit 62fed1d562)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.json

* chore: resolved conflicts

---------

Co-authored-by: Mohammad Umair Sayed <umair_sayyed@yahoo.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-11 17:57:49 +00:00
Raghav Ruia
b96d6e2a93 fix: remove incorrect Payable account_type from Customer Deposits in Philippines CoA (#57018) 2026-07-11 23:09:14 +05:30
Pandiyan P
ad17efe243 fix(accounts): retain invoice table on opening invoice creation error (#56353)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-11 13:17:28 +00:00
Pandiyan P
0524a235d7 fix: update events order by date asc (#56963)
Co-authored-by: nareshkannasln <nareshkannashanmugam@gmail.com>
2026-07-11 18:24:49 +05:30
Nikhil Kothari
d449ad3b3f fix(banking): allow negative balance in bank statement import (#56959) 2026-07-11 12:28:03 +00:00
Vishnu Priya Baskaran
9b66382463 fix(financial_statement): render columnar financial statements instea… (#56921) 2026-07-11 16:09:26 +05:30
Vishnu Priya Baskaran
a721ad3948 fix(payment reconciliation): read user permissions from current session user (#56782) 2026-07-11 16:08:27 +05:30
Vishnu Priya Baskaran
199eeff22c fix: map stock_qty in apply_price_list_on_item (#56869) 2026-07-11 15:03:49 +05:30
Nabin Hait
c5bc17b884 Merge branch 'develop' into fix-flaky-usd-exchange-rate-tests 2026-07-11 14:29:22 +05:30
Nabin Hait
df60d61402 Merge branch 'develop' into refactor-stock-gl-composer 2026-07-11 14:28:52 +05:30
Nabin Hait
0264202b42 Merge pull request #56978 from nabinhait/refactor-ac-services
refactor(accounts): extract deferred-accounting and document-schedule from AccountsController
2026-07-11 14:28:16 +05:30
MochaMind
4b6860de62 fix: sync translations from crowdin (#57010)
* fix: Swedish translations

* fix: Bosnian translations
2026-07-11 00:33:45 +02:00
Shllokkk
3cabf13abe Merge pull request #57045 from Shllokkk/work-order-mapper-method-path
fix: use correct mapper path for make_work_orders call
2026-07-10 21:01:06 +05:30
Shllokkk
394c9d80f9 fix: use correct mapper path for make_work_orders call 2026-07-10 20:59:08 +05:30
Mihir Kandoi
ad9430d17e Merge pull request #57035 from aerele/perf-bom-default-accounts-lookup
perf(bom): batch default account/cost-center/warehouse lookups in bom…
2026-07-10 17:15:01 +05:30
Vishnu Priya Baskaran
a30f72dae1 fix: fetch payment entry reference amounts from invoice (#56928) 2026-07-10 17:01:49 +05:30
Mihir Kandoi
51d7bf64a8 Merge pull request #57031 from mihir-kandoi/fix-job-card-pick-list-transfer
fix(stock): link job card in stock entry created from pick list
2026-07-10 16:54:31 +05:30
Mihir Kandoi
bf4e1a3ae0 Merge pull request #57033 from aerele/perf-bom-raw-material-warehouse-lookup
perf(stock): avoid n+1 queries for work order item source warehouse
2026-07-10 16:37:44 +05:30
Mihir Kandoi
2c03894e00 fix(stock): batch job card item lookup and honour semi_fg_bom
Fetch job_card_item for all pick list locations in one query instead
of one per row, and prefer the job card's semi_fg_bom over the work
order BOM, mirroring the direct Job Card -> Stock Entry mapper.
2026-07-10 16:36:44 +05:30
pandiyan
e88e63976a perf(bom): batch default account/cost-center/warehouse lookups in bom explosion 2026-07-10 16:35:43 +05:30
pandiyan
3e4d5e6745 perf(stock): avoid n+1 queries for work order item source warehouse
hoist the invariant work order lookup and batch-fetch work order item
source warehouses once instead of querying per raw material row in
get_bom_raw_materials
2026-07-10 16:03:18 +05:30
Mihir Kandoi
bdbb8481b0 fix(stock): link job card in stock entry created from pick list
A Stock Entry created from a Pick List against a job card's Material
Request never set job_card, job_card_item, fg_completed_qty or the
'Material Transfer for Manufacture' purpose, so the Job Card did not
recognize the transfer and blocked submission. The WIP warehouse was
also not populated.

Route such pick lists through a job-card-aware branch mirroring the
direct Material Request -> Stock Entry mapper, and set the purpose to
'Material Transfer for Manufacture' in the work order branch so the
WO -> MR -> Pick List flow updates the work order too.
2026-07-10 15:20:28 +05:30
ruthra kumar
4aabe3347f Merge pull request #56801 from ruthra-kumar/reversing_exchange_rate_revaluation
refactor: reversing exchange rate revaluation journals
2026-07-10 11:49:41 +05:30
Sudharsanan Ashok
9cb6610b9e fix(stock): correct stock ageing value for moving average and lifo items (#56693)
* fix(stock): recompute moving average item slots

* test(stock): add test to validate the stock value of moving average items

* fix(stock): support lifo valuation in stock ageing report

lifo items were aged as fifo (oldest consumed first), so the report kept the
newest lots on hand and reported the wrong stock value and average age. prefetch
each item's valuation method (it can't be resolved mid-stream without breaking the
unbuffered cursor) and consume from the tail for lifo items. also reuse that shared
lookup in the moving average revaluation pass. scoped to plain items; batch, serial
and same-voucher repack legs stay on fifo.

* test(stock): add test for lifo consumption in stock ageing report
2026-07-10 11:34:47 +05:30
ruthra kumar
65775e59a1 refactor(test): for reverse journals as well 2026-07-10 11:34:30 +05:30
ruthra kumar
6838242063 refactor: handle reverse ERR journals in AR / AP report 2026-07-10 10:38:54 +05:30
ruthra kumar
a0b14c0607 refactor: reversal capability on exchange rate revaluation 2026-07-10 10:38:52 +05:30
ruthra kumar
6a4c5b6062 refactor: add payment ledger to ignore link 2026-07-10 08:27:28 +05:30
Vishnu Priya Baskaran
38da3fc76e fix: display outstanding amount using company default currency (#56785)
Co-authored-by: S Sakthivel Murugan <s.sakthivelmurugan2003@gmail.com>
2026-07-10 00:57:20 +05:30
Mihir Kandoi
271f8cf1ca Merge pull request #56948 from aerele/feat/production-plan-group-rm-warehouse
feat(manufacturing): allow group warehouse for raw material availability in production plan
2026-07-09 20:52:18 +05:30
Mihir Kandoi
1eed00f5d4 Merge pull request #56428 from raghavisruia/item-allow-negative-stock-confirmation
feat: confirmation dialog when enabling negative stock on Item
2026-07-09 20:52:03 +05:30
Mihir Kandoi
4fbb20fbd9 Merge pull request #56255 from akhtarmohsin/fix-variant-of-filter-item-list
Fix Variant Of filter to show only template items
2026-07-09 20:41:54 +05:30
Mihir Kandoi
8583f0ba05 Merge pull request #56979 from harisansari008/fix/bom-routing-update-operations
fix: update BOM operations when routing is changed
2026-07-09 20:41:12 +05:30
Mihir Kandoi
1b6d3ee870 Merge pull request #56561 from aerele/fix/report-currency
fix: use company currency instead of global default in report
2026-07-09 20:38:55 +05:30
Sudharsanan11
2247ef9a50 test(manufacturing): add production plan group warehouse tests
verify child-stock aggregation, transfer sourcing from child warehouses,
that a for warehouse outside the group is rejected, and that a for
warehouse is required when raw materials are fetched.
2026-07-09 20:37:04 +05:30
Sudharsanan11
0574a0d95e feat(manufacturing): allow group warehouse for raw material availability in production plan
add an optional raw material group warehouse on production plan. when set,
raw material availability is checked across its child warehouses (bin rows
aggregated), while material is still received into for warehouse. for
warehouse is restricted to a child of the group and required when raw
materials are fetched; a group warehouse can never reach a material request
line. when the group is left blank, availability falls back to for warehouse
and the previous flow.
2026-07-09 20:37:04 +05:30
Sudharsanan11
b625525b03 fix(manufacturing): accept plain dict for doc in get_items_for_material_requests
the whitelisted endpoint typed doc as str | frappe._dict | Document, so a
json request body (a plain dict) failed pydantic type validation. widen to
str | dict | Document, matching the convention used elsewhere in erpnext.
2026-07-09 20:37:04 +05:30
Mihir Kandoi
87e7e53a6a Merge pull request #56932 from aerele/fix/work-order-planned-date-validation
fix: validate planned end date is not before planned start date in wo…
2026-07-09 20:34:57 +05:30
Mihir Kandoi
97533812e7 Merge pull request #56925 from aerele/fix/batch-bin-lookup-delivery-note
perf: batch bin lookups in delivery note stock update
2026-07-09 20:34:37 +05:30
Mihir Kandoi
508f03a73e Merge pull request #56923 from aerele/fix/reorder-item-warehouse-lookup-perf
perf: avoid per-row Warehouse doc fetches in auto reorder job
2026-07-09 20:33:56 +05:30
Mihir Kandoi
8dd56ecba5 Merge pull request #56913 from aerele/fix/trend-report-column-translations
fix: make trend report based-on and group-by column labels translatable
2026-07-09 20:33:13 +05:30
Mihir Kandoi
d6682703fb Merge pull request #56909 from aerele/fix/sync-variant-item-code-on-abbr-rename
fix(stock): rename variant item_code/item_name when attribute abbreviation changes
2026-07-09 20:32:50 +05:30
Mihir Kandoi
015fb61b10 Merge pull request #56984 from aerele/fix/apply-price-list-doc-dict
fix(stock): repair client calls broken by type-hint and module refactors
2026-07-09 20:31:24 +05:30
Sudharsanan11
4d629df299 fix(stock): point stock entry client calls at services module
The stock_entry_handler modules were moved to services; update the
retention, expired-batch and subcontract call paths in the client
so the whitelisted methods resolve again.
2026-07-09 18:52:31 +05:30
Sudharsanan11
5956d3e092 fix(stock): accept dict for doc arg in apply_price_list
The type-hint refactor rejected the doc dict sent by the form
controller; broaden the annotation to match ctx and fix the
cts= kwarg typo in transaction_base.
2026-07-09 18:48:01 +05:30
Nabin Hait
d387155e16 test: seed current-dated USD<->INR exchange rate in bootstrap
Tests that create USD documents dated today() (e.g. Sales Order in
test_advance_payment_ledger_entry, USD BOM in test_routing) rely on
get_exchange_rate() finding a USD->INR Currency Exchange record. The
only seeded records are dated 2016, so the lookup misses and falls back
to an external API that is blocked in CI, returning 0. That surfaces as
"Exchange Rate is mandatory" on Sales Order validation and a
ZeroDivisionError in BOM.get_routing (hour_rate / conversion_rate).

Whether it passes depends on which shard incidentally committed the
2016 records first, making it an order-dependent flake that unrelated
PRs trip by shifting test distribution.

Seed today()-dated USD<->INR rates once in BootStrapTestData so the
lookup resolves deterministically without the external API. Rates
mirror the latest Currency Exchange test_records to keep cost
calculations unchanged.
2026-07-09 18:26:14 +05:30
Mohd Haris
758a837de4 fix: update BOM operations when routing is changed
The routing field handler only fetched operations from the routing when
the operations table was empty. When a new BOM version is created (via
"New Version"), operations are copied from the source BOM, so selecting a
different routing left the old operations in place - both in the form and
after saving.

Drop the `!frm.doc.operations.length` guard from the routing handler so
that (re)selecting a routing always refetches the operations from that
routing via the existing get_routing method, which clears and repopulates
the operations table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:01:17 +05:30
Nabin Hait
174027bd57 refactor(accounts): extract deferred-accounting and document-schedule out of AccountsController
Continues the AccountsController service decomposition (Phase 5).

- Add accounts/services/deferred_accounting.py with DeferredAccountingService
  owning the deferred revenue/expense validations (income/expense account
  defaulting and service start/end date checks).
- Move the document-schedule orchestration (validate_all_documents_schedule
  and the invoice/non-invoice variants) into PaymentScheduleService, where
  they already delegated, removing the controller-to-service round trip.
- Update the three validate() call sites; keep
  validate_auto_repeat_subscription_dates on the controller (still called by
  buying/selling controllers).

No behavior change. accounts_controller.py 1818 -> 1745 lines.
2026-07-09 17:55:19 +05:30
Pandiyan P
8b3caeb578 fix(stock): pick list serial batch posting date (#56957)
* fix(stock): fall back to current date/time for serial and batch bundle posting datetime

Pick List has no posting_date/posting_time fields, so creating or updating a
Serial and Batch Bundle from a Pick List row crashed with
"TypeError: combine() argument 1 must be datetime.date, not None". Fall back
to today/now when the parent voucher doesn't carry its own posting date.

Fixes #56951

* fix(stock): accept a plain dict for add_serial_batch_ledgers' doc and child_row

The whitelisted add_serial_batch_ledgers only converted child_row into an
attribute-accessible frappe._dict when it arrived as a JSON string, and doc's
type hint only allowed Document | str. Frappe's JSON API delivers both as
plain dicts (see frappe.app.make_form_dict, which parses the request body
with orjson and only wraps the top-level dict, not nested values), so every
real request was rejected before the handler body ever ran: first with a
FrappeTypeError on doc, and once that's fixed, with an AttributeError on
child_row.serial_and_batch_bundle. parse_json already wraps a plain dict in
frappe._dict (and leaves a real Document instance untouched), so routing
child_row through it unconditionally fixes both.
2026-07-09 17:28:03 +05:30
Pandiyan P
e2178a5f19 feat(manufacturing): create material request for raw materials from work order (#56961)
* feat(manufacturing): create material request for raw materials from work order

allow raising a material transfer request directly from a work order,
mirroring the existing job card flow, so stores can fulfil it into wip
before the actual stock entry happens

* test(manufacturing): cover work order material request flow

verify the material request created from a work order carries the
right bom/purpose onto the resulting stock entry, and that the work
order status still moves to in process on a partial material-request
transfer
2026-07-09 17:27:21 +05:30
Nabin Hait
798680d2d5 refactor(stock): de-conditionalize BaseStockGLComposer via subclass hooks
Remove per-doctype branching from the shared stock GL composer so each
voucher owns its own behavior:

- Move the Stock Reconciliation voucher-detail synthesis out of
  BaseStockGLComposer.get_voucher_details into a
  StockReconciliationGLComposer.get_voucher_details override.
- Replace the hardcoded doctype allow-list in check_expense_account with
  an overridable class attribute enforce_pl_expense_account (default
  True). Vouchers that post the difference to a balance-sheet account
  (Stock Entry, Stock Reconciliation, Delivery Note) set it False.
- Add DeliveryNoteGLComposer to own the P&L-exempt rule and wire
  DeliveryNote.get_gl_entries to it.

No change to GL output; behavior is relocated, not altered.
2026-07-09 17:25:32 +05:30
mergify[bot]
d418dd9e70 fix(patch): moved create_company_custom_fields from pre_model_sync to post_model_sync (backport #56962) (backport #56965) (#56970)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-07-09 10:36:50 +00:00
Diptanil Saha
1885f9fb35 Merge pull request #56674 from diptanilsaha/fix/frappe_crm_sync
fix(crm_settings): skip allowed users check when frappe crm is installed locally
2026-07-09 15:30:46 +05:30
MochaMind
49228b458d fix: sync translations from crowdin (#56943) 2026-07-09 15:30:26 +05:30
Khushi Rawat
e3fb1340e1 Merge pull request #56964 from khushi8112/fix/depr-schedule-precision-match
fix: match depreciation schedule rows at currency precision to avoid duplicate JEs
2026-07-09 14:43:13 +05:30
khushi8112
947ed5dfe1 fix: match depreciation schedule rows at currency precision to avoid duplicate JEs 2026-07-09 14:14:44 +05:30
diptanilsaha
0f987d7135 chore: patch to clear out allowed users on crm_settings if frappe crm is installed on the site 2026-07-09 12:37:47 +05:30
diptanilsaha
2de423e225 fix(frappe_crm_api): handle failure for after_app_install and after_app_uninstall 2026-07-09 12:37:47 +05:30
diptanilsaha
c86aa2d6fe feat(crm_settings): auto-update crm sync settings on frappe crm install and uninstall 2026-07-09 12:37:41 +05:30
diptanilsaha
41badb3d74 fix(crm_settings): skip allowed users check when frappe crm is installed locally 2026-07-09 12:01:49 +05:30
Abdeali Chharchhodawala
95e2ce6d85 feat: add grouping by dimension functionality in financial reports (#54650)
Co-authored-by: Copilot <copilot@github.com>
2026-07-08 19:04:09 +05:30
Raffael Meyer
c12e3fba5e feat(sla): filter service level agreement link by document type (#56954) 2026-07-08 13:47:57 +02:00
Smit Vora
e7f81de159 Merge pull request #54855 from Abdeali099/growth-view-with-cfs-54675
refactor(financial-report): fix row transformation for growth calculations
2026-07-08 16:13:44 +05:30
Abdeali Chharchhoda
aad287d09e fix: enhance growth view filtering by validating period keys 2026-07-08 16:03:28 +05:30
Abdeali Chharchhoda
4c7499600c fix: update formatting of growth view for FS report 2026-07-08 16:03:28 +05:30
Abdeali Chharchhoda
698876672d refactor: simple utility for growth value computation for custom FS report 2026-07-08 16:03:28 +05:30
Abdeali Chharchhoda
c179460c98 refactor(financial-report): fix row transformation for growth calculations 2026-07-08 16:03:28 +05:30
Diptanil Saha
95212e5738 fix(tnc): get_terms_and_conditions render_template with safe_exec (#56944) 2026-07-08 00:18:35 +00:00
Diptanil Saha
8a940af7e1 fix: added permission checks on various whitelisted functions (#56745)
* fix: added permission checks on various whitelisted functions

* fix: permission checks on `get_party_account` and using "select" `ptype`

* test(`BootStrapTestData`): assign `Accounts User` role to User `test@example.com`
2026-07-08 05:29:52 +05:30
Shllokkk
c94973a932 Merge pull request #56926 from Shllokkk/pricing-rule-template-variant-validation
fix: validate template and its variant in the same Pricing Rule
2026-07-07 19:59:10 +05:30
Dany Robert
be10c8ced9 fix: precision issue causing reconciliation error (#54043)
* fix: precision issue causing reconciliation error

* chore: code styling changes

* test: precision causing reconciliation failure

* fix: enhance payment reconciliation tests for floating-point precision

* fix(test): incorrect assertion on status

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2026-07-07 19:44:35 +05:30
pandiyan
2ec780cb35 fix: validate planned end date is not before planned start date in work order 2026-07-07 16:34:58 +05:30
MochaMind
eb37d3b14b fix: sync translations from crowdin (#56919) 2026-07-07 12:59:22 +02:00
Shllokkk
a88048b378 fix: validate template and its variant in the same Pricing Rule 2026-07-07 13:55:22 +05:30
pandiyan
5da878d25f perf: batch bin lookups in delivery note stock update
update_current_stock() in delivery_note.py used to call
frappe.db.get_value("Bin", ...) separately for every row in items and
every row in packed_items - so a delivery note with 200 items and 200
packed items made 400 separate database calls on every save.

now it groups item codes by warehouse and fetches bin data with one
query per distinct warehouse, then assigns actual_qty/projected_qty to
each row from that result - same values as before, far fewer database
calls, and no cross-product over-fetch across warehouses.
2026-07-07 13:00:08 +05:30
pandiyan
6beb3d2509 perf: avoid per-row Warehouse doc fetches in auto reorder job
get_item_warehouse_projected_qty ran an uncached frappe.get_doc per Bin
row to walk the warehouse parent chain, re-fetching the same ancestors
for every item sharing a warehouse. Preload the warehouse parent map
once and walk it in memory instead.
2026-07-07 11:43:52 +05:30
pandiyan
015fa68fc0 fix: make trend report based-on and group-by column labels translatable
based_wise_columns_query() and group_wise_column() in trends.py built
column labels as raw strings, so "Item", "Item Name", "Customer",
"Supplier", "Territory", "Currency", etc. never went through _() and
stayed in English regardless of the user's language, unlike the
period and total columns right next to them which were already
wrapped correctly.
2026-07-06 18:41:06 +05:30
S Sakthivel Murugan
b72ecdda0d test: add regression test for trends chart total row 2026-07-06 17:54:25 +05:30
S Sakthivel Murugan
e6f9149ad7 fix: use company currency instead of global default in report 2026-07-06 17:54:25 +05:30
ljain112
0691c7c7bc refactor: move functionality in postprocess for mapped doc 2026-07-06 17:41:56 +05:30
rohitwaghchaure
607f0e943f fix: workspace for stock and manufacturing (#56906) 2026-07-06 11:48:15 +00:00
MochaMind
be93530b5f fix: sync translations from crowdin (#56866) 2026-07-06 10:49:55 +02:00
pandiyan
e718a70b26 test: cover variant item_code/item_name rename on abbreviation change
Add regression coverage for the new abbreviation-rename propagation:
a simple item_code rename, item_name derived from a template whose
item_name differs from its item_code, and a manually customized
item_name getting rebuilt rather than left stale.
2026-07-06 14:15:40 +05:30
pandiyan
c0cfe5f363 fix: rename variant item_code/item_name when attribute abbreviation changes
Item Attribute abbreviations only got baked into a variant's item_code
and item_name at creation time (make_variant_item_code returns early
once item_code is set). Renaming an abbreviation afterwards left every
existing variant stuck with the stale code, silently out of sync with
its own attribute.

Detect abbreviation renames on Item Attribute save, find every variant
using the affected value, and rebuild+rename its item_code via
frappe.rename_doc so linked records follow along. item_name is rebuilt
in lockstep from the template's item_name, even if it had since been
customized, since both fields are meant to be derived from the same
abbreviation.
2026-07-06 14:15:29 +05:30
S Sakthivel Murugan
c7774a95e5 fix(asset): allow asset repair creation for fully depreciated assets 2026-07-06 14:13:06 +05:30
MochaMind
0688cedba2 chore: update POT file (#56899) 2026-07-05 20:29:59 +02:00
Mohsin Akhtar
41c00de4d3 Merge branch 'develop' into fix-variant-of-filter-item-list 2026-07-05 23:27:46 +05:30
Mohsin Akhtar
54da9fc27a fix: update modified timestamp in item.json 2026-07-05 23:26:06 +05:30
Mihir Kandoi
8aeca5922c Merge pull request #56905 from mihir-kandoi/pg-lock-races-and-advisory-valuation
fix(stock): close postgres locking races; gate batch valuation with a txn advisory lock
2026-07-05 22:42:18 +05:30
Mihir Kandoi
2ec469257e perf(stock): gate batch valuation with a txn advisory lock on postgres
On postgres, outward batch valuation row-locked the item's ENTIRE SLE / Serial and Batch Entry history (a separate plain SELECT ... FOR UPDATE per site, since FOR UPDATE is invalid with GROUP BY there). That writes a lock marker (xmax + WAL) on every historical tuple per outward movement - write amplification that grows with history forever - and locks nothing at all when the history is empty (negative-stock edge: two concurrent outwards don't serialize).

Replace all four history-wide postgres lock statements with one frappe.db.transaction_advisory_lock(("batch-valuation", item_code, warehouse)) at the top of BatchNoValuation.calculate_avg_rate's outward branch - every valuation read (bundle + the three deprecated paths) is downstream of it. Released at commit/rollback, participates in deadlock detection, and serializes regardless of history size, so the empty-history edge is closed by construction. Batch qty updates iterate in sorted order so concurrent vouchers lock Batch rows in the same sequence.

MariaDB is unchanged: it keeps the original grouped FOR UPDATE row locks (and its gap locks). Requires frappe#40621.

Test: outward delivery of a batched item must leave the xact advisory lock visible in pg_locks for the submitting transaction.
2026-07-05 22:31:45 +05:30
Mihir Kandoi
db58858c68 fix(stock): close postgres lock-then-read races in pick list and stock reservation
Postgres has no gap locks, so the lock-then-read pattern (plain SELECT ... FOR UPDATE before a grouped read) only serializes on rows that already exist. Two sites had reachable races where the lock set is empty or disjoint:

- Pick list: two pick lists against the same SO item submitted concurrently lock only docstatus=1 rows, so with no previously-submitted picks their lock sets are disjoint and both pass validate_picked_qty (over-pick; picked_qty last-writer-wins). Gate on the referenced Sales Order Item / Packed Item rows, which always exist.
- Stock reservation: the first concurrent reservations for an (item, warehouse) find no SRE rows to lock, so both pass and reserved qty can exceed actual. Gate on the Bin row, which exists once there is stock.

MariaDB is unchanged (its gap locks already serialize both; the gates are postgres-only). Also: ORDER BY on the small-set postgres lock selects for deterministic lock order, and the repost pre-lock in get_future_stock_vouchers selects a constant instead of shipping every matching SLE name to the client.
2026-07-05 22:31:29 +05:30
Nabin Hait
8abcb7decc Merge pull request #56301 from frappe/chore/payment-entry-purchase-coverage
test: purchase-side Payment Entry allocation coverage
2026-07-05 19:39:38 +05:30
Mihir Kandoi
c68bf89924 Merge pull request #56903 from mihir-kandoi/pg-second-order-groupby-docs
docs: catalog second-order GROUP BY traps (wrap-is-the-bug classes)
2026-07-05 19:11:14 +05:30
Mihir Kandoi
0922d85bf0 docs: catalog second-order GROUP BY traps (wrap-is-the-bug classes)
A full audit of the loose-GROUP-BY fixes found four recurring mistakes in the fixes themselves: incoherent Max/Min pairs over coupled columns, NULL-skipping Max on discriminators, Sum(x)*Max(y) fabricated arithmetic, and wrong-bound picks. Add them to the compatibility catalog (new 3.1) and to the Greptile review instructions so future PRs get flagged.
2026-07-05 19:00:47 +05:30
Mohd Haris
48418eadb0 fix(budget-variance): correct month shift in comparison chart
The Budget Variance Report chart plotted the actual expense one month
earlier than the table (e.g. July actual shown under June).

build_comparison_chart_data() collected budget columns using
fieldname.startswith("budget_"). The dimension column "budget_against"
also matches that prefix, so it was added as an extra leading entry to
budget_fields and labels, while actual_fields had no such leading entry.
This shifted every actual value one position ahead of its label.

Skip the "budget_against" dimension column so budget/actual values and
labels stay aligned per month.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:06:43 +05:30
rohitwaghchaure
7844905e21 fix: dark theme shop floor (#56887) 2026-07-05 16:33:08 +05:30
Mihir Kandoi
00e6b44701 Merge pull request #56897 from mihir-kandoi/pg-requested-items-required-date
fix(buying): show earliest schedule date as required date
2026-07-05 15:45:52 +05:30
Mihir Kandoi
e3a38e3733 Merge pull request #56896 from mihir-kandoi/pg-production-planning-arrival-qty
fix(manufacturing): scope Production Planning arrival qty to open POs
2026-07-05 15:43:04 +05:30
Mihir Kandoi
1d7bb19620 Merge pull request #56898 from mihir-kandoi/pg-trends-empty-row-guard
fix(controllers): guard empty group-by detail row in trends
2026-07-05 15:39:53 +05:30
Mihir Kandoi
8486578c05 Merge pull request #56894 from mihir-kandoi/pg-procurement-tracker-coherent-row
fix(buying): make Procurement Tracker rows coherent PO lines
2026-07-05 15:39:33 +05:30
Mihir Kandoi
df82c7ac1c Merge pull request #56893 from mihir-kandoi/pg-bom-groupby-phantom-pair
fix(manufacturing): keep bom_no/is_phantom_item pair coherent in get_bom_items_as_dict
2026-07-05 15:38:29 +05:30
Mihir Kandoi
4cb622b8e3 Merge pull request #56895 from mihir-kandoi/pg-budget-requested-amount-per-row
fix(controllers): compute budget requested amount per row
2026-07-05 15:37:56 +05:30
Mihir Kandoi
a5d0b25ac4 test: assert earliest schedule date wins for duplicate MR item rows 2026-07-05 15:32:02 +05:30
Mihir Kandoi
2a6bc517bc fix: exclude fully received PO lines from arrival qty and date 2026-07-05 15:29:18 +05:30
Mihir Kandoi
5d56a04a84 fix(controllers): guard empty group-by detail row in trends
With an explicit GROUP BY, a zero-match detail query returns no rows (the old bare aggregate returned one all-NULL row), so row1[0][0] would raise IndexError. Skip the entry instead.
2026-07-05 15:10:32 +05:30
Mihir Kandoi
b5aee6a9cd fix(buying): show earliest schedule date as required date
When a Material Request lists the same item on multiple rows, the consolidated row showed Max(schedule_date), understating urgency. Use Min - the earliest date the item is needed.
2026-07-05 15:10:17 +05:30
Mihir Kandoi
efd777a1a8 fix(manufacturing): scope Production Planning arrival qty to open POs
arrival_qty summed the all-time ordered qty of every submitted PO line for the item+warehouse, so it grew monotonically with purchase history. Sum the pending qty (qty - received_qty) on open POs instead, and take the earliest schedule date from the same scope.
2026-07-05 15:10:03 +05:30
Mihir Kandoi
e52b1d6cff fix(controllers): compute budget requested amount per row
get_requested_amount multiplied the pooled pending qty of all matching Material Request items by a single Max(rate), fabricating the total whenever rates differ and biasing it upward - making false Budget Exceeded stops more likely. Sum (stock_qty - ordered_qty) * rate per row instead, matching get_ordered_amount.
2026-07-05 15:09:49 +05:30
Mihir Kandoi
39d2a62692 fix(buying): make Procurement Tracker rows coherent PO lines
get_po_entries aggregated every non-key column with Max() over (PO, material_request_item), which could stitch values from different PO lines into a row that never existed (one line's item_code with another's qty and amount). Select one representative line per group instead: a subquery picks Min(child.name) per group under the same filters and the outer query reads all columns bare from that line. Row count is unchanged.
2026-07-05 15:09:21 +05:30
Mihir Kandoi
db5ab9fb35 fix(manufacturing): keep bom_no/is_phantom_item pair coherent in get_bom_items_as_dict
When a BOM lists the same item twice (one line phantom via sub-BOM P, one non-phantom via sub-BOM N), grouping by item_code and aggregating bom_no and is_phantom_item with independent Max() could pair one line's phantom flag with the other line's bom_no, exploding the wrong sub-BOM or silently dropping a direct requirement. Group by the pair instead so each line keeps a coherent (bom_no, is_phantom_item); the consumer accumulates duplicate keys and explodes phantom rows with their own qty. Same fix as 41da9eb7fc, which missed this single-level path.
2026-07-05 15:08:58 +05:30
Nabin Hait
17851ceaae Merge pull request #56877 from frappe/chore/test-appointment-booking-settings
test: add coverage for Appointment Booking Settings
2026-07-05 13:16:15 +05:30
Nabin Hait
d2f0ec883e Merge pull request #56880 from frappe/chore/test-email-campaign
test: add coverage for Email Campaign
2026-07-05 13:16:06 +05:30
Nabin Hait
86435bc961 Merge pull request #56891 from frappe/fix/company-coa-test-abbr-collision
test: fix flaky test_coa_based_on_country_template (abbreviation collision)
2026-07-05 12:57:43 +05:30
Nabin Hait
b418df595e Merge branch 'develop' into chore/test-appointment-booking-settings 2026-07-05 12:50:53 +05:30
Nabin Hait
70e1acdb6d Merge branch 'develop' into chore/test-email-campaign 2026-07-05 12:50:47 +05:30
Nabin Hait
2959bfcafa Merge pull request #56888 from frappe/chore/test-lower-deduction-certificate
test: add coverage for Lower Deduction Certificate
2026-07-05 12:50:05 +05:30
Nabin Hait
94afc2a05b Merge pull request #56889 from frappe/chore/test-italy-utils
test: add coverage for Italy e-invoice utility helpers
2026-07-05 12:49:54 +05:30
Nabin Hait
e82a9b9c3b Merge pull request #56890 from frappe/chore/test-import-supplier-invoice
test: add coverage for Import Supplier Invoice
2026-07-05 12:49:31 +05:30
Nabin Hait
df4fc6b186 test: use a unique company abbreviation to fix COA-template flake 2026-07-05 12:44:31 +05:30
Nabin Hait
e86f6fc89c Merge pull request #56849 from frappe/chore/test-stock-closing-entry
fix: Stock Closing Entry duplicate check misses contained date ranges
2026-07-05 12:18:35 +05:30
Nabin Hait
11023511ae Merge pull request #56856 from frappe/chore/test-job-card-dark-paths
test: cover Job Card quantity, docstatus and capacity logic
2026-07-05 12:18:23 +05:30
Nabin Hait
58ee02780f Merge pull request #56883 from nabinhait/sherlock/fix-dunning-outstanding-currency
fix: use transaction-currency outstanding on Dunning for foreign-currency invoices
2026-07-05 12:17:27 +05:30
Nabin Hait
8d289dfe52 Merge pull request #56881 from frappe/chore/test-campaign
fix: Campaign with a naming series breaks the UTM Campaign link
2026-07-05 12:16:50 +05:30
Nabin Hait
616ceb8126 test: add coverage for Import Supplier Invoice validation and country lookup 2026-07-05 12:16:09 +05:30
Nabin Hait
f2f1b2597d test: add coverage for Italy e-invoice utility helpers 2026-07-05 12:13:48 +05:30
Nabin Hait
c293cb8871 test: add coverage for Lower Deduction Certificate date validation 2026-07-05 12:11:34 +05:30
rohitwaghchaure
29be72fae4 fix: warning message for new item standard cost (#56885) 2026-07-04 23:29:32 +05:30
Nabin Hait
a9bb6b31df fix: use transaction-currency outstanding on Dunning for foreign-currency invoices
When a Sales Invoice is in a foreign currency (e.g. USD) but the receivable
account is in the company currency (e.g. INR), `outstanding_amount` on the
invoice is stored in the party account currency (INR). `postprocess_dunning`
was copying that value directly into the Dunning's Overdue Payment row, which
is expected to carry the transaction-currency (USD) amount.

The fix: when `party_account_currency != currency`, use
`payment_schedule[0].outstanding` (already maintained in transaction currency)
instead of `outstanding_amount`.

Closes #56006
2026-07-04 19:45:44 +05:30
Nabin Hait
2b1e922183 fix: don't hijack another Campaign's UTM mirror when display names collide 2026-07-04 19:42:13 +05:30
Nabin Hait
486a1c78b0 Merge pull request #56882 from frappe/fix/pos-invoice-reset-mop-attributeerror
fix: reset_mode_of_payments raises AttributeError on a POS Invoice
2026-07-04 19:37:40 +05:30
Nabin Hait
ca9dcbf2d7 fix: reuse the existing UTM Campaign mirror when campaign_name is edited 2026-07-04 18:45:58 +05:30
Nabin Hait
c969ee7bef test: use separate documents for the invalid and valid slot cases 2026-07-04 18:44:04 +05:30
Nabin Hait
a1f6ae56ff fix: removed unused import
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-04 18:39:52 +05:30
Nabin Hait
14c1b02025 Merge pull request #56879 from frappe/chore/test-contract-template
test: add coverage for Contract Template
2026-07-04 17:58:32 +05:30
Nabin Hait
3b335db64c Merge pull request #56878 from frappe/chore/test-crm-settings
test: add coverage for CRM Settings
2026-07-04 17:58:18 +05:30
Nabin Hait
99ed620dad fix: reset_mode_of_payments raises AttributeError on POS Invoice 2026-07-04 17:54:46 +05:30
Nabin Hait
e5b7c3f98c test: cover Job Card quantity, docstatus and capacity-overlap logic 2026-07-04 17:51:52 +05:30
Nabin Hait
10c6cda6db fix: detect contained/enclosing date ranges in Stock Closing Entry duplicate check 2026-07-04 17:47:22 +05:30
Nabin Hait
4b4afe12df Merge pull request #56832 from frappe/chore/test-repost-payment-ledger
test: add coverage for Repost Payment Ledger
2026-07-04 17:45:10 +05:30
Nabin Hait
9fdcfd5f58 fix: link UTM Campaign to the Campaign's document name, not campaign_name 2026-07-04 17:40:16 +05:30
Nabin Hait
5eaafd3025 test: add coverage for Campaign naming and UTM mirroring 2026-07-04 17:33:35 +05:30
Nabin Hait
79c359ef5d test: add coverage for Email Campaign date and recipient validation 2026-07-04 17:30:46 +05:30
Nabin Hait
34f3870f2a test: add coverage for Contract Template validation and rendering 2026-07-04 17:28:28 +05:30
Nabin Hait
7f903b63dd test: add coverage for CRM Settings sync and contact-us guards 2026-07-04 17:26:58 +05:30
Nabin Hait
1a2e3e03b3 test: add coverage for Appointment Booking Settings slot validation 2026-07-04 17:25:20 +05:30
Nabin Hait
4c26ec8cd9 test: make add_manually test distinguish manual mode from auto-loading 2026-07-04 16:48:52 +05:30
Nabin Hait
f68f53dec0 test: cover on-cutoff boundary in voucher loading 2026-07-04 16:48:52 +05:30
Nabin Hait
9901746e02 test: add coverage for Repost Payment Ledger 2026-07-04 16:48:52 +05:30
Nabin Hait
ae0cfbd3e1 test: assert the saved closing entry exists rather than a truthy name 2026-07-04 16:48:51 +05:30
Nabin Hait
e37ceb5f69 test: cover Stock Closing Entry duplicate date-range validation 2026-07-04 16:48:51 +05:30
Nabin Hait
807c52e32b Merge pull request #56855 from frappe/chore/test-stock-reservation-entry-dark-paths
test: cover Stock Reservation Entry validations and helper
2026-07-04 16:43:55 +05:30
Nabin Hait
38b91a2800 Merge pull request #56846 from frappe/chore/test-bisect-accounting-statements
test: add coverage for Bisect Accounting Statements
2026-07-04 16:43:28 +05:30
Nabin Hait
0f3b77a344 Merge pull request #56841 from frappe/chore/test-exchange-rate-revaluation
test: cover Exchange Rate Revaluation validation and gain/loss paths
2026-07-04 16:42:22 +05:30
ruthra kumar
66e70e312d Merge pull request #56852 from ruthra-kumar/race_condition_in_process_pcv
fix: race condition in process pcv
2026-07-04 16:22:16 +05:30
Mihir Kandoi
ac57e389a4 Merge pull request #56871 from mihir-kandoi/cast-error-postgres
fix: type cast error on postgres
2026-07-04 14:24:20 +05:30
Mihir Kandoi
093bbb07a7 fix: type cast error on postgres 2026-07-04 14:11:29 +05:30
Mihir Kandoi
57e7ceae24 Merge pull request #56859 from aerele/fix/item-form-permission-errors
Fix(stock): item form permission errors
2026-07-04 14:07:54 +05:30
rohitwaghchaure
8093e44746 feat: shop floor interface for operators (#55551)
* feat: shop floor interface for operators

* fix: documentation

* fix: UI/UX for shop floor

* fix: shop floor query and OEE edge cases from review

- Push the draft / To Manufacture condition into the Job Card query
  (or_filters) so a busy workstation's submitted history cannot fill
  the row limit and hide active drafts
- Clamp the OEE quality factor at zero when process loss exceeds
  completed qty

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 13:31:37 +05:30
Nabin Hait
55c27d3dde Merge pull request #56853 from frappe/chore/test-payment-entry-dark-paths
test: cover untested Payment Entry field validations
2026-07-04 11:52:16 +05:30
Nabin Hait
b85776c00b Merge pull request #56843 from frappe/chore/test-share-transfer
test: cover Share Transfer consistency validations
2026-07-03 23:44:40 +05:30
Nabin Hait
0f7ee3a843 Merge pull request #56844 from frappe/chore/test-process-statement-of-accounts
test: cover Process Statement Of Accounts validation
2026-07-03 23:44:29 +05:30
Nabin Hait
e546132ac3 Merge pull request #56845 from frappe/chore/test-chart-of-accounts-importer
test: add coverage for Chart of Accounts Importer parsing
2026-07-03 23:44:20 +05:30
Nabin Hait
24a13d16bb Merge pull request #56847 from frappe/chore/test-asset-capitalization
test: cover Asset Capitalization row validations
2026-07-03 23:43:52 +05:30
Nabin Hait
17e7f91690 Merge pull request #56850 from frappe/chore/test-packing-slip
test: cover Packing Slip package-number and item validations
2026-07-03 23:43:43 +05:30
Nabin Hait
ef7fb1084d Merge pull request #56854 from frappe/chore/test-serial-batch-bundle-dark-paths
test: cover Serial and Batch Bundle helpers and validations
2026-07-03 23:43:04 +05:30
Nabin Hait
18d16fa5cf Merge pull request #56851 from frappe/chore/test-email-digest
test: cover Email Digest date-window calculations
2026-07-03 23:42:25 +05:30
Nikhil Kothari
dc09362454 fix: replace all old icons (#56864) 2026-07-03 23:03:28 +05:30
Nabin Hait
2000a9db36 Merge pull request #56842 from frappe/chore/test-bank-reconciliation-tool
test: cover Bank Reconciliation Tool date filter and message helper
2026-07-03 22:08:23 +05:30
Nabin Hait
dd35d977f7 Merge pull request #56829 from frappe/chore/test-process-subscription
test: add coverage for Process Subscription
2026-07-03 22:06:19 +05:30
Mihir Kandoi
4545dd939a Merge pull request #56837 from aerele/fix/manufacture-stock-entry-cost-center-default
fix: remove company default on cost center in stock entry detail
2026-07-03 21:11:52 +05:30
rohitwaghchaure
7b0c35caaf fix: auto fetch serial no from previous operation output (#56445)
* fix: auto fetch serial no from previous operation output

* fix: order by

* fix: warehouse for operations
2026-07-03 20:23:33 +05:30
rohitwaghchaure
341a07dffa fix: restrict state-changing whitelisted endpoints to POST (#56858)
Add methods=["POST"] to 50 whitelisted functions that create or modify
documents (get_doc followed by insert/save/submit), so they can no
longer be invoked via GET requests.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 18:47:13 +05:30
pandiyan
ef794f390c fix: skip item prices tab render for users without item price read access 2026-07-03 18:25:08 +05:30
pandiyan
8c7b2f4d3c fix: clear stray permission message when item dashboard has no warehouse access 2026-07-03 18:25:01 +05:30
rohitwaghchaure
9c911438f1 fix: do not rebook standard cost variance on non-update-stock purchase invoice (#56799) 2026-07-03 17:17:38 +05:30
ruthra kumar
a9ffdac806 chore: linter fix 2026-07-03 17:16:06 +05:30
ruthra kumar
dbc409736a refactor(test): row name based utility methods 2026-07-03 17:16:03 +05:30
Nabin Hait
008742bdbe test: exercise every mandatory field in the Stock Reservation Entry check 2026-07-03 17:08:27 +05:30
Nabin Hait
ed77392741 test: also accept a mid-range rounding loss allowance 2026-07-03 17:07:31 +05:30
Nabin Hait
3240411876 test: drop unused timedelta import 2026-07-03 17:05:53 +05:30
Nabin Hait
8f96e5f2aa test: replace lambda with nested def (ruff E731) 2026-07-03 17:03:51 +05:30
Nabin Hait
7b2f38cd6f test: cover Stock Reservation Entry validations and helper 2026-07-03 16:23:52 +05:30
Nabin Hait
8456e88d93 test: cover Serial and Batch Bundle helpers and in-memory validations 2026-07-03 16:20:41 +05:30
ruthra kumar
21f4603144 refactor: prevent whole table scan while scheduling next date
- helps in concurrency isolation
2026-07-03 16:19:35 +05:30
Nabin Hait
e0ea8eee1a test: cover untested Payment Entry field validations 2026-07-03 16:15:24 +05:30
Nabin Hait
c5ab9958ff test: cover Email Digest date-window calculations 2026-07-03 16:01:10 +05:30
Nabin Hait
113d914b9c test: cover Packing Slip package-number and item validations 2026-07-03 15:59:13 +05:30
Nabin Hait
ebd8547629 test: cover Asset Capitalization row validations 2026-07-03 15:54:17 +05:30
ruthra kumar
7e4045e828 fix: prevent repeatable read related concurrency errors
Process Period Closing Voucher and Process Period Closing Voucher
Details are trackers how the jobs are processed. Keep transactions on
them very short.
2026-07-03 15:28:28 +05:30
ruthra kumar
ff6881764b fix: race condition and repeatable read in process pcv
- Update using child table name to avoid scanning whole table, which
eventually leads to mariadb 1020 (REPEATABLE READ).
 - Avoid race condition in final summarization
2026-07-03 15:28:26 +05:30
Nabin Hait
28367f75e9 test: add coverage for Bisect Accounting Statements bisection 2026-07-03 14:57:47 +05:30
Nabin Hait
740c5a07ff test: add coverage for Chart of Accounts Importer parsing 2026-07-03 14:55:31 +05:30
Nabin Hait
3dfb3f385b test: cover Process Statement Of Accounts validation defaults 2026-07-03 14:53:22 +05:30
Nabin Hait
dae90e90df test: cover Share Transfer consistency validations 2026-07-03 14:50:52 +05:30
Nabin Hait
d1d592cf0c test: cover bank reconciliation date filter and auto-reconcile message 2026-07-03 14:48:21 +05:30
Nabin Hait
65500d5102 test: cover Exchange Rate Revaluation validation and gain/loss paths 2026-07-03 14:45:06 +05:30
Nabin Hait
23e3dd94c0 Merge pull request #56831 from frappe/chore/test-process-payment-reconciliation
fix: Process Payment Reconciliation drops bank/cash and cost center filters
2026-07-03 14:31:17 +05:30
Nabin Hait
6255d99fda Merge pull request #56827 from frappe/chore/test-subscription-plan
fix: Subscription Plan Monthly Rate under-bills across a year boundary
2026-07-03 14:30:25 +05:30
Nabin Hait
81d5eac0ea Merge pull request #56824 from frappe/chore/test-journal-entry-template
fix: Journal Entry Template rows must belong to its company
2026-07-03 14:29:28 +05:30
Nabin Hait
0b20438da9 test: mirror subscription test setup for known settings 2026-07-03 14:24:58 +05:30
Nabin Hait
8632019d2a Merge pull request #56830 from frappe/chore/test-account-closing-balance
test: add coverage for Account Closing Balance
2026-07-03 14:22:40 +05:30
Nabin Hait
c9960b4d51 fix: carry bank/cash account and cost center into Payment Reconciliation 2026-07-03 14:16:46 +05:30
Nabin Hait
2cc02e61d9 fix: validate Journal Entry Template rows belong to its company 2026-07-03 14:15:19 +05:30
Nabin Hait
6fc28edde9 Merge pull request #56828 from frappe/chore/test-cashier-closing
test: add coverage for Cashier Closing
2026-07-03 14:08:55 +05:30
Nabin Hait
196730c535 fix: bill all months across a year boundary in Monthly Rate plans 2026-07-03 14:08:20 +05:30
Nabin Hait
4d39f698bd Merge pull request #56823 from frappe/chore/test-party-link
test: add coverage for Party Link
2026-07-03 14:07:04 +05:30
Nabin Hait
0a7abe7144 Merge pull request #56822 from frappe/chore/test-mode-of-payment
test: add coverage for Mode of Payment
2026-07-03 14:06:50 +05:30
pandiyan
a168bb7ea4 test: cover cost center fallback to item group default in manufacture entry
the existing test_cost_center_for_manufacture only checks a raw material
row against an item-level override, which is set independently of the
":company" default guard and never exercised the bug.
2026-07-03 14:06:22 +05:30
pandiyan
edfa0a7a1d fix: remove company default on cost center in stock entry detail
the ":company" default pre-filled every row before set_default_cost_center()
ran, so its "if not row.cost_center" guard was always false and the
project/item group/brand priority chain in get_default_cost_center()
never ran.
2026-07-03 14:06:22 +05:30
Nabin Hait
5888cdf3a0 Merge pull request #56821 from frappe/chore/test-item-tax-template
test: add coverage for Item Tax Template
2026-07-03 14:04:03 +05:30
Nabin Hait
a1f413e8a8 Merge pull request #56820 from frappe/chore/test-monthly-distribution
test: add coverage for Monthly Distribution
2026-07-03 14:03:36 +05:30
Nabin Hait
cd167bdd40 Merge pull request #56819 from frappe/chore/test-bank-guarantee
test: add coverage for Bank Guarantee
2026-07-03 14:02:53 +05:30
Mihir Kandoi
5d48c44bbb Merge pull request #56826 from frappe/fix/stock-ledger-invariant-check-report
fix: FIFO queue checks and incorrect entries filter in stock ledger reports
2026-07-03 13:19:29 +05:30
rohitwaghchaure
ecc8ec672b fix: replay immutable SLE qty for serial/batch bundle valuation (#56814) 2026-07-03 12:15:07 +05:30
Mihir Kandoi
3b1e57966e test: drop redundant cleanup, db rolls back after each test 2026-07-03 12:12:46 +05:30
Nabin Hait
974571aba7 test: guard account lookups and cover dropped pr_instance filters 2026-07-03 12:08:11 +05:30
Nabin Hait
7d917e497a test: assert account-currency sums carry through the merge 2026-07-03 12:06:56 +05:30
Nabin Hait
e041e33860 test: reload invoice for outstanding and cover equal-time boundary 2026-07-03 12:06:20 +05:30
Nabin Hait
832b5a56bf test: lock current cross-year monthly-rate underbilling value 2026-07-03 12:05:31 +05:30
Nabin Hait
abded56174 test: guard account lookup and lock missing company-check behaviour 2026-07-03 12:04:42 +05:30
Nabin Hait
6f866545b9 test: complete supplier-primary assertions and lock uniqueness gap 2026-07-03 12:03:56 +05:30
Nabin Hait
147e1539dc test: guard account lookup and lock dead POS guard behaviour 2026-07-03 12:02:50 +05:30
Nabin Hait
f58ea8e17d test: guard account lookup and lock current tax-rate behaviour 2026-07-03 12:01:47 +05:30
Nabin Hait
9980d47524 test: lock current end-date behaviour and assert persisted state 2026-07-03 12:00:45 +05:30
Nabin Hait
97794b7ded test: add coverage for Process Payment Reconciliation 2026-07-03 11:41:21 +05:30
Mihir Kandoi
ef5f47fafd fix: address review comments
- restore mutated SLE after test via addCleanup
- explicit return False in has_difference
- comment the fifo_stock_diff guard for non-queue predecessors
2026-07-03 11:39:51 +05:30
Nabin Hait
c51edbd88e test: add coverage for Account Closing Balance 2026-07-03 11:39:50 +05:30
Nabin Hait
745f657a0e test: add coverage for Process Subscription 2026-07-03 11:37:02 +05:30
Nabin Hait
5c87e2e398 test: add coverage for Cashier Closing 2026-07-03 11:34:27 +05:30
Nabin Hait
3167e8ba77 test: add coverage for Subscription Plan 2026-07-03 11:31:30 +05:30
Mihir Kandoi
94ab09e4a3 fix: FIFO queue checks and incorrect entries filter in stock ledger reports
- 'Show Incorrect Entries' always returned an empty result (regression
  from #43619); now returns entries from one row before the first
  incorrect one
- FIFO queue columns were computed for serialized/batched SLEs that
  don't maintain a stock queue, showing false differences; left empty
  for such rows
- compare value/valuation differences at currency precision, qty at
  float precision
2026-07-03 11:29:44 +05:30
Nabin Hait
83d821d8c4 test: add coverage for Journal Entry Template 2026-07-03 11:28:56 +05:30
Nabin Hait
22dc51a57a test: add coverage for Party Link 2026-07-03 11:25:32 +05:30
Nabin Hait
df54382727 test: add coverage for Mode of Payment 2026-07-03 11:23:08 +05:30
Nabin Hait
3e9843059e test: add coverage for Item Tax Template 2026-07-03 11:19:56 +05:30
Nabin Hait
ccd2aae481 test: add coverage for Monthly Distribution 2026-07-03 11:18:11 +05:30
Nabin Hait
41000ea109 test: add coverage for Bank Guarantee 2026-07-03 11:14:54 +05:30
Khushi Rawat
344f58b98a Merge pull request #56811 from khushi8112/fix/letterhead-footer-print-formats
fix: render letter head footer in print formats
2026-07-03 02:43:26 +05:30
Khushi Rawat
c9145c5ece Merge branch 'develop' into fix/letterhead-footer-print-formats 2026-07-03 02:27:58 +05:30
khushi8112
2d0c0a8c09 fix: add page numbers to print format footer 2026-07-03 02:26:52 +05:30
khushi8112
e60a467972 fix: render letter head footer in print formats 2026-07-03 02:16:59 +05:30
Nabin Hait
e3e3d97a72 Merge pull request #56809 from frappe/chore/test-custom-financial-statement
test: Custom Financial Statement report coverage
2026-07-03 00:33:08 +05:30
Nabin Hait
42c6768b4c test: guard period_keys index access for clearer failure 2026-07-03 00:16:06 +05:30
Nabin Hait
2e13691ffa Merge pull request #56808 from frappe/chore/test-dimension-wise-accounts-balance
test: Dimension-wise Accounts Balance report coverage
2026-07-03 00:14:30 +05:30
Nabin Hait
ae80d29dcf Merge pull request #56792 from frappe/chore/test-timesheet-billing-summary
test: Timesheet Billing Summary report coverage
2026-07-03 00:13:39 +05:30
Nabin Hait
a33f7ead24 Merge pull request #56791 from frappe/chore/test-project-summary
test: Project Summary report coverage
2026-07-03 00:13:28 +05:30
Nabin Hait
817ecaa92f Merge pull request #56790 from frappe/chore/test-lead-owner-efficiency
test: Lead Owner Efficiency report coverage
2026-07-03 00:12:44 +05:30
Nabin Hait
8fd98ccbe2 Merge pull request #56789 from frappe/chore/test-supplier-quotation-comparison
test: Supplier Quotation Comparison report coverage
2026-07-03 00:12:34 +05:30
Nabin Hait
bc82197bd1 Merge pull request #56807 from frappe/chore/test-accounts-payable-summary
test: Accounts Payable Summary report coverage
2026-07-02 23:35:22 +05:30
Nabin Hait
9c53a91b82 test: add simulate=True to draft timesheet for overlap safety 2026-07-02 23:32:02 +05:30
Nabin Hait
21a9f2754e test: unpack report_summary tuple and key labels via _() 2026-07-02 23:31:13 +05:30
Nabin Hait
f0434cadd4 test: guard against missing owner row before subscripting 2026-07-02 23:30:34 +05:30
Nabin Hait
0ba43a17c1 test: strengthen price_per_unit assertion, drop no-op quotation guard 2026-07-02 23:29:48 +05:30
Nabin Hait
4feaacc649 test: Custom Financial Statement report coverage 2026-07-02 23:26:38 +05:30
Nabin Hait
7034dc71e7 test: Dimension-wise Accounts Balance report coverage 2026-07-02 23:24:12 +05:30
Nabin Hait
ed72732bb2 test: Accounts Payable Summary report coverage 2026-07-02 23:21:36 +05:30
Nabin Hait
b12725c4b2 Merge pull request #56778 from frappe/chore/test-quotation-trends
test: Quotation Trends report coverage
2026-07-02 23:07:02 +05:30
Nabin Hait
20928bd600 Merge pull request #56796 from frappe/chore/fix-flaky-bom-cost-valuation-reset
test: fix flaky test_update_bom_cost_in_all_boms via valuation reset
2026-07-02 23:05:22 +05:30
Nabin Hait
15862566a8 Merge pull request #56784 from frappe/chore/test-territory-wise-sales
test: Territory-wise Sales report coverage
2026-07-02 23:04:22 +05:30
Nabin Hait
e446f54f2e Merge pull request #56787 from frappe/chore/test-purchase-analytics
test: Purchase Analytics report coverage
2026-07-02 23:04:10 +05:30
Nabin Hait
cf9f16a921 Merge pull request #56788 from frappe/chore/test-subcontract-order-summary
test: Subcontract Order Summary report coverage
2026-07-02 23:03:57 +05:30
Nabin Hait
6cffa0faeb Merge pull request #56783 from frappe/chore/test-sales-person-wise-transaction-summary
fix: correct filter handling in Sales Person-wise Transaction Summary + tests
2026-07-02 22:58:48 +05:30
Nabin Hait
a075437db7 Merge pull request #56780 from frappe/chore/test-customer-wise-item-price
test: Customer-wise Item Price report coverage
2026-07-02 22:57:19 +05:30
Nabin Hait
9f4914e08f Merge pull request #56781 from frappe/chore/test-sales-person-commission-summary
test: Sales Person Commission Summary report coverage
2026-07-02 22:56:20 +05:30
Mihir Kandoi
c9d712fa49 Merge pull request #56800 from aerele/fix/wo-status-partial-pick
fix(manufacturing): update work order status on partial pick-list transfer
2026-07-02 21:44:46 +05:30
Mihir Kandoi
3345336a5c Merge pull request #56798 from aerele/fix/sre-auto-reserve
fix: skip stock reservation for opted-out production plans
2026-07-02 21:42:54 +05:30
MochaMind
ceadc4f269 fix: sync translations from crowdin (#56673) 2026-07-02 17:08:31 +02:00
Shllokkk
caa4358057 fix: guard against missing DocType in onboarding steps patch (#56804) 2026-07-02 19:33:16 +05:30
Nabin Hait
36f56fa1c3 Merge pull request #56786 from frappe/chore/test-territory-target-variance
test: Territory Target Variance based on Item Group report coverage
2026-07-02 18:09:21 +05:30
Raffael Meyer
5b738b7b0d fix: don't attempt to create SABB for non-serialized / non-batch items (#56627)
* fix: don't attempt to create SABB for non-serialized / non-batch items

* fix(stock): skip serial batch lookup for rows without item code
2026-07-02 12:33:26 +00:00
Sudharsanan11
f85f6be3cf test(manufacturing): add test to validate the work order status on partial pick-list transfer
Cover the pick-list flow where a stock entry moves only one of the work
order's required items: material_transferred_for_manufacturing stays 0 (min
fraction) while the status must move to "in process".
2026-07-02 17:39:33 +05:30
Sudharsanan11
6591ae195d fix(manufacturing): update work order status on partial pick-list transfer
A stock entry created from a pick list has fg_completed_qty=0, so
material_transferred_for_manufacturing is derived from the min-fraction of
item-level transfers. When a pick list moves only some required items, the
un-picked item stays at 0, which zeroes the aggregate and leaves the work
order status at "not started" even though material is already in wip.

Promote the status to "in process" when any raw material has been transferred
via a pick list. material_transferred_for_manufacturing stays min-fraction
based (0 correctly means no full finished good can be started yet).
2026-07-02 17:38:14 +05:30
Shllokkk
7229957107 Merge pull request #56747 from Shllokkk/create-payment-entries-from-payable-report
fix: surface create payment entries as primary action on row selection
2026-07-02 17:33:56 +05:30
Nabin Hait
50b6f50b88 test: assert root rollup and no item leak in Purchase Analytics 2026-07-02 17:16:20 +05:30
Nabin Hait
0888405640 test: reuse shared distribution helper and assert a single territory row 2026-07-02 17:15:05 +05:30
Nabin Hait
087fb29d51 test: narrow Territory-wise Sales docstring to covered stages 2026-07-02 17:13:42 +05:30
Nabin Hait
2bab709ac4 test: scope date range, reload invoice, strengthen total-row check 2026-07-02 17:12:58 +05:30
Nabin Hait
5298438905 test: also assert Jun amount bucket in Quotation Trends monthly test 2026-07-02 17:11:37 +05:30
Nabin Hait
e0b0926dff fix: only resolve items when item_group/brand filter is set 2026-07-02 17:10:19 +05:30
Nabin Hait
a3d22f4a51 chore: re-trigger CI (unrelated flaky shard) 2026-07-02 17:03:54 +05:30
Nabin Hait
3f9b8fe37e test: reconcile negative-stock warehouses in reset_item_valuation_rate 2026-07-02 17:03:32 +05:30
pandiyan
820f5498e7 test: cover reserve stock gating on purchase receipt submit 2026-07-02 16:56:44 +05:30
pandiyan
71f02d412a fix: skip stock reservation for opted-out production plans 2026-07-02 16:56:34 +05:30
Nabin Hait
f9ac05f4a1 test: Timesheet Billing Summary report coverage 2026-07-02 15:54:55 +05:30
Nabin Hait
c7fed29569 test: Project Summary report coverage 2026-07-02 15:53:19 +05:30
Nabin Hait
5514c64b7c test: Lead Owner Efficiency report coverage 2026-07-02 15:51:22 +05:30
Nabin Hait
2a8d26c0a7 test: Supplier Quotation Comparison report coverage 2026-07-02 15:49:52 +05:30
Nabin Hait
56e7690e64 test: Subcontract Order Summary report coverage 2026-07-02 15:47:57 +05:30
Nabin Hait
6e57bd325f test: Purchase Analytics report coverage 2026-07-02 15:45:50 +05:30
Nabin Hait
8d70385019 test: Territory Target Variance based on Item Group report coverage 2026-07-02 15:43:59 +05:30
Nabin Hait
f95baa54de test: Territory-wise Sales report coverage 2026-07-02 15:42:07 +05:30
Nabin Hait
3092c920ff fix: pass only valid document filters in Sales Person-wise Transaction Summary 2026-07-02 15:40:32 +05:30
Nabin Hait
55646667be test: Sales Person Commission Summary report coverage 2026-07-02 15:36:47 +05:30
Nabin Hait
9865f63613 test: Customer-wise Item Price report coverage 2026-07-02 15:33:53 +05:30
Nabin Hait
08876ae07a test: Quotation Trends report coverage 2026-07-02 15:32:13 +05:30
Nabin Hait
489a799bc4 Merge pull request #56729 from frappe/chore/test-production-analytics
test: Production Analytics report coverage
2026-07-02 15:23:44 +05:30
Nabin Hait
0790d2e6df fix(manufacturing): include last-day records in Production Analytics
`get_work_orders` bounded a BETWEEN on the datetime columns `creation`
and `actual_end_date` with a bare date `to_date`, which MariaDB coerces
to midnight. Work orders created after 00:00:00 on the period's last day
were therefore dropped from the report (and made the new coverage test
fail on month-end CI runs). Extend `to_date` to end of day.
2026-07-02 15:09:00 +05:30
Nabin Hait
04b94ed61f Merge pull request #56766 from frappe/chore/budget-variance-zero-actuals-guard
test: zero pre-committed actuals in Budget Variance report tests
2026-07-02 15:06:48 +05:30
Nabin Hait
334b8ab09a Merge pull request #56733 from frappe/chore/test-work-order-consumed-materials
test: Work Order Consumed Materials report coverage
2026-07-02 15:06:10 +05:30
Nabin Hait
7592f568ae Merge pull request #56731 from frappe/chore/test-quality-inspection-summary
test: Quality Inspection Summary report coverage
2026-07-02 15:06:00 +05:30
Nabin Hait
bd21f506a1 Merge pull request #56730 from frappe/chore/test-bom-explorer
test: BOM Explorer report coverage
2026-07-02 15:05:33 +05:30
Nabin Hait
2eec826219 Merge pull request #56728 from frappe/chore/test-job-card-summary
test: Job Card Summary report coverage
2026-07-02 15:01:50 +05:30
Nabin Hait
4716084a41 Merge pull request #56725 from frappe/chore/test-consolidated-financial-statement
fix: Consolidated Financial Statement total double-count + test coverage
2026-07-02 15:01:28 +05:30
Nabin Hait
81e838c4f8 Merge pull request #56724 from frappe/chore/test-share-ledger
test: Share Ledger report coverage
2026-07-02 15:00:36 +05:30
Nabin Hait
c8eebd3a96 Merge pull request #56722 from frappe/chore/test-bank-clearance-summary
test: Bank Clearance Summary report coverage
2026-07-02 14:59:39 +05:30
Nabin Hait
b6bdf81ce8 Merge pull request #56738 from frappe/chore/test-production-plan-summary
test: Production Plan Summary report coverage
2026-07-02 14:58:07 +05:30
Nabin Hait
64db8072d8 Merge pull request #56739 from frappe/chore/test-exponential-smoothing-forecasting
test: Exponential Smoothing Forecasting report coverage
2026-07-02 14:57:56 +05:30
Nabin Hait
cabdb7417d Merge pull request #56767 from frappe/chore/incorrect-balance-qty-negative-case
test: cover inconsistent balance detection in Incorrect Balance Qty report
2026-07-02 14:57:08 +05:30
Nabin Hait
b4d3a879d2 Merge pull request #56759 from frappe/chore/fix-payment-period-range-buckets
fix: bucket late payments into 90 Above in Payment Period report
2026-07-02 14:56:36 +05:30
Nabin Hait
040b33070b Merge pull request #56765 from frappe/chore/profitability-analysis-unique-cost-centers
test: isolate Profitability Analysis tests from shared cost centers
2026-07-02 14:56:26 +05:30
Nabin Hait
dae3a21b61 Merge pull request #56762 from frappe/chore/cogs-by-item-group-scoping
test: isolate COGS By Item Group test with a dedicated item group
2026-07-02 14:55:54 +05:30
Nabin Hait
0769484fd6 Merge pull request #56761 from frappe/chore/item-wise-consumption-total-amount
test: isolate Item-wise Consumption test with a unique item
2026-07-02 14:53:47 +05:30
Nabin Hait
683ef19b8a Merge pull request #56760 from frappe/chore/fix-share-balance-company-filter
fix: scope Share Balance report to the selected company
2026-07-02 14:53:35 +05:30
rohitwaghchaure
0e8ae7548d fix: block serialized to non-serialized item change when SABB exists (#56773) 2026-07-02 08:55:49 +00:00
Nabin Hait
7f05b8ce58 Merge pull request #56734 from frappe/chore/test-bom-variance-report
test: BOM Variance Report report coverage
2026-07-02 14:25:37 +05:30
Nabin Hait
e92a9c706b Merge pull request #56736 from frappe/chore/test-cost-of-poor-quality-report
test: Cost of Poor Quality Report report coverage
2026-07-02 14:25:09 +05:30
Nabin Hait
7d8d1eaec7 test: submit overpaid payment for GL coverage and sync received_amount 2026-07-02 14:15:38 +05:30
Nabin Hait
18d1947154 test: assert to_date upper bound and use assertIsNone in Bank Clearance Summary 2026-07-02 14:13:29 +05:30
Nabin Hait
a69590b609 test: named column indices and Transfer-label coverage in Share Ledger 2026-07-02 14:12:24 +05:30
Nabin Hait
5adbc7baba test: target leaf accounts and robust amount assertions in Consolidated Financial Statement 2026-07-02 14:10:24 +05:30
Nabin Hait
7e7fd610cb test: guard job card list and derive status filter from stored status 2026-07-02 14:08:19 +05:30
Nabin Hait
ece8c9538d test: locale-safe status match and stable period window in Production Analytics 2026-07-02 14:06:37 +05:30
Nabin Hait
b77f6168d9 test: load BOM fixtures and scope to top-level rows in BOM Explorer test 2026-07-02 14:05:15 +05:30
Nabin Hait
14f862f80c test: add positive item_code filter case in Quality Inspection Summary 2026-07-02 14:03:45 +05:30
Nabin Hait
f1e91b6be6 test: add positive anchor and robust row pairing in Work Order Consumed Materials 2026-07-02 14:02:45 +05:30
Nabin Hait
835a050cfb test: cover produced-on-plan exclusion in BOM Variance report 2026-07-02 14:01:22 +05:30
Nabin Hait
2d3a1f5fab fix: expose hour rate column in Cost of Poor Quality report + robust float assert 2026-07-02 14:00:02 +05:30
Nabin Hait
14091a8996 fix: report full planned qty as pending when a plan has no work order 2026-07-02 13:58:31 +05:30
Nabin Hait
cc9d94efe8 test: use unique item and assert exact forecast in Exponential Smoothing test 2026-07-02 13:57:07 +05:30
Nabin Hait
c17517d22a test: use a unique item group per run in COGS test 2026-07-02 13:55:16 +05:30
Nabin Hait
4c9520bb1f Merge pull request #56769 from frappe/chore/negative-batch-report-negative-case
test: cover negative-batch detection in Negative Batch Report
2026-07-02 13:42:27 +05:30
Kavin
7248053c6a feat(stock): add configurable Stock Delivered But Not Billed (SDBNB) support (#56070)
* feat: add company setting to enable Stock Delivered But Not Billed accounting

* test: add tests for Stock Delivered But Not Billed account config

* fix(company): skip outstanding SDBNB validation when no previous config exists

* test: add dedicated company fixture for SDBNB tests

* test: use SDBNB company for Sales Invoice SDBNB test

---------

Co-authored-by: Pugazhendhi Velu <pugazhendhi720@gmail.com>
Co-authored-by: Pugazhendhi Velu <126157273+PugazhendhiVelu@users.noreply.github.com>
2026-07-02 13:34:25 +05:30
Mihir Kandoi
c98ca6d2cc Merge pull request #56771 from mihir-kandoi/pg/advisory-lock-postgres-only
fix: restrict repost advisory-lock gate to Postgres
2026-07-02 13:27:49 +05:30
Jatin3128
0a05dd4426 fix: restore Save button on reverse journal entry (#56770)
Reversing a submitted Journal Entry opened a draft with reversal_of set,
which called frm.set_read_only(). That strips the write and submit perms
from frm.perm, so the toolbar never rendered the Save (or later Submit)
button and the reversal could not be saved.

Lock the fields and the accounts grid as read_only instead, leaving perms
intact so Save and Submit still work while nothing stays editable.

Ticket: 72857
2026-07-02 13:17:14 +05:30
Mihir Kandoi
99fbd61bd9 fix: restrict repost advisory-lock gate to Postgres
MariaDB falls back to the existing deadlock-retry path; the advisory-lock
serialization from #56697 now applies on Postgres only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 13:15:12 +05:30
Nabin Hait
b9e321c106 test: cover negative-batch detection in Negative Batch Report 2026-07-02 12:57:16 +05:30
Nabin Hait
27f5235e67 test: cover inconsistent balance detection in Incorrect Balance Qty report 2026-07-02 12:48:29 +05:30
Nabin Hait
694328aab6 test: zero pre-committed actuals in Budget Variance report tests 2026-07-02 12:46:53 +05:30
Nabin Hait
cb4f3588fa test: isolate Profitability Analysis tests from shared cost centers 2026-07-02 12:45:01 +05:30
Mihir Kandoi
7835f11f96 Merge pull request #56757 from frappe/fix/stock-ageing-negative-batch-head
fix: don't treat batch slot at FIFO queue head as qty slot
2026-07-02 12:43:53 +05:30
Nabin Hait
2e72c13aee test: isolate COGS By Item Group test with a dedicated item group 2026-07-02 12:43:07 +05:30
Nabin Hait
898a70d340 test: isolate Item-wise Consumption test with a unique item 2026-07-02 12:41:18 +05:30
Nabin Hait
435998cc4e Merge pull request #56720 from frappe/chore/test-billed-items-to-be-received
fix: Billed Items To Be Received invoice filter + test coverage
2026-07-02 12:38:42 +05:30
Nabin Hait
ca7c6ca6da fix: scope Share Balance report to the selected company 2026-07-02 12:36:38 +05:30
Nabin Hait
d10504af03 fix: bucket late payments into 90 Above in Payment Period report 2026-07-02 12:34:38 +05:30
Nabin Hait
63cf379dbf Merge pull request #56743 from frappe/chore/fix-process-loss-report-filters
fix: apply item and work_order filters in Process Loss Report
2026-07-02 12:32:16 +05:30
Nabin Hait
65e3394481 Merge pull request #56749 from frappe/chore/strengthen-bom-operations-time-filters
test: strengthen BOM Operations Time filter isolation coverage
2026-07-02 12:32:00 +05:30
Mihir Kandoi
8928b42d5d test: assert full negative batch slot in ageing regression test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:31:24 +05:30
Mihir Kandoi
c47a95a4d2 fix: don't treat batch slot at FIFO queue head as qty slot
An incoming SLE without resolvable serial/batch details hit the
negative-head branch in _compute_incoming_stock even when the head was
a batch slot, because flt() on the batch number string returns 0.0.
_add_to_negative_fifo_head then crashed with
"TypeError: can only concatenate str (not 'float') to str".

Guard the branch with is_qty_slot, mirroring the existing check in
_add_transfer_slot_to_fifo_queue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:25:52 +05:30
ruthra kumar
5f83887334 Merge pull request #56754 from ruthra-kumar/update_process_statement_print_title
refactor: update title for process statement of accounts
2026-07-02 12:12:14 +05:30
ruthra kumar
04468c3c33 refactor: update title for process statement of accounts 2026-07-02 11:56:17 +05:30
Nabin Hait
f9029f8644 chore: re-trigger CI (infra untar failure) 2026-07-02 11:35:04 +05:30
Nabin Hait
8b3c3d9fef chore: re-trigger CI (infra untar failure) 2026-07-02 11:35:01 +05:30
Nabin Hait
2333afcd1e chore: re-trigger CI (infra untar failure) 2026-07-02 11:34:58 +05:30
Nabin Hait
4062e92c17 Merge pull request #56717 from frappe/chore/test-calculated-discount-mismatch
test: Calculated Discount Mismatch report coverage
2026-07-02 11:34:49 +05:30
Nabin Hait
7023817a71 chore: re-trigger CI (infra untar failure) 2026-07-02 11:08:49 +05:30
Nabin Hait
c1fb7d0545 Merge pull request #56727 from frappe/chore/test-work-order-summary
test: Work Order Summary report coverage
2026-07-02 11:05:30 +05:30
Nihantra C. Patel
cab1b129c0 fix: validate reverse GL entries on current date under immutable ledger (#56709)
* fix: validate reverse GL entries on current date under immutable ledger

When Immutable Ledger is enabled, the reverse GL entry is posted on the
current date, but the closed-period checks in make_reverse_gl_entries still
validate against the original (backdated) posting date. This blocks cancelling
a backdated voucher, such as a suspense Journal Entry for a migrated NPA loan,
with a books-closed error even though the reverse entry lands in an open period.

Validate both check_freezing_date and validate_against_pcv against the current
date when Immutable Ledger is enabled. When it is disabled, behaviour is
unchanged.

Follow-up to #55268.

* test: reset frozen till date after reverse entry test

The freeze date set on the company was not reset, so it leaked into the next
test which posts entries in that period. Reset it in a finally block.

* fix: prefer explicit posting_date under immutable ledger

Prefer the posting_date argument before frappe.form_dict and getdate, at both
the validation and the GL entry site, so an explicit date passed by the caller
is honoured and validation still matches the posted date.
2026-07-02 10:16:57 +05:30
Nabin Hait
0f812e0686 test: strengthen BOM Operations Time filter isolation coverage 2026-07-02 07:54:52 +05:30
Mihir Kandoi
171f12c2eb Merge pull request #56697 from mihir-kandoi/pg/advisory-lock
perf: serialize concurrent reposts with an advisory lock
2026-07-02 07:54:03 +05:30
Diptanil Saha
9cea43b006 fix(company): ignore user permissions for link fields having link to Account and Cost Center (#56748) 2026-07-02 07:42:34 +05:30
Mihir Kandoi
15adc92e76 Merge pull request #56744 from mihir-kandoi/pg/repost-recovery-by-exception-type
fix: classify repost recovery by exception type, not traceback string
2026-07-02 07:34:34 +05:30
Raffael Meyer
ba1e8f0005 fix(Quotation): create Customer from Lead (#55923) 2026-07-02 03:27:14 +02:00
Soham Kulkarni
3eaea74a51 Merge pull request #56656 from sokumon/revamp-workspaces
chore: exporting workspaces with sidebars
2026-07-02 05:39:24 +05:30
sokumon
d84eb9a97b fix: remove roles from budgeting workspace 2026-07-02 04:25:22 +05:30
mergify[bot]
0d8c65a013 ci(mergify): upgrade configuration to current format 2026-07-01 20:50:03 +00:00
Shllokkk
48aef307f9 fix: surface create payment entries as primary action on row selection 2026-07-02 02:19:38 +05:30
Mihir Kandoi
e5569f681a fix: classify repost recovery by exception type, not traceback string
repost() decided whether a failed Repost Item Valuation was recoverable
(re-queue as "In Progress") or permanently "Failed" by string-matching the
traceback for "timeout" or MariaDB's "Deadlock found". On Postgres a deadlock
surfaces as "deadlock detected" / "could not serialize access" and matches
neither, so a retriable deadlock was marked Failed and never re-queued -- the
scheduler only re-picks Queued/In Progress entries.

Classify by isinstance(e, RecoverableErrors) instead, the same tuple already
used to gate the error email. This covers deadlocks and lock/query timeouts on
both engines (frappe raises QueryDeadlockError / QueryTimeoutError uniformly)
and the advisory-lock repost gate's own QueryTimeoutError, which previously
recovered only because its class name incidentally contains "timeout".
2026-07-02 00:52:20 +05:30
Nabin Hait
145a0b154e fix: apply item and work_order filters in Process Loss Report 2026-07-02 00:34:36 +05:30
Mihir Kandoi
e99be23b57 Merge pull request #56696 from mihir-kandoi/pg/index-search
perf(postgres): partial/covering indexes + trigram item search
2026-07-02 00:23:18 +05:30
Nabin Hait
5cc866e840 Merge pull request #56737 from frappe/chore/test-bom-operations-time
test: BOM Operations Time report coverage
2026-07-02 00:21:54 +05:30
Mihir Kandoi
9e5b492db1 fix: tighten repost gate timeout, key, and scope (review)
- REPOST_LOCK_TIMEOUT 600 -> 300s: a contended waiter re-queues and frees its
  long-queue worker slot sooner instead of pinning it for up to 10 minutes
  (still well under the 1800s repost job timeout).
- Collision-free lock key: pass a ("stock_repost", item, warehouse) tuple so a
  colon in item_code/warehouse can't map two distinct pairs onto one lock.
- Document that the gate is repost-vs-repost only; the synchronous
  repost_current_voucher submit path is deliberately left ungated (gating a live
  submit behind a background repost would be a worse regression).
2026-07-02 00:20:15 +05:30
Nabin Hait
4cc2902b99 Merge pull request #56735 from frappe/chore/test-process-loss-report
test: Process Loss Report report coverage
2026-07-02 00:20:11 +05:30
Nabin Hait
b09889643f test: don't override tearDown; rely on ERPNextTestSuite rollback 2026-07-02 00:04:13 +05:30
Nabin Hait
4e88157ed7 test: stock raw materials before manufacture to avoid negative stock in CI 2026-07-02 00:02:50 +05:30
Mihir Kandoi
5e1296a0b9 perf(postgres): partial/covering indexes + trigram item search
Postgres-guarded on_doctype_update indexes: partial WHERE is_cancelled=0 + covering INCLUDE on GL Entry/SLE and Serial and Batch Bundle/Entry, and pg_trgm GIN on Item item_code/item_name (~128x faster LIKE search at scale). No-ops on MariaDB. Requires frappe framework support.
2026-07-02 00:00:37 +05:30
Nabin Hait
7ccd729cc5 Merge pull request #56732 from frappe/chore/test-downtime-analysis
test: Downtime Analysis report coverage
2026-07-01 23:57:38 +05:30
Nabin Hait
0aed70153b Merge pull request #56719 from frappe/chore/test-delivered-items-to-be-billed
test: Delivered Items To Be Billed report coverage
2026-07-01 23:54:45 +05:30
Nabin Hait
bdd6a63556 Merge pull request #56718 from frappe/chore/test-received-items-to-be-billed
test: Received Items To Be Billed report coverage
2026-07-01 23:54:37 +05:30
Mihir Kandoi
e51ffb1bf1 Merge pull request #56695 from mihir-kandoi/pg/recursive-cte
perf: use recursive CTEs for BOM and Task tree traversal
2026-07-01 23:53:39 +05:30
Nabin Hait
52d5085360 Merge pull request #56723 from frappe/chore/test-share-balance
fix: Share Balance respects the as-on date + test coverage
2026-07-01 23:52:15 +05:30
Nabin Hait
1969c9ca47 Merge pull request #56721 from frappe/chore/test-payment-period-based-on-invoice-date
fix: Payment Period ages by payment period + test coverage
2026-07-01 23:51:42 +05:30
Mihir Kandoi
83278d6f3b Merge pull request #56741 from mihir-kandoi/fix/multiple-variant-dialog-numeric
fix(item): rework multiple variant dialog for large numeric ranges
2026-07-01 23:43:50 +05:30
Mihir Kandoi
d4da9a3d7d fix(item): error on uncommitted input and escape values in variant dialog
Address review feedback:
- A typed-but-not-selected value passed validation yet was dropped by
  get_selected_attributes (reads committed pills only). Treat any pending
  input as an error so it is never silently omitted from creation.
- Escape pill / pending values before interpolating them into the HTML
  error message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 23:37:32 +05:30
Mihir Kandoi
99152b8300 fix(item): rework multiple variant dialog for large numeric ranges
The 'Create Multiple Variants' dialog rendered one checkbox per attribute
value and read the numeric config from the variant attribute child row. This
broke in several ways:

- A template whose attribute was made numeric after being added kept
  numeric_values=0 on the child row, so the dialog treated it as non-numeric,
  queried the empty Item Attribute Value table, and showed no values.
- Enumerating a large range (e.g. 1-100000) into checkboxes froze the browser.

Rework the dialog:

- Read numeric_values / from_range / to_range / increment from the Item
  Attribute master, and guard increment > 0.
- Replace the checkbox-per-value list with one MultiSelectPills per attribute,
  with a search placeholder.
- Stop enumerating numeric ranges: preview the first few values and validate
  typed input against the range on demand, so huge ranges stay instant.
- Block variant creation with a modal error if any selected value or pending
  input is invalid (out of range, off-increment, or not a number), so garbage
  like '00A' can't reach creation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 23:28:03 +05:30
Mihir Kandoi
4afbd4d3d9 fix(item-attribute): clear attribute values when marking numeric
Marking an attribute numeric hides the Item Attribute Values grid but leaves
its rows in the doc, whose mandatory Attribute Value / Abbreviation block the
save client-side before the server can clear them. Clear the table on the
client too so the save goes through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 23:27:51 +05:30
Nabin Hait
e3e62a2211 Merge pull request #56716 from frappe/chore/test-voucher-wise-balance
test: Voucher-wise Balance report coverage
2026-07-01 22:55:12 +05:30
Nabin Hait
8b7780d494 test: add coverage for Exponential Smoothing Forecasting report 2026-07-01 22:54:38 +05:30
Nabin Hait
c38363c16d test: add coverage for Production Plan Summary report 2026-07-01 22:54:31 +05:30
Nabin Hait
304a247dc8 test: add coverage for BOM Operations Time report 2026-07-01 22:54:23 +05:30
Nabin Hait
8abef22a49 Merge pull request #56715 from frappe/chore/test-invalid-ledger-entries
fix: Invalid Ledger Entries account filter + test coverage
2026-07-01 22:54:22 +05:30
Nabin Hait
75ba81c79a test: add coverage for Cost of Poor Quality Report report 2026-07-01 22:54:16 +05:30
Nabin Hait
222842b7d1 test: add coverage for Process Loss Report report 2026-07-01 22:54:09 +05:30
Nabin Hait
376a5a2aee test: add coverage for BOM Variance Report report 2026-07-01 22:54:02 +05:30
Nabin Hait
47ee1d126d test: add coverage for Work Order Consumed Materials report 2026-07-01 22:53:54 +05:30
Nabin Hait
e179100afd Merge pull request #56714 from frappe/chore/test-purchase-invoice-trends
test: Purchase Invoice Trends report coverage
2026-07-01 22:52:16 +05:30
Nabin Hait
44aa01b115 Merge pull request #56713 from frappe/chore/test-sales-invoice-trends
test: Sales Invoice Trends report coverage
2026-07-01 22:51:47 +05:30
Nabin Hait
1a8ef852f1 test: add coverage for Downtime Analysis report 2026-07-01 22:49:01 +05:30
Nabin Hait
e0bf3713ea test: add coverage for Quality Inspection Summary report 2026-07-01 22:48:53 +05:30
Nabin Hait
eadaf37606 test: add coverage for BOM Explorer report 2026-07-01 22:48:43 +05:30
Nabin Hait
baae9bfb22 test: add coverage for Production Analytics report 2026-07-01 22:48:35 +05:30
Nabin Hait
4f3dcd9e39 test: add coverage for Job Card Summary report 2026-07-01 22:48:27 +05:30
Nabin Hait
7a9e901e5e test: add coverage for Work Order Summary report 2026-07-01 22:48:13 +05:30
Mihir Kandoi
bb184f90a7 perf: serialize concurrent reposts with an advisory lock
Wraps per-(item, warehouse) reposting in repost_future_sle with a session-level advisory lock in front of the existing for-update row locks -- an outer gate that turns lock-order deadlocks into an orderly wait. Postgres/MariaDB only; nullcontext elsewhere. Row locks still enforce correctness. Requires frappe advisory_lock.
2026-07-01 22:11:43 +05:30
Mihir Kandoi
b2ad93be81 perf: use recursive CTEs for BOM and Task tree traversal
Reimplements get_ancestor_boms, BOM.traverse_tree, Task.check_recursion and the BOM Explorer report as single recursive-CTE queries (frappe.qb recursive=True), replacing query-per-node walks (N->1). Task cycle detection now catches cycles at any depth (was capped at 15 nodes). Requires the framework recursive-CTE support (frappe#40464).
2026-07-01 22:10:57 +05:30
Mihir Kandoi
65cb89cc40 Merge pull request #56552 from aerele/fix-quotation-conversion-rate-from-customer
fix: set conversion_rate on quotation created from customer
2026-07-01 21:46:28 +05:30
Mihir Kandoi
26a646aae5 Merge pull request #56701 from aerele/fix/pick-list-status-issue
feat(stock): support partial transfer from pick list
2026-07-01 21:45:17 +05:30
Mihir Kandoi
7d5efaf124 Merge pull request #56670 from aerele/fix/pick-list-wo-status-not-started
fix: recompute transferred qty before deciding work order status
2026-07-01 21:44:35 +05:30
Nabin Hait
0e8b152c68 fix: avoid double-counting the total in accumulated Consolidated Financial Statement 2026-07-01 21:26:04 +05:30
Nabin Hait
028cc2cf49 fix: age payments by the payment period (payment date - invoice date) 2026-07-01 21:23:58 +05:30
Nabin Hait
249d519d02 fix: compute Share Balance as-on the selected date 2026-07-01 21:20:57 +05:30
Nabin Hait
e9aac23913 fix: filter Billed Items To Be Received by invoice name, not per_received 2026-07-01 21:18:01 +05:30
Nabin Hait
f4088d48a1 fix: accept a scalar account filter in Invalid Ledger Entries report 2026-07-01 21:16:20 +05:30
Nabin Hait
2c3285286c test: add coverage for Consolidated Financial Statement report 2026-07-01 21:13:21 +05:30
Nabin Hait
8e560f1d1c test: add coverage for Share Ledger report 2026-07-01 21:13:12 +05:30
Nabin Hait
02460b4684 test: add coverage for Share Balance report 2026-07-01 21:13:02 +05:30
Nabin Hait
2a1461c754 test: add coverage for Bank Clearance Summary report 2026-07-01 21:08:05 +05:30
Nabin Hait
ee8e6e806f test: add coverage for Payment Period Based On Invoice Date report 2026-07-01 21:07:56 +05:30
Nabin Hait
cccfdc72c9 test: add coverage for Billed Items To Be Received report 2026-07-01 21:07:45 +05:30
Nabin Hait
196482348d test: add coverage for Delivered Items To Be Billed report 2026-07-01 21:07:31 +05:30
Nabin Hait
237605889f test: add coverage for Received Items To Be Billed report 2026-07-01 21:07:21 +05:30
Nabin Hait
293f737e4a test: add coverage for Calculated Discount Mismatch report 2026-07-01 21:02:36 +05:30
Nabin Hait
38ebfd7bd6 test: add coverage for Voucher-wise Balance report 2026-07-01 21:02:29 +05:30
Nabin Hait
9ec2945e6e test: add coverage for Invalid Ledger Entries report 2026-07-01 21:02:21 +05:30
Nabin Hait
cb9ea22b6f test: add coverage for Purchase Invoice Trends report 2026-07-01 21:02:12 +05:30
Nabin Hait
8aec16376e test: add coverage for Sales Invoice Trends report 2026-07-01 21:02:02 +05:30
Nabin Hait
7bc121e308 Merge pull request #56520 from frappe/chore/test-incorrect-serial-and-batch-bundle
test: Incorrect Serial and Batch Bundle report coverage
2026-07-01 20:54:20 +05:30
Nabin Hait
0f65004626 Merge pull request #56544 from frappe/chore/test-stock-and-account-value-comparison
test: Stock and Account Value Comparison report coverage
2026-07-01 20:30:25 +05:30
Nabin Hait
1fdc3875b6 Merge pull request #56513 from frappe/chore/test-warehouse-wise-stock-balance
test: Warehouse Wise Stock Balance report coverage
2026-07-01 20:30:12 +05:30
Nabin Hait
bea8c7bea2 Merge pull request #56507 from frappe/chore/budget-variance-report-test-coverage
test: Budget Variance report value coverage
2026-07-01 20:29:55 +05:30
Nabin Hait
4ea9125902 Merge pull request #56506 from frappe/chore/stock-ledger-test-coverage
test: Stock Ledger report coverage
2026-07-01 20:29:06 +05:30
Nabin Hait
ef877c4001 Merge pull request #56505 from frappe/chore/item-wise-purchase-history-test-coverage
test: Item-wise Purchase History report coverage
2026-07-01 20:28:39 +05:30
Nabin Hait
bb0a46db9e Merge pull request #56504 from frappe/chore/item-wise-sales-history-test-coverage
test: Item-wise Sales History report coverage
2026-07-01 20:28:29 +05:30
Nabin Hait
ae155e916a Merge pull request #56524 from frappe/chore/test-stock-ledger-variance
test: Stock Ledger Variance report coverage
2026-07-01 20:27:51 +05:30
Nabin Hait
cd6a8952e6 Merge pull request #56519 from frappe/chore/test-incorrect-balance-qty-after-transaction
test: Incorrect Balance Qty After Transaction report coverage
2026-07-01 20:27:33 +05:30
Nabin Hait
726edab495 Merge pull request #56518 from frappe/chore/test-fifo-queue-vs-qty-after-transaction-comparison
test: FIFO Queue vs Qty After Transaction Comparison report coverage
2026-07-01 20:27:02 +05:30
Nabin Hait
343557cf24 Merge pull request #56530 from frappe/chore/test-item-prices
test: Item Prices report coverage
2026-07-01 20:26:36 +05:30
Nabin Hait
bc28cfe182 Merge pull request #56521 from frappe/chore/test-incorrect-serial-no-valuation
test: Incorrect Serial No Valuation report coverage
2026-07-01 20:26:18 +05:30
Nabin Hait
f47141a3b7 test: flag an actual balance-qty variance in Stock Ledger Variance 2026-07-01 20:15:32 +05:30
Nabin Hait
ea5be1f7a5 fix: minor fix
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-01 20:12:42 +05:30
Nabin Hait
4960ca12fa Merge pull request #56532 from frappe/chore/test-itemwise-recommended-reorder-level
test: Itemwise Recommended Reorder Level report coverage
2026-07-01 20:11:44 +05:30
Nabin Hait
b52a8f5a77 test: apply ruff formatting 2026-07-01 19:34:29 +05:30
Nabin Hait
86eff05303 test: drop unused variable and apply ruff formatting 2026-07-01 19:34:03 +05:30
Nabin Hait
bd874d09ef test: drop unused variable and apply ruff formatting 2026-07-01 19:33:00 +05:30
Nabin Hait
04c710bec2 Merge pull request #56508 from frappe/chore/profitability-analysis-test-coverage
test: Profitability Analysis report coverage
2026-07-01 19:29:26 +05:30
Nabin Hait
875fc72842 test: flag an SLE whose balance is out of sync with the FIFO queue 2026-07-01 19:29:16 +05:30
ervishnucs
e4d6c0854b fix: remove redundant conversion_rate 2026-07-01 19:28:38 +05:30
Nabin Hait
527765001c test: flag a serial with mismatched in/out valuation 2026-07-01 19:27:56 +05:30
Nabin Hait
bd9aa8db68 Merge pull request #56509 from frappe/chore/gross-net-profit-report-test-coverage
test: Gross and Net Profit report coverage
2026-07-01 19:27:09 +05:30
Nabin Hait
7af8ca58d2 Merge pull request #56534 from frappe/chore/test-purchase-receipt-trends
test: Purchase Receipt Trends report coverage
2026-07-01 19:26:29 +05:30
Nabin Hait
adbd8276cf Merge pull request #56535 from frappe/chore/test-delivery-note-trends
test: Delivery Note Trends report coverage
2026-07-01 19:26:12 +05:30
Nabin Hait
beb2974317 test: flag an unlinked (orphan) serial and batch bundle 2026-07-01 19:25:54 +05:30
Nabin Hait
e5c0bd7931 Merge pull request #56516 from frappe/chore/test-product-bundle-balance
test: Product Bundle Balance report coverage
2026-07-01 19:25:40 +05:30
Nabin Hait
705f308ef7 Merge pull request #56514 from frappe/chore/test-total-stock-summary
test: Total Stock Summary report coverage
2026-07-01 19:25:30 +05:30
Nabin Hait
f8ce46f127 Merge pull request #56517 from frappe/chore/test-item-wise-consumption
test: Item-wise Consumption report coverage
2026-07-01 19:24:43 +05:30
Nabin Hait
039314c306 test: make Item Prices tests deterministic (fresh items, label-based columns) 2026-07-01 19:22:30 +05:30
Nabin Hait
f42198fb3c Merge pull request #56522 from frappe/chore/test-negative-batch-report
test: Negative Batch Report report coverage
2026-07-01 19:21:59 +05:30
Nabin Hait
7139639e77 Merge pull request #56525 from frappe/chore/test-stock-qty-vs-batch-qty
test: Stock Qty vs Batch Qty report coverage
2026-07-01 19:21:04 +05:30
Nabin Hait
f4ad1541bd Merge pull request #56529 from frappe/chore/test-warehouse-wise-item-balance-age-and-value
test: Warehouse Wise Item Balance Age and Value report coverage
2026-07-01 19:20:51 +05:30
Nabin Hait
49ecab6514 Merge pull request #56540 from frappe/chore/test-serial-no-and-batch-traceability
test: Serial No and Batch Traceability report coverage
2026-07-01 19:18:56 +05:30
Nabin Hait
3104369d79 Merge pull request #56542 from frappe/chore/test-cogs-by-item-group
test: COGS By Item Group report coverage
2026-07-01 19:18:42 +05:30
Nabin Hait
116b7bf672 fix: minor fix
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-07-01 19:18:17 +05:30
Nabin Hait
7f44583a94 Merge remote-tracking branch 'origin/develop' into chore/test-stock-and-account-value-comparison
# Conflicts:
#	erpnext/stock/report/stock_and_account_value_comparison/test_stock_and_account_value_comparison.py
2026-07-01 18:57:43 +05:30
ruthra kumar
c482a3b699 Merge pull request #56706 from ruthra-kumar/rename_synced_to_snapshot
refactor: rename synced to snapshot report
2026-07-01 17:42:58 +05:30
Mihir Kandoi
bfe01476be Merge pull request #56708 from frappe/fix-redundant-cast
chore: remove redundant type cast
2026-07-01 17:35:18 +05:30
ruthra kumar
981e90e4da refactor: rename feature toggle in report master 2026-07-01 17:32:33 +05:30
Mihir Kandoi
9cf356f6f5 chore: remove redundant type case 2026-07-01 17:23:19 +05:30
Nikhil Kothari
bbc4d2ccab feat: capture user persona during setup (#56705) 2026-07-01 11:51:15 +00:00
Mihir Kandoi
f493417c3d Merge pull request #56702 from mihir-kandoi/fix/normalize-ctx-input-py314
fix: keep normalize_ctx_input's ctx annotation on Python 3.14
2026-07-01 17:18:54 +05:30
Mihir Kandoi
8271b29e42 style: apply ruff formatter
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:04:11 +05:30
Sudharsanan11
fad904d68b fix(stock): backfill transferred qty for existing pick lists
Pick Lists transferred before this feature have transferred_qty = 0 and
their Stock Entry rows carry no pick_list_item link, so the new
is_fully_transferred check would never fire and, with the old
duplicate-entry guard removed, they could be transferred again. Set
transferred_qty = picked_qty for non-Delivery submitted pick lists that
already have a linked Stock Entry so they stay completed and locked.
2026-07-01 16:54:57 +05:30
Nabin Hait
9738228d9c Merge pull request #56526 from frappe/chore/test-stock-qty-vs-serial-no-count
test: Stock Qty vs Serial No Count report coverage
2026-07-01 16:46:30 +05:30
pandiyan
d072909451 fix: recompute transferred qty before deciding work order status
work order status was decided using a stale transferred-qty value,
computed before the current stock entry's transfer got recomputed.
this left work orders stuck at "not started" for pick-list-driven
transfers, since those entries never set fg_completed_qty and their
transferred qty can only be known from actual item-level transfers.

an earlier attempt fixed this by setting fg_completed_qty from the pick
list's for_qty, but that broke two things tied to fg_completed_qty
being zero: the excess-transfer guard, and the partial-transfer
fraction logic used to avoid marking a work order as fully supplied too
early.

recompute the transferred qty first, then decide status from the fresh
value. revert the fg_completed_qty change since it's no longer needed.
2026-07-01 16:41:15 +05:30
Mihir Kandoi
c00e5050cc Merge pull request #56703 from mihir-kandoi/fix/supplier-scorecard-recursion
fix: prevent max recursion on supplier scorecard save
2026-07-01 16:34:32 +05:30
Mihir Kandoi
e6f8f8f7e9 refactor: use frappe._dict in importers of ItemDetailsCtx
Extend the boundary rule to callers: non-decorated code that built or
annotated with ItemDetailsCtx now uses frappe._dict directly, and drops
the now-unused import. asset_capitalization keeps ItemDetailsCtx for its
own normalize_ctx_input-decorated functions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:30:16 +05:30
Mihir Kandoi
9406ec49de refactor: use frappe._dict directly in non-decorated helpers
ItemDetailsCtx signals the normalize_ctx_input boundary, so keep it only
on the decorator and the ctx param of decorated functions. Every other
annotation/constructor in non-decorated code becomes plain frappe._dict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:27:42 +05:30
Mihir Kandoi
603404775b fix: reset in_rescore flag after re-save
Ensure the recursion guard only applies to the nested save() and is cleared
afterwards, so a later save() on the same doc instance still creates periods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:19:52 +05:30
Mihir Kandoi
f26cb793b1 fix: restore | dict + normalization on non-decorated helpers
get_item_price is an internal, non-decorated helper: the "| dict" and
"pctx = frappe._dict(pctx)" were load-bearing (callers may pass a plain
dict; the body does attribute access). Restore both. Also restore the
"| dict" on set_valuation_rate/update_party_blanket_order out params
(these are not normalize_ctx_input-decorated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:19:07 +05:30
Mihir Kandoi
31e2d4ac5a fix: prevent max recursion on supplier scorecard save
on_update() called self.save(), which re-enters on_update() via
run_post_save_methods(), recursing indefinitely when make_all_scorecards()
keeps returning newly created periods. Guard the re-save with an in_rescore
flag so the nested on_update() short-circuits, while still running the full
validate() once to refresh score and standings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:15:59 +05:30
Mihir Kandoi
6eeadbdbef fix: keep normalize_ctx_input's ctx annotation on Python 3.14
Python 3.14 (PEP 649/749) replaced "__annotations__" with "__annotate__"
in functools.WRAPPER_ASSIGNMENTS. normalize_ctx_input excluded only
"__annotations__" when wrapping, so functools.wraps copied the wrapped
function's __annotate__ and the wrapper's permissive ctx annotation
(_dict | Document | dict | str) was overwritten by the narrow
ItemDetailsCtx | str. Now that Frappe casts whitelisted args via
typing_validations, a dict ctx failed the isinstance-only frappe._dict
check and raised FrappeTypeError. Exclude "__annotate__" too.

Cleanup while here:
- Merge the three identical frappe._dict aliases (ItemDetails,
  ItemDetailsCtx, ItemPriceCtx) into ItemDetailsCtx.
- Drop the now-redundant "| str" from decorated signatures; the
  decorator's wrapper union is what typing_validations enforces.
- Decorate get_batch_based_item_price with normalize_ctx_input instead
  of a manual parse_json, renaming its arg pctx -> ctx (JS caller
  updated) so a dict/string payload is normalized to frappe._dict.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 16:07:45 +05:30
Nabin Hait
497ca14747 test: detect a real stock/account value mismatch in comparison report 2026-07-01 15:55:33 +05:30
Nabin Hait
a2f8063804 Merge pull request #56543 from frappe/chore/test-landed-cost-report
test: Landed Cost Report report coverage
2026-07-01 15:54:32 +05:30
rohitwaghchaure
adae0bd732 feat: weekly auto-repost of incorrect stock valuation entries (#56637) 2026-07-01 15:47:46 +05:30
Sudharsanan11
a1daad8d4f test(stock): add test for partial transfer status from pick list 2026-07-01 15:44:35 +05:30
Sudharsanan11
27d5165755 feat(stock): support partial transfer from pick list
Creating a Stock Entry from a Pick List blocked any further entry
(stock_entry_exists) and flipped the pick list to Completed as soon as
one entry existed, so picked stock could not be transferred in parts.

Track transferred_qty per Pick List Item (summed from submitted Stock
Entry rows via a new pick_list_item link, mirroring delivered_qty), add
a Partially Transferred status, and map each new Stock Entry from the
remaining qty so transfers can continue until fully transferred.
2026-07-01 15:44:26 +05:30
sokumon
088b8ff69b fix: remove roles from home workspace 2026-07-01 14:22:43 +05:30
rohitwaghchaure
58e5755780 fix: manufacturing variance for standard cost valuation (#56684) 2026-07-01 14:04:14 +05:30
Mihir Kandoi
04cbb5da75 Merge pull request #56691 from mihir-kandoi/pg-ci-warmup-test-data
ci(postgres): warm up test data before baking the datadir
2026-07-01 13:55:38 +05:30
Nikhil Kothari
300471da12 fix(banking): handle blank password protected PDFs and negative amounts in CR/DR columns (#56690)
* fix(banking): strip signs from amount if column has CR/DR values

* fix(banking): try decrypting PDF with a blank password
2026-07-01 13:41:37 +05:30
Mihir Kandoi
3c067502f3 Merge pull request #56688 from mihir-kandoi/pg-greptile-over-rollback
ci(postgres): flag the over-broad-rollback trap in txn-abort review
2026-07-01 13:34:41 +05:30
Mihir Kandoi
63325cb976 ci(postgres): match MariaDB test job name (drop "(PG)")
Both server-test workflows now name the test job "Python Unit Tests" so the
check appears under the same name regardless of engine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:32:53 +05:30
sokumon
3b25878d71 fix: remove text from projects workspace 2026-07-01 13:29:25 +05:30
ruthra kumar
ba7b6a47c5 refactor: rename execute_synced_report to execute_snapshot_report
Match the framework rename of the standard report entry point in the
trial balance, P&L, balance sheet, and general ledger reports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:27:21 +05:30
Mihir Kandoi
e36e6bbb96 ci(postgres): warm up test data before baking the datadir
Mirror frappe/erpnext#56655 for the Postgres CI. Run the
bootstrap_test_data module in the setup job while Postgres is still up, so
the BootStrapTestData records are baked into the PGDATA artifact every shard
hydrates from — the shards start on already-warmed data instead of each
building it.

Unlike the MariaDB step, no `su -m` wrapper: the Postgres CI is
GitHub-hosted ubuntu-latest running as the runner user directly, matching
its own "Run Tests" step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:22:30 +05:30
Mihir Kandoi
b6382dce52 ci(postgres): add the return-contract note to the over-rollback bullet
Mirror the config.json guidance in POSTGRES_COMPATIBILITY.md: when scoping a rollback, keep the function's
success/None return contract -- don't return the doc that was just rolled back. (greptile #56688)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:21:18 +05:30
Nikhil Kothari
26583ae357 chore: update dependencies in banking app (#56685)
chore: update deps in banking app
2026-07-01 07:42:52 +00:00
Mihir Kandoi
06fb20d02d ci(greptile): flag over-broad full rollbacks in catch-and-continue handlers
Mirror the POSTGRES_COMPATIBILITY.md rule into the greptile instructions: prefer a scoped savepoint over a
full frappe.db.rollback() when recovering a poisoned txn; 'owns the txn' is not safe in a loop handler; and
keep the success/None return contract when scoping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:05:57 +05:30
Mihir Kandoi
c976b86714 ci(postgres): teach the parity guide the over-broad-rollback trap
Recovering a poisoned Postgres txn with a full frappe.db.rollback() discards rows the handler already
created before the failure -- which MariaDB keeps (no statement-abort) -- so it's a silent MariaDB
regression. 'Owns the txn' does not make a full rollback safe in a loop handler. Document the safe cases
(re-raise / single op / atomic batch) and the per-iteration/per-record savepoint alternative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 13:05:56 +05:30
Mihir Kandoi
a98474cab0 Merge pull request #56686 from mihir-kandoi/pg-d3-serial-no-fixture-case
test(stock): fix Available Serial No fixture item-code case for Postgres
2026-07-01 13:04:01 +05:30
Mihir Kandoi
9f229d614e test(stock): fix Available Serial No fixture item-code case for Postgres
setUp creates/receives/delivers the item as '_Test Item with Serial No' (lowercase w) but the report
filter used '_Test Item With Serial No' (capital W). MariaDB's case-insensitive collation resolved it,
but Postgres (case-sensitive) matched no item, so the report's 'if items:' guard dropped the item
filter and returned serial-no rows for every item in the window -- an order-dependent, flaky count on
Postgres. Align the filter to the created item's case (a no-op on MariaDB).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:51:54 +05:30
Mihir Kandoi
cb1642c7f6 Merge pull request #56683 from mihir-kandoi/pg-c1-txn-abort-followup
fix: scope three more Postgres txn-abort savepoints (fiscal year, Plaid sync, CRM customer)
2026-07-01 12:44:19 +05:30
Mihir Kandoi
59b49120b7 fix(crm): keep returning None from create_customer on a linking failure
Preserve the pre-existing contract: create_customer returned None when contact/address linking failed.
The savepoint fix kept the Customer (good) but started returning its name in that case, so a CRM caller
treating a non-None return as full success could skip its retry/error handling. Return None on a linking
failure while still keeping the Customer. (greptile #56683)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 12:28:36 +05:30
Mihir Kandoi
76b31d9269 fix(crm): scope create_customer rollback so a contact/address failure keeps the Customer
create_customer wrapped customer.insert() + create_contacts() + create_address() in one try whose except
did a full frappe.db.rollback(), so a failure while linking contacts/address discarded the Customer just
created (MariaDB kept it pre-migration). Split the try: the customer insert keeps its full rollback (safe
-- nothing precedes it), and contact/address linking runs under a savepoint so its failure rolls back only
the links, preserving the Customer and healing the Postgres txn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:55:42 +05:30
Mihir Kandoi
c58a4026a7 fix(integrations): per-transaction savepoint in Plaid sync_transactions
new_bank_transaction inserts+submits Bank Transactions in a loop within one transaction. On a failed
insert/submit, Postgres poisons the transaction so the except's log_error dies with InFailedSqlTransaction;
MariaDB keeps the Bank Transactions synced before the failure. A full rollback would discard those on
MariaDB too, so wrap each iteration in a savepoint + rollback(save_point=) and re-raise -- preserves
MariaDB's partial-sync behaviour and heals the Postgres txn. The sibling handlers add_institution /
add_bank_accounts were already fixed; this closes the third.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:55:41 +05:30
Mihir Kandoi
b926b846b1 fix(accounts): savepoint auto_create_fiscal_year loop to survive a duplicate year on Postgres
The daily scheduler loops creating next-year Fiscal Years (autoname=field:year). A duplicate-year
INSERT aborts the statement; on Postgres that poisons the whole transaction, so the next iteration's
get_doc/insert dies with InFailedSqlTransaction. MariaDB statement-rolls-back and continues. Wrap each
iteration in a savepoint + rollback(save_point=) in the DuplicateEntryError branch -- a strict no-op on
MariaDB (same INSERT, same skip), recovers the txn on Postgres.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 11:55:40 +05:30
Mihir Kandoi
36e0b71602 Merge pull request #56681 from mihir-kandoi/pg-b4-orderby-tiebreakers
fix: add unique tiebreakers to ORDER BY … LIMIT 1 picks for MariaDB↔Postgres parity
2026-07-01 09:30:17 +05:30
Mihir Kandoi
8f3eb6cb31 fix(selling): tie-break POS customer contact pick for cross-engine parity
The contact lookup orders only by is_primary_contact desc then takes contacts[0]; contacts commonly
tie (the no-primary case), so MariaDB and Postgres could pick a different contact. Add a parent
(contact name) tiebreaker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:10:19 +05:30
Mihir Kandoi
7a798dcba9 fix(stock): tie-break pick-list lookup in update_packed_item_with_pick_list_info
The Pick List Item get_value orders only by qty desc; a pick list can hold multiple rows for the same
SO item split across warehouses/batches/serials that tie on qty, so MariaDB and Postgres could stamp a
different warehouse/batch/serial onto the packed item. Add a name tiebreaker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:10:18 +05:30
Mihir Kandoi
813dcca29a fix(accounts): tie-break open Payment Request ordering for cross-engine parity
get_open_payment_requests_for_references orders by Coalesce(transaction_date, creation); when
transaction_date is set the coalesce never falls back to creation, so PRs sharing a transaction_date
have no tiebreaker and MariaDB/Postgres can allocate a different PR first. Append creation, name keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:10:18 +05:30
Mihir Kandoi
7f6a234cf7 fix(stock): pin get_item_price tie-break so MariaDB and Postgres agree
get_item_price ORDER BYs valid_from/batch_no/uom/party then LIMIT 1 with no unique key. Two Item
Price rows tied on all of those but differing price_list_rate would be picked arbitrarily -- MariaDB
and Postgres can return a different rate. Append a name tiebreaker; for exact ties MariaDB's pick was
already undefined, so its output is preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 09:10:16 +05:30
Diptanil Saha
85853fce12 Merge pull request #56678 from diptanilsaha/fix/gross_profit_debit_note
fix(gross_profit): correct GP calculation for rate adjustment debit notes
2026-07-01 08:32:05 +05:30
diptanilsaha
17ef5d6034 test(gross_profit): added test cases for rate adjustment entry 2026-07-01 08:17:42 +05:30
diptanilsaha
b9f330a158 fix: gross profit calculation with rate adjustment entries 2026-07-01 08:02:45 +05:30
Shllokkk
35de9deb0a fix: use live source warehouse valuation for internal transfer purchase receipts (#56431)
fix: anchor incoming SLE rate to DN rate for intra-company PR transfers
2026-07-01 06:54:26 +05:30
Mihir Kandoi
94a0c102a3 Merge pull request #56446 from aerele/fix/support-#72225
fix: support quality inspection for stock entry by purpose
2026-06-30 22:37:26 +05:30
Sudharsanan11
847fd8aa33 fix(stock): exclude consumption from outgoing quality inspection
The QI-by-purpose check required an inspection on every outgoing
(s_warehouse) row for any purpose that was not incoming. Material
Consumption for Manufacture rows are source-only and the Work Order
mapper copies inspection_required from the BOM, so this silently
blocked submission. An inspection_required BOM inspects the finished
good, not each consumed raw material.

Replace the "anything not incoming" fallthrough with an explicit
QI_OUTGOING_PURPOSES allow-list (mirrored in transaction.js) so a new
purpose cannot silently start requiring a QI. Consumption and Return
Raw Material to Customer now need no QI; Issue, Transfer, Transfer for
Manufacture, Send to Subcontractor, Subcontracting Delivery and
Disassemble keep their outgoing checks. Scope item_query to the same
set and add a regression test.
2026-06-30 21:32:30 +05:30
Mihir Kandoi
5afabb089d Merge pull request #56665 from mihir-kandoi/pg-greptile-audit-learnings
ci(greptile): DISTINCT row-count trap + refactor/conversion row-set faithfulness
2026-06-30 21:08:43 +05:30
Shllokkk
6425b9afaf Merge pull request #56661 from Shllokkk/fix-bulk-transaction-method
fix: handle None args in transaction_processing
2026-06-30 21:02:05 +05:30
Mihir Kandoi
f560767eb0 ci(postgres): include §6 in the How-to-review closing summary
The closing paragraph named only the §2/§3 semantic divergences as static-checker-invisible;
§6 (refactor/conversion row-set changes) is equally invisible and belongs there too. (greptile nit.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:58:52 +05:30
Mihir Kandoi
e79c24791f ci(greptile): flag the DISTINCT row-count trap and refactor-smuggled row-set changes
Mirror the two new POSTGRES_COMPATIBILITY.md rules into the greptile instructions so the bot flags
them on changed queries: (1) adding an ORDER BY column to a SELECT DISTINCT grows the distinct key
and the MariaDB row count unless it is functionally dependent; (2) a refactor / raw-frappe.db.sql->qb
conversion can silently change the WHERE/row set on both engines -- review the predicate, not just
the query shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:41:35 +05:30
Mihir Kandoi
a26329b2a0 ci(postgres): teach the parity guide the DISTINCT row-count trap + refactor faithfulness
Two review lessons from the post-merge net-diff/whole-repo re-audit of the SQL-dialect classes:

- Section 3 (row-count trap) now covers SELECT DISTINCT too: adding the ORDER BY column to the
  select to satisfy Postgres grows the DISTINCT key and changes the MariaDB row count when the
  column is not single-valued per distinct row -- sort in Python instead.
- New section 6: a 'refactor' / raw-SQL->qb conversion is not automatically 1:1. Diff the
  WHERE/predicate and the resulting row set, not just the SELECT shape -- a conversion that widens
  a filter (e.g. posting_datetime > X gaining an OR (== X AND creation > ...) branch under a
  sql->qb refactor) changes the rows touched on both engines and hides under a refactor label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 20:41:34 +05:30
Mihir Kandoi
78a64cd79b Merge pull request #56662 from mihir-kandoi/st72149
fix: use correct variable to fetch valuation method
2026-06-30 20:33:48 +05:30
Mihir Kandoi
1492c9fbc3 fix: use correct variable to fetch valuation method 2026-06-30 20:23:14 +05:30
Diptanil Saha
5b3e6e4714 Revert "chore: remove unused whitelisted method from project" (#56660) 2026-06-30 20:12:03 +05:30
Shllokkk
0db4af22e0 fix: handle None args in transaction_processing 2026-06-30 20:11:28 +05:30
Venkatesh
3b0e1fbb79 fix: remove translation for filter (#56629)
Co-authored-by: SowmyaArunachalam <sowmyaarunachalam57@gmail.com>
2026-06-30 14:52:11 +02:00
Mihir Kandoi
1c92dac274 Merge pull request #56450 from aerele/fix/support-#70387
fix(selling): update sales order per billed on credit note submission
2026-06-30 17:54:09 +05:30
ruthra kumar
d389a03c15 Merge pull request #56655 from ruthra-kumar/bootstrap_test_data_in_warmed_db
ci: warmup test data along with DB
2026-06-30 17:44:45 +05:30
ruthra kumar
dcdbf9df17 ci: warmup test data along with DB 2026-06-30 17:30:36 +05:30
sokumon
55afd95b20 fix: remove duplicate links from export 2026-06-30 17:24:58 +05:30
sokumon
5a32866b93 chore: export more workspaces 2026-06-30 17:00:54 +05:30
rohitwaghchaure
b8be1c8efd refactor: frappe.db.sql to frappe.qb for update_qty_in_future_sle (#56609) 2026-06-30 15:40:10 +05:30
Nikhil Kothari
8447f551e7 fix(banking): use custom renderer for translated strings and parser for rules (#56643)
fix(banking): use custom renderer for translated strings and parser for formula evaluation
2026-06-30 09:21:42 +00:00
Sudharsanan Ashok
6184c057db fix(stock): value batch/serial return from ledger when original receipt has no bundle (#56631)
* fix(stock): value batch/serial return from ledger when original receipt has no bundle

* test(stock): add test to validate the valuation of serial/batch for return when original receipt has no bundle
2026-06-30 14:28:04 +05:30
Mihir Kandoi
3d7bcd1f6a Merge pull request #56630 from mihir-kandoi/pg-convergence-fixes
fix: Postgres transaction-abort savepoints + div0/tiebreaker convergence fixes
2026-06-30 13:40:07 +05:30
Sudharsanan11
710d0667fa test(selling): add test to validate the per billed after credit note submission 2026-06-30 13:03:07 +05:30
Mihir Kandoi
460bb9e5d0 fix(crm): scope create_address rollback to a savepoint (review)
create_address is a helper called by create_prospect/create_customer AFTER they insert the Prospect/Customer. Its full frappe.db.rollback() on an address-save failure rolled back the caller's just-inserted parent doc, then swallowed the exception, so the caller returned a Prospect/Customer name that no longer existed. Scope the rollback to savepoint('crm_create_address') so only the address work is undone; the parent doc survives and the failed address is just logged.
2026-06-30 13:01:32 +05:30
Sudharsanan11
1202e79a16 fix(stock): fix tests 2026-06-30 12:31:14 +05:30
Khushi Rawat
8dfabf0e19 Merge pull request #56569 from frappe/fix-asset-is-fully-depreciated-visibility
fix(asset): conditionally show Is Fully Depreciated field
2026-06-30 12:28:22 +05:30
Mihir Kandoi
a36065931d fix(telephony): scope link_existing_conversations rollback to a savepoint (review)
link_existing_conversations is the Contact after_insert hook; a full frappe.db.rollback() on a failed call_log.save() would discard the triggering Contact insert itself (and, in test mode, the whole unit of work). Savepoint the hook's DB work and roll back only to it.
2026-06-30 12:17:39 +05:30
Mihir Kandoi
bd57e43446 fix(setup): scope regional-tax-settings rollback to a savepoint (review)
from_detailed_data inserts tax templates/accounts before update_regional_tax_settings in the same transaction; a full frappe.db.rollback() on regional-setup failure discarded those templates while the wizard continued. Take a savepoint before the regional call and roll back only to it.
2026-06-30 12:17:38 +05:30
Mihir Kandoi
16a6a4913e fix(stock): log Material Request failure without the rolled-back doc (review)
After rollback(save_point=reorder_mr) discards the just-inserted Material Request, mr.log_error() left a dangling Error Log reference. Use frappe.log_error(title=...).
2026-06-30 12:17:37 +05:30
Mihir Kandoi
b0331f13f1 fix(accounts): log bank-entry failure without the rolled-back doc (review)
The savepoint rollback erases the just-inserted Bank Transaction row, so bank_transaction.log_error() created an Error Log pointing at a row that no longer exists. Use frappe.log_error(title=...) with no doc reference.
2026-06-30 12:17:36 +05:30
Mihir Kandoi
3a1b47435f Merge pull request #56621 from aerele/fix/support-72552
fix: set mr status to received when per_received is 100 even if per_o…
2026-06-30 11:10:31 +05:30
pandiyan
a3c5ef6aa3 fix: set mr status to received when per_received is 100 even if per_ordered < 100 2026-06-30 11:00:23 +05:30
MochaMind
5c17c7d285 fix: sync translations from crowdin (#56633)
* fix: Persian translations

* fix: Swedish translations
2026-06-29 23:20:06 +02:00
Mihir Kandoi
f41e8208d8 fix(crm): savepoint Email Campaign send loop (Postgres)
send_mail (called per campaign schedule in a loop) inserts a Communication via make(); on failure the except calls frappe.log_error with no rollback, raising InFailedSqlTransaction on Postgres and poisoning subsequent sends. Savepoint before make() + rollback(save_point=) before log_error. No-op on MariaDB.
2026-06-29 22:48:28 +05:30
Mihir Kandoi
4feb9f9910 fix(assets): savepoint per-entry depreciation posting (Postgres)
make_depreciation_entry posts a Journal Entry per schedule row in a loop; the except only stored the error, so the next row's je.save()/submit() ran on the Postgres-poisoned txn (InFailedSqlTransaction). Savepoint per iteration + rollback(save_point=) before storing the error; the final raise of the collected error is unchanged. No-op on MariaDB.
2026-06-29 22:48:27 +05:30
Mihir Kandoi
944eeb5921 fix(accounts): savepoint subscription-status update loop in Payment Entry (Postgres)
trigger_invoice_update_for_subscriptions loops invoices calling refresh_subscription_status (db_set/save); on failure the except calls frappe.log_error with no rollback, raising InFailedSqlTransaction on Postgres, and the next invoice runs in the poisoned txn. Savepoint per iteration + rollback(save_point=) before log_error. No-op on MariaDB.
2026-06-29 22:48:26 +05:30
Mihir Kandoi
f3785f10a2 fix(accounts): savepoint per-row merge in Ledger Merge (Postgres)
start_merge merges accounts in a loop; on failure it only rolled back when not in_test, so in tests a failed merge_account left the Postgres txn poisoned and the except log_error + the finally db_set(status) raised InFailedSqlTransaction. Wrap each row in savepoint('ledger_merge_row') and rollback to it unconditionally before log_error - this recovers the txn in both paths without the full rollback discarding the rest of the test transaction. Production still commits per successful merge, so the per-iteration savepoint rollback is equivalent to the prior full rollback. No-op on MariaDB.
2026-06-29 22:48:25 +05:30
Mihir Kandoi
6b0f3cd243 fix(crm): rollback before logging in Frappe CRM webhook handlers (Postgres)
create_prospect/create_address/create_customer insert docs and on failure call frappe.log_error with no rollback; on Postgres (untrusted external CRM webhook input) a failed insert poisons the txn so log_error raises InFailedSqlTransaction. Full frappe.db.rollback() before each log_error. No-op on MariaDB.
2026-06-29 22:45:22 +05:30
Mihir Kandoi
5110e7f0fd fix(accounts): rollback before log_error in deferred-accounting in_test branch (Postgres)
book_deferred_entries' make_gl_entries failure path: the else branch already rolls back before log_error, but the frappe.in_test branch ran doc.log_error then re-raised with no rollback -> on Postgres log_error hits InFailedSqlTransaction and masks the original error. Rollback before log_error in the in_test branch too. No-op on MariaDB.
2026-06-29 22:45:21 +05:30
Mihir Kandoi
c643fe5274 fix(manufacturing): rollback before marking BOM Creator failed (Postgres)
create_production_plan_bom (background job) save+submits BOMs in a loop; on failure the except runs self.db_set(status=Failed, error_log) with no rollback, raising InFailedSqlTransaction on Postgres so status is never set. Full frappe.db.rollback() at the top of the except. No-op on MariaDB.
2026-06-29 22:45:20 +05:30
Mihir Kandoi
790560ebf8 fix(stock): rollback before marking Stock Closing Entry failed (Postgres)
prepare_closing_stock_balance (background job) saves Stock Closing Balance rows + db_set status; on failure the except runs db_set('Failed')+log_error with no rollback, raising InFailedSqlTransaction on Postgres so the doc is never marked Failed and the job dies. Full frappe.db.rollback() before the handler's db_set. No-op on MariaDB.
2026-06-29 22:45:19 +05:30
Mihir Kandoi
44458b0ba5 fix(setup): rollback before logging in update_regional_tax_settings (Postgres)
Regional tax-template setup writes docs; on failure the except calls frappe.log_error with no rollback -> InFailedSqlTransaction on Postgres. Full rollback before log_error. No-op on MariaDB.
2026-06-29 22:42:49 +05:30
Mihir Kandoi
8c0b4a99cf fix(setup): rollback before logging in install_country_fixtures (Postgres)
Regional fixture setup writes docs; on failure the except calls frappe.log_error before frappe.throw with no rollback -> InFailedSqlTransaction on Postgres. Full rollback before log_error. No-op on MariaDB.
2026-06-29 22:42:48 +05:30
Mihir Kandoi
01811ccf85 fix(telephony): rollback before logging in call_log link_existing_conversations (Postgres)
The hook saves Call Logs in a loop; on failure the except calls frappe.log_error (INSERT) with no rollback, raising InFailedSqlTransaction on Postgres (it runs on every Contact create/update). Full frappe.db.rollback() before log_error. No-op on MariaDB.
2026-06-29 22:42:48 +05:30
Mihir Kandoi
09a3eb8509 fix(integrations): savepoint the Plaid bank-account update branch + rollback add_institution (Postgres)
add_bank_accounts hardened only the INSERT branch with savepoint('plaid_bank_account'); the parallel else/UPDATE branch ran log_error+throw after a failed existing_account.save() with no rollback -> InFailedSqlTransaction on Postgres (masking the friendly throw). Mirror the insert branch with savepoint('plaid_update_account')+rollback. Also add_institution's except log_error after a failed bank.insert() now rolls back first. No-op on MariaDB.
2026-06-29 22:42:47 +05:30
Mihir Kandoi
298df4d3aa fix(stock): savepoint per-company Material Request creation in reorder (Postgres)
create_material_request loops companies inserting+submitting a Material Request; the except calls mr.log_error (INSERT) with no rollback, raising InFailedSqlTransaction on Postgres in the scheduled reorder job, and the next company runs in the poisoned txn. Savepoint per iteration + rollback(save_point=) before log_error. No-op on MariaDB.
2026-06-29 22:38:02 +05:30
Mihir Kandoi
c97eac34bf fix(accounts): savepoint per-row bank entry in Bank Transaction upload (Postgres)
create_bank_entries loops rows inserting+submitting a Bank Transaction; on failure the except calls bank_transaction.log_error (INSERT) with no rollback, raising InFailedSqlTransaction on Postgres, and the next row runs in the poisoned txn. Savepoint per row + rollback(save_point=) before log_error. No-op on MariaDB.
2026-06-29 22:38:02 +05:30
Mihir Kandoi
4a572311bc fix(buying): insert default Supplier Scorecard records with ignore_if_duplicate (Postgres)
make_default_records inserted Scorecard Variable/Standing rows in a loop and swallowed DuplicateEntryError (frappe.NameError). On Postgres the failed insert poisons the txn so the next iteration's insert raises InFailedSqlTransaction. insert(ignore_if_duplicate=True) emits ON CONFLICT DO NOTHING, never poisoning the txn. No-op on MariaDB.
2026-06-29 22:38:01 +05:30
Mihir Kandoi
1dde2b5f1e fix(stock): savepoint repost loop in Stock Ledger Invariant Check (Postgres)
Same shape: the rows loop submits a Repost Item Valuation; a caught DuplicateEntryError poisons the Postgres txn so the next iteration's submit raises InFailedSqlTransaction. Savepoint + rollback(save_point=) before continue. No-op on MariaDB.
2026-06-29 22:38:00 +05:30
Mihir Kandoi
d7a81affc2 fix(stock): savepoint repost loop in Stock and Account Value Comparison (Postgres)
The item/warehouse loop submits a Repost Item Valuation; a DuplicateEntryError poisons the Postgres transaction, so the next iteration's .submit() raises InFailedSqlTransaction. MariaDB continues. Savepoint per iteration + rollback(save_point=) on the caught duplicate (mirrors repost_item_valuation:782). No-op on MariaDB.
2026-06-29 22:37:59 +05:30
Mihir Kandoi
91dae91769 fix(setup): deterministic tiebreaker in get_exchange_rate Currency Exchange lookup (Postgres)
get_exchange_rate orders Currency Exchange by 'date desc' LIMIT 1 with no unique tiebreaker. Currency Exchange autoname {date}-{from}-{to}-{purpose} allows multiple same-date rows (different purpose) for one currency pair; on the no-purpose-filter path all match, so MariaDB and Postgres can return a different exchange_rate for the same inputs. Add 'name desc' so both engines pick the same row. MariaDB row count unchanged.
2026-06-29 22:25:39 +05:30
Mihir Kandoi
8c03029f28 fix(controllers): guard return-rate division against a zero stock qty (Postgres)
get_rate_for_return builds Abs(stock_value_difference / actual_qty) for Sales/Delivery returns and passes it to get_value with no actual_qty filter. A matched Stock Ledger Entry with actual_qty=0 (a zero-qty repost / serial-batch row) makes Postgres raise 'division by zero' while MariaDB returns NULL. Wrap the divisor in NullIf(actual_qty, 0) so both engines return NULL. MariaDB output unchanged. Sibling of the already-fixed /actual_qty sites in stock_ledger.py and incorrect_serial_no_valuation.py.
2026-06-29 22:25:19 +05:30
Mihir Kandoi
c26ad9fc36 Merge pull request #56625 from mihir-kandoi/pg-isi-txn-fix
fix: survive a failed invoice during Import Supplier Invoice on Postgres
2026-06-29 22:20:06 +05:30
Mihir Kandoi
0431b20945 Merge pull request #56620 from mihir-kandoi/pg-orderby-limit1-tiebreakers
fix: deterministic tiebreakers for ORDER BY <date> DESC LIMIT 1 lookups (MariaDB↔Postgres parity)
2026-06-29 22:18:04 +05:30
Mihir Kandoi
4952ce0cac Merge pull request #56622 from mihir-kandoi/pg-savepoint-txn-abort
fix: savepoint catch-and-continue DB writes to survive Postgres txn-abort (InFailedSqlTransaction)
2026-06-29 22:11:47 +05:30
Mihir Kandoi
dc2d3c433d Merge pull request #56624 from mihir-kandoi/pg-ci-fanout-stop-guard
ci(postgres): fail setup if pg_ctl stop fails; drop redundant ALTER SYSTEM block
2026-06-29 22:10:52 +05:30
Mihir Kandoi
65539d44b8 fix(regional): survive a failed invoice during Import Supplier Invoice on Postgres
create_purchase_invoice caught its own failure and then ran frappe.db.set_value + log_error in the SAME transaction. On Postgres a failed insert/save aborts the whole transaction, so the error-marking died with InFailedSqlTransaction and the failure cascaded through prepare_data_for_import's per-file loop, killing the entire import; MariaDB recovers per-statement and continues.

Let create_purchase_invoice raise, and wrap each call in prepare_data_for_import in frappe.db.savepoint + rollback(save_point=...). On failure the savepoint rollback un-poisons the transaction, the error is logged, and the per-file status is set to Error and committed (self.db_set(commit=True), matching the existing process_file_data status commit) so an interrupted import durably reflects Error instead of staying at the already-committed Processing File Data; the loop then continues to the next file. The savepoint is taken after create_supplier/create_address so those are preserved exactly as before.

Behaviour change (MariaDB): a failed invoice's partially-created draft Purchase Invoice is now rolled back on BOTH engines instead of being left as an orphan draft on MariaDB. Deliberate and more correct - a failed import should not leave a partial invoice; release-note worthy.
2026-06-29 22:10:00 +05:30
SowmyaArunachalam
07f641c48c fix(journal-entry): fetch outstanding on foreign currency 2026-06-29 21:48:38 +05:30
Mihir Kandoi
9157c9a67b Merge pull request #56623 from frappe/patch-test-download-from-release
ci(patch): pull v14 baseline from GitHub release instead of frappe.io
2026-06-29 21:47:04 +05:30
Mihir Kandoi
f645e51338 ci(patch): fetch v14 baseline from public release URL without a token
Greptile flagged that `gh release download` with `github.token` could be
rejected for fork pull requests (token scoped to the fork, asset in
frappe/erpnext). The release is public and published, so the asset is
downloadable anonymously from objects.githubusercontent.com — drop the token
and curl the public URL directly. Removes the cross-repo token dependency and
keeps fork PRs working. Cloudflare is still bypassed since GitHub serves the
asset, not frappe.io.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:33:42 +05:30
Mihir Kandoi
b93a3bca16 ci(postgres): fail setup if pg_ctl stop fails before baking the datadir
The "Stop DB and stage datadir" step swallowed a failed `pg_ctl -m fast -w
stop` with `|| true`, then moved and tarred the PGDATA regardless. A stop
that times out or errors would bake a still-running, crash-inconsistent
cluster into the artifact every test shard consumes — and with
full_page_writes off, crash recovery can't repair torn pages. Drop the
`|| true` so a failed stop fails the job, mirroring the MariaDB sister's
"don't bake a dirty datadir" guard.

Also drop the redundant `ALTER SYSTEM SET fsync/synchronous_commit/
full_page_writes = off` block from install.sh. Its comment claimed the
postgres workflow "runs a service-container DB and never calls start-db.sh",
but it does call start-db.sh, which already applies those flags via `-o` on
every postgres start (setup job and each shard). The block was a no-op and
its justification was factually wrong.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:29:22 +05:30
Mihir Kandoi
6e955bdf3f ci(patch): download v14 baseline from GitHub release instead of frappe.io
The Patch Test job intermittently failed on the "Download erpnext v14 backup"
step with HTTP 403 Forbidden: frappe.io sits behind Cloudflare, and wget's
default User-Agent gets flagged by bot protection on cache misses. This caused
random failures across PRs that only a re-run would clear.

Pull the fixed baseline from the v14-baseline GitHub release using the built-in
token instead. Release assets are served from GitHub's CDN and authenticated
from the runner, so no rate-limit roulette.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:28:08 +05:30
Mihir Kandoi
0978d0304f test(manufacturing): savepoint duplicate Routing insert in create_routing (Postgres)
create_routing inserts a Routing and, on DuplicateEntryError, re-fetches and updates. On Postgres the failed insert aborts the transaction so the get_doc/save in the except raises InFailedSqlTransaction; MariaDB recovers. Savepoint + rollback before the fallback path.
2026-06-29 20:50:05 +05:30
Mihir Kandoi
2b966b69ce fix(stock): savepoint per-voucher accounting repost submit (Postgres)
make_reposting_for_accounting_ledgers submits a new Repost Item Valuation per voucher in a loop under except Exception. On Postgres a failed submit aborts the transaction so the next iteration's DB work dies with InFailedSqlTransaction; MariaDB continues. Savepoint per iteration, roll back on failure. No-op on MariaDB.
2026-06-29 20:50:05 +05:30
Mihir Kandoi
864fe50b24 fix(subcontracting): savepoint Purchase Receipt submit in make_purchase_receipt (Postgres)
Same submit()/add_comment-in-except shape as the PO->SCO mapper: on Postgres a failed submit aborts the transaction so the follow-on Comment insert raises InFailedSqlTransaction; MariaDB continues. Savepoint + rollback before add_comment. No-op on MariaDB.
2026-06-29 20:50:05 +05:30
Mihir Kandoi
b6165844ed fix(buying): savepoint Subcontracting Order submit in make_subcontracting_order (Postgres)
target_doc.submit() is wrapped in except Exception whose handler calls add_comment (a Comment insert). On Postgres a failed submit poisons the transaction so the add_comment insert raises InFailedSqlTransaction; MariaDB logs the comment. Savepoint + rollback before add_comment. No-op on MariaDB.
2026-06-29 20:50:05 +05:30
Mihir Kandoi
70142d147e fix(selling): break last-sales-amount ties deterministically in Inactive Customers
get_last_sales_amt ordered by the sales date DESC only; same-date documents made the reported Last Order Amount engine-dependent. Add name DESC tiebreaker.
2026-06-29 16:29:46 +05:30
Mihir Kandoi
e52b9825e3 fix(accounts): break last-purchase-rate ties deterministically in Gross Profit
get_last_purchase_rate ordered by posting_date DESC only; same-date Purchase Invoices yielded an undefined last_purchase_rate that diverged between MariaDB and Postgres. Add purchase_invoice.name DESC tiebreaker.
2026-06-29 16:29:45 +05:30
Mihir Kandoi
93c186fea7 fix(assets): break latest asset-movement ties deterministically
get_latest_location_and_custodian ordered by transaction_date DESC only; equal-dated movements left the current location/custodian engine-dependent. Add asm.name DESC tiebreaker so both engines pick the same movement.
2026-06-29 16:29:44 +05:30
Mihir Kandoi
63ea907881 fix(accounts): break exchange-rate revaluation GLE ties deterministically
calculate_exchange_rate_using_last_gle ordered the latest-GLE lookups by posting_date DESC only. With multiple GL Entries on the latest posting_date the picked row was undefined, so MariaDB and Postgres could choose different vouchers and return a different last_exchange_rate (and revaluation gain/loss). Add gl.name DESC as a tiebreaker so both engines pick the same row; MariaDB row count unchanged.
2026-06-29 16:29:43 +05:30
ervishnucs
dead28e50e test: assert quotation from customer uses actual exchange rate 2026-06-28 20:49:56 +05:30
ervishnucs
8446be6518 fix: set currency and price list before computing quotation totals 2026-06-28 20:13:53 +05:30
ervishnucs
e61d299e63 fix: recalculate totals after setting quotation conversion rate 2026-06-28 09:53:38 +05:30
Mohd Haris
a7c1ebacbe fix(asset): conditionally show Is Fully Depreciated field
The "Is Fully Depreciated" field was hidden on the Asset form (hidden: 1),
so it could never be set for manually entered existing assets.

Make it visible based on context:
- Existing Asset with Calculate Depreciation off -> visible and editable
- Calculate Depreciation on -> visible but read-only and forced unchecked
  (it is only meaningful for manually entered assets)

The unchecked value is enforced in the form script (immediate feedback on
toggle and on load) and in server-side validate() so it can never be saved
as checked while depreciation is being calculated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 17:34:49 +05:30
Nabin Hait
87af67febe test: cover last purchase rate and valuation rate in Item Prices report 2026-06-26 15:00:39 +05:30
Nabin Hait
f2adb64f3b test: cover period, based_on and group_by filters in Delivery Note Trends 2026-06-26 14:50:54 +05:30
Nabin Hait
d2abb569d4 test: cover period, based_on and group_by filters in Purchase Receipt Trends 2026-06-26 14:49:27 +05:30
Nabin Hait
ba88667d99 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:36:42 +05:30
Nabin Hait
03ecd2fd3a test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:36:39 +05:30
Nabin Hait
a90db9a223 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:36:35 +05:30
Nabin Hait
18c4a20ad4 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:36:30 +05:30
ervishnucs
31ee3f1923 fix: set conversion_rate on quotation created from customer 2026-06-26 13:33:30 +05:30
Nabin Hait
d46b3f3627 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:28 +05:30
Nabin Hait
51bd2727a0 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:23 +05:30
Nabin Hait
5a62746dd3 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:19 +05:30
Nabin Hait
9cad192ccb test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:15 +05:30
Nabin Hait
5d217295e5 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:09 +05:30
Nabin Hait
e005d7021b test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:00 +05:30
Nabin Hait
db76533c16 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:52 +05:30
Nabin Hait
04fd425fb6 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:43 +05:30
Nabin Hait
3398e05190 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:39 +05:30
Nabin Hait
286ac77a05 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:35 +05:30
Nabin Hait
3b23e039e4 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:31 +05:30
Nabin Hait
9aef148a44 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:27 +05:30
Nabin Hait
0b35d394c5 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:22 +05:30
Nabin Hait
79bd6a9b7d test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:27:09 +05:30
Nabin Hait
7da4bc46bf test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:54 +05:30
Nabin Hait
851dfb16be test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:48 +05:30
Nabin Hait
abb7fec598 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:41 +05:30
Nabin Hait
04617b40b4 test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:37 +05:30
Nabin Hait
78de0c976a test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:32 +05:30
Nabin Hait
364250467f test: reuse BootStrapTestData master data to reduce runtime
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:26:19 +05:30
Nabin Hait
403788324a test: add coverage for Stock and Account Value Comparison report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:40:24 +05:30
Nabin Hait
6e23e49f23 test: add coverage for Landed Cost Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:40:16 +05:30
Nabin Hait
6595a32d90 test: add coverage for COGS By Item Group report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:40:07 +05:30
Nabin Hait
2092909f21 test: add coverage for Serial No and Batch Traceability report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:39:51 +05:30
Nabin Hait
25bcd12e92 test: add coverage for Delivery Note Trends report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:32:51 +05:30
Nabin Hait
68330843d8 test: add coverage for Purchase Receipt Trends report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:32:43 +05:30
Nabin Hait
993578dc2f test: add coverage for Itemwise Recommended Reorder Level report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:48 +05:30
Nabin Hait
6d97a5d543 test: add coverage for Item Prices report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:33 +05:30
Nabin Hait
495677ceb7 test: add coverage for Warehouse Wise Item Balance Age and Value report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:25:26 +05:30
Nabin Hait
b5405a02cc test: add coverage for Stock Qty vs Serial No Count report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:20:22 +05:30
Nabin Hait
655dea37dd test: add coverage for Stock Qty vs Batch Qty report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:20:15 +05:30
Nabin Hait
e9d4e2cedd test: add coverage for Stock Ledger Variance report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:20:07 +05:30
Nabin Hait
ddb07bcc0a test: add coverage for Negative Batch Report report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:52 +05:30
Nabin Hait
06592a49c8 test: add coverage for Incorrect Serial No Valuation report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:45 +05:30
Nabin Hait
047014f2b5 test: add coverage for Incorrect Serial and Batch Bundle report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:38 +05:30
Nabin Hait
7c8ef4cfc6 test: add coverage for Incorrect Balance Qty After Transaction report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:31 +05:30
Nabin Hait
ec739b213d test: add coverage for FIFO Queue vs Qty After Transaction Comparison report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:24 +05:30
Nabin Hait
119e0caafb test: add coverage for Item-wise Consumption report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:17 +05:30
Nabin Hait
752aefbdfd test: add coverage for Product Bundle Balance report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:19:11 +05:30
Nabin Hait
3c749ec785 test: add coverage for Total Stock Summary report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:18:57 +05:30
Nabin Hait
c7d6b6c0c4 test: add coverage for Warehouse Wise Stock Balance report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 10:18:48 +05:30
Nabin Hait
7688a7653e test: add coverage for Gross and Net Profit report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:52:14 +05:30
Nabin Hait
55c6d16d69 test: add coverage for Profitability Analysis report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:49:40 +05:30
Nabin Hait
d4ec544b25 test: add value-level coverage for Budget Variance report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:38:45 +05:30
Raghav Ruia
69d5d2bbc1 refactor: extract negative stock confirmation into shared util
Deduplicate the identical confirmation dialog used by Item and Stock
Settings into erpnext.utils.confirm_negative_stock, and collapse the
message into a single translatable string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 09:38:23 +05:30
Nabin Hait
e2dc38433e test: cover Enable Serial/Batch Bundle filter in Stock Ledger report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:34:39 +05:30
Nabin Hait
8b28aa8992 test: add coverage for Stock Ledger report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:23:28 +05:30
Nabin Hait
34fbcc9514 test: add coverage for Item-wise Purchase History report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:20:33 +05:30
Nabin Hait
16c71fa102 test: add coverage for Item-wise Sales History report
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:16:41 +05:30
Shllokkk
ecb6d48ec0 fix: restrict jinja globals in process statement of accounts templates 2026-06-25 14:48:02 +05:30
Sudharsanan11
4fa8a12bcb fix(selling): update sales order per billed on credit note submission 2026-06-25 13:17:47 +05:30
Sudharsanan11
2373db06ec fix: support quality inspection for stock entry by purpose
Fetch QI items by warehouse direction per inspection_type, require QI on
the correct rows per stock entry purpose (finished good for Manufacture,
inward goods for Receipt/Repack, outgoing rows for issue/transfer), and
show the QI field only on those rows.
2026-06-25 12:22:28 +05:30
Raghav Ruia
2bf9fcb817 feat: confirmation dialog when enabling negative stock on Item
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:06:30 +05:30
Nabin Hait
ac8b3f18c7 test: add purchase-side Payment Entry allocation coverage
- pay multiple purchase invoices with a single Payment Entry
- unallocated (advance) amount when a supplier payment is overpaid
- allocating more than a purchase invoice's outstanding amount is rejected
2026-06-22 14:13:18 +05:30
Mohsin Akhtar
2433129850 fix: show only template items in Variant Of filter 2026-06-21 16:10:43 +05:30
sokumon
90aba582ec chore: export workspaces with new schema 2026-06-12 13:08:48 +05:30
848 changed files with 179574 additions and 111823 deletions

View File

@@ -45,7 +45,9 @@ Flag a changed query that uses any of these:
- **`HAVING` referencing a `SELECT` alias** — PostgreSQL rejects output-column aliases in
`HAVING` (regardless of whether the query has a `GROUP BY`; MariaDB allows them). Repeat the
underlying expression in `HAVING`, or move a non-aggregate predicate into `WHERE`.
- **`SELECT DISTINCT … ORDER BY <expr not in the select list>`** — add the expr to the select.
- **`SELECT DISTINCT … ORDER BY <expr not in the select list>`** — add the expr to the select
**only if it is single-valued per distinct row**; otherwise it grows the `DISTINCT` key and the
MariaDB row count (see §3) — drop the SQL `ORDER BY` and sort in Python instead.
- **Single-quoted column alias** `AS 'x'` — PostgreSQL reads `'x'` as a string literal. Use an
unquoted (or double-quoted) alias.
- **`varchar | varchar`** (bitwise OR misused as a coalesce) — errors on PostgreSQL. Use
@@ -119,7 +121,7 @@ These don't error, so a one-engine CI stays green. Flag them:
---
## 3. The `GROUP BY` row-count trap (the single most important rule)
## 3. The row-count trap — `GROUP BY` **and** `DISTINCT` (the single most important rule)
When making a loose `GROUP BY` PostgreSQL-valid, **do not add a non-functionally-dependent
column to the `GROUP BY` just to satisfy PostgreSQL** — that turns one group row into N and
@@ -140,6 +142,42 @@ versa) to make a number "more correct" — that changes the MariaDB value. The w
MariaDB's prior one-value-per-group output; a different aggregate is a product change, out of
scope for a portability fix.
**The same trap applies to `SELECT DISTINCT`.** To satisfy PostgreSQL's "an `ORDER BY` expr must
appear in the select list under `DISTINCT`" rule, **do not blindly add the ordered column to the
select** — if it is not single-valued per existing distinct row, the `DISTINCT` key grows and
MariaDB returns **more rows** (a regression), exactly as adding a non-FD column to `GROUP BY` does.
Add it only when it is functionally dependent on the existing select columns; otherwise drop the
SQL `ORDER BY` and **sort in Python** (`key=str.casefold`, per §2) so the distinct row set is
unchanged.
### 3.1 Second-order traps — when the `Max()`/`Min()` wrap itself is the bug
The wrap is only a no-op when the column is provably single-valued per group (**"`Max()` means
provably constant"**). When the column can genuinely vary, the wrap is a decision, and a full
audit of these fixes found four recurring mistakes:
- **Incoherent pair** — two semantically-coupled columns (a flag + a link:
`is_phantom_item` + `bom_no`; a discriminator + its value) aggregated with *independent*
`Max()`/`Min()` can pair values from **different rows** — a chimera row that never existed.
MariaDB's loose pick was at least row-coherent. Fix: group by the pair (when consumers
tolerate the extra rows), or select one **representative row** (`Min(child.name)` subquery +
join-back) so every column comes from the same line.
- **NULL-skipping** — `MAX`/`MIN` ignore NULLs, so `Max()` over a mostly-NULL discriminator
(an `original_item`-style column) *deterministically* returns the non-NULL value where
MariaDB could return NULL — deterministically wrong where the old behavior was only
intermittently wrong. Flag it wherever "no value" is a meaningful state (fallback gates,
dict keys).
- **Fabricated arithmetic** — `Sum(x) * Max(y)` where `y` varies within the group invents a
number no row ever had (and `Max` biases it upward) — poisonous when it feeds validation,
budgets, valuation, or GL/stock values. Fix per-row: `Sum(x * y)`.
- **Wrong bound** — where the value has a semantic, pick the bound deliberately:
`Min(schedule_date)` for a "required by", `Min(idx)` for first-line ordering, a qty-weighted
average for a rate. A blind `Max` can understate urgency or overstate a figure.
Review heuristic: **if choosing between `Max` and `Min` would change the answer, the column is
not functionally dependent** — wrapping either is the wrong fix. Group by it, restructure, or
pick a bound for a stated reason, and cover the varying-group case with a test.
---
## 4. False positives — do NOT flag these
@@ -167,17 +205,44 @@ These are auto-handled by the framework and are **not** breaks:
frappe#40075). Such a handler must wrap the fallible insert in `frappe.db.savepoint(name)` +
`rollback(save_point=name)` — unless it re-`throw`s with no DB call before the throw, or the
insert uses `ignore_if_duplicate=True` / `autoname="hash"` (→ `ON CONFLICT DO NOTHING`).
- **Recover the txn with a *scoped* savepoint, not a full `frappe.db.rollback()`, if any prior work
must survive.** A full rollback un-poisons the txn but also discards every row the handler committed
*before* the failure — which MariaDB kept (it has no statement-abort), so it's a **silent MariaDB
regression**. **"The background job / whitelist entrypoint owns the txn" does NOT make a full rollback
safe** if it did multiple inserts in a loop first — it drops the partial results MariaDB retained. A
full rollback is safe only when it (a) immediately re-`throw`s/`raise`s (MariaDB rolls back anyway),
(b) has nothing successful before it (a single op), or (c) the batch is genuinely meant to be
**atomic** (a partial result is an invalid state → rollback + mark *Failed* is correct). Otherwise use
a **per-iteration / per-record savepoint** — and keep the function's success/`None` return contract:
do **not** return the doc when the savepoint was rolled back.
---
## 6. Refactors and raw-SQL→ORM conversions are not automatically 1:1
A commit labeled a **refactor** or a **raw-`frappe.db.sql` → `frappe.qb`/ORM conversion** is meant
to preserve behaviour — but it easily doesn't, and the change passes the static checker and a
one-engine green run. **Diff the `WHERE`/predicate, the `JOIN`/`ON` conditions, and the resulting
row set — not just the `SELECT` shape.** A conversion that silently widens or narrows the filter
changes the rows touched on **both** engines and is a regression hiding under a "refactor" label.
Real example: an `UPDATE` whose bound was `posting_datetime > X` gained an
`OR (posting_datetime == X AND creation > args.creation)` branch during a "`sql` → `qb` refactor",
widening the rows updated on both engines. Even when such a change is a deliberate bug-fix it must
be called out and tested — it is **not** the no-op the refactor label implies. Confirm the
converted query touches exactly the same rows with the same values MariaDB produced before.
---
## How to review
For every changed query: does it (a) use a construct from §1 (would error on PostgreSQL), or
(b) match a divergence in §2/§3 (different result across engines)? If so, comment with the
For every changed query: does it (a) use a construct from §1 (would error on PostgreSQL),
(b) match a divergence in §2/§3 (different result across engines), or (c) change the row set under
a refactor/conversion label (§6)? If so, comment with the
portable fix and confirm it leaves **MariaDB output unchanged**. Skip the §4 false positives.
Prefer a comment that names the rule (e.g. "loose GROUP BY — Max()-wrap, don't add to GROUP BY:
splits the row count") so the fix is unambiguous.
The static pre-commit checker (`.github/helper/postgres_compat.py`) catches the *mechanical*
§1 breaks; the **semantic** §2/§3 divergences are exactly what a reviewer (and this guide) must
cover, because no static check can see them.
§1 breaks; the **semantic** §2/§3 divergences and the §6 refactor/conversion row-set changes are
exactly what a reviewer (and this guide) must cover, because no static check can see them.

View File

@@ -6,7 +6,27 @@ cd ~ || exit
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
frappecommitish=${FRAPPE_BRANCH:-}
# A stacked pull request targets another erpnext branch, which has no counterpart in frappe.
# Fall back to develop so the bench is still installed. An explicit FRAPPE_BRANCH is trusted as
# given, since it can be a commit sha rather than a branch.
if [ -z "$frappecommitish" ]; then
frappecommitish=$githubbranch
# git ls-remote --exit-code reports 2 for a branch that is not there and 128 for a remote it
# could not reach. Only the first one is proof of absence; keep the branch on anything else so
# a flaky probe cannot install an unrelated frappe.
probe=0
git ls-remote --exit-code --heads "https://github.com/${frappeuser}/frappe" "$frappecommitish" >/dev/null 2>&1 || probe=$?
if [ "$probe" -eq 2 ]; then
echo "frappe has no branch ${frappecommitish}, falling back to develop"
frappecommitish=develop
elif [ "$probe" -ne 0 ]; then
echo "could not reach frappe to check for branch ${frappecommitish} (git ls-remote exited ${probe}), keeping it"
fi
fi
db_host=${DB_HOST:-"127.0.0.1"}
db_user_host=${DB_USER_HOST:-"localhost"}
wkhtmltox_deb=${WKHTMLTOX_DEB:-"/tmp/wkhtmltox.deb"}
@@ -297,14 +317,10 @@ if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
# Disposable CI DB: durability off for speed (postgres fsyncs every commit by default, which
# dominates a commit-heavy suite). All reloadable, no restart. The postgres workflow runs a
# service-container DB and never calls start-db.sh, so the flags must be applied here.
echo "travis" | psql -h 127.0.0.1 -p 5432 -U postgres \
-c "ALTER SYSTEM SET synchronous_commit = 'off'" \
-c "ALTER SYSTEM SET fsync = 'off'" \
-c "ALTER SYSTEM SET full_page_writes = 'off'" \
-c "SELECT pg_reload_conf()";
# Durability-off for speed (no fsync/synchronous_commit/full_page_writes) is applied by
# start-db.sh's postgres `-o` flags on every start — setup job AND each test shard — so it is
# NOT repeated here. The postgres workflow runs in-runner via start-db.sh, not a service
# container.
fi
cd ~/frappe-bench || exit

View File

@@ -66,7 +66,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
# The v14 baseline backup is a fixed published file — cache it instead of re-downloading
# ~100MB from frappe.io every run.
# it from the GitHub release every run.
- name: Cache erpnext v14 backup
id: cache-v14
uses: actions/cache@v4
@@ -76,7 +76,10 @@ jobs:
- name: Download erpnext v14 backup
if: steps.cache-v14.outputs.cache-hit != 'true'
run: wget -O ~/erpnext-v14.sql.gz https://frappe.io/files/erpnext-v14.sql.gz
run: |
curl -fSL --retry 5 --retry-all-errors --retry-delay 5 \
-o ~/erpnext-v14.sql.gz \
https://github.com/frappe/erpnext/releases/download/v14-baseline/erpnext-v14.sql.gz
- name: Cache pip
uses: actions/cache@v4

View File

@@ -107,6 +107,13 @@ jobs:
SKIP_SYSTEM_SETUP: "1"
SKIP_WKHTMLTOX_SETUP: "1"
- name: Warm up test data
run: |
su -m "${ERPNEXT_CI_USER:-frappe}" -s /bin/bash <<'EOF'
cd ~/frappe-bench/
bench --site test_site run-tests --lightmode --module erpnext.tests.bootstrap_test_data
EOF
# Clean shutdown (consistent InnoDB datadir), then stage it inside the bench for packaging.
- name: Stop DB and stage datadir
run: |

View File

@@ -105,10 +105,19 @@ jobs:
FRAPPE_BRANCH: develop
BENCH_CACHE_DIR: /home/runner/bench-cache
- name: Warm up test data
run: |
cd ~/frappe-bench/
bench --site test_site run-tests --lightmode --module erpnext.tests.bootstrap_test_data
- name: Stop DB and stage datadir
run: |
PG_BIN=$(ls -d /usr/lib/postgresql/*/bin | sort -V | tail -1)
"$PG_BIN/pg_ctl" -D /home/runner/pgdata -m fast -w stop || true
# Clean shutdown so the baked datadir is consistent. Do NOT swallow a failed stop with
# `|| true`: moving and tarring a still-running cluster ships a torn datadir the shards
# cannot crash-recover (full_page_writes is off). Fail the job instead — mirrors the
# MariaDB sister's "don't bake a dirty datadir" guard.
"$PG_BIN/pg_ctl" -D /home/runner/pgdata -m fast -w stop
mv /home/runner/pgdata /home/runner/frappe-bench/pgdata
- name: Package bench for test shards
@@ -128,7 +137,7 @@ jobs:
compression-level: 0
test:
name: Python Unit Tests (PG)
name: Python Unit Tests
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 60

File diff suppressed because one or more lines are too long

View File

@@ -88,7 +88,6 @@ pull_request_rules:
actions:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ body }}
commit_message_format:
title: pr-title
body: pr-body

View File

@@ -14,35 +14,35 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.3.0",
"@tailwindcss/vite": "^4.3.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.24",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^6.0.3",
"chrono-node": "^2.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.20",
"frappe-react-sdk": "^1.15.0",
"frappe-react-sdk": "^1.17.0",
"fuse.js": "^7.3.0",
"jotai": "^2.20.0",
"jotai-family": "^1.0.1",
"jotai": "^2.20.1",
"jotai-family": "^1.0.2",
"lodash.isplainobject": "^4.0.6",
"lucide-react": "^1.14.0",
"radix-ui": "^1.4.3",
"react": "^19.2.6",
"radix-ui": "^1.6.1",
"react": "^19.2.7",
"react-currency-input-field": "^4.0.5",
"react-day-picker": "9.14.0",
"react-dom": "^19.2.6",
"react-dom": "^19.2.7",
"react-dropzone": "^15.0.0",
"react-hook-form": "^7.75.0",
"react-hotkeys-hook": "^5.3.2",
"react-markdown": "^10.1.0",
"react-router": "^7.15.0",
"react-router-dom": "^7.15.0",
"react-router": "^8.1.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"safe-expr-eval": "^1.0.4",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.3.0",
@@ -51,15 +51,15 @@
"vite": "^8.0.16"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@eslint/js": "^9.39.4",
"@types/node": "^25.3.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0"
"typescript-eslint": "^8.62.1"
}
}

View File

@@ -1,5 +1,5 @@
import { lazy, useEffect } from 'react'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router'
import { FrappeProvider } from 'frappe-react-sdk'
import { Toaster } from '@/components/ui/sonner'
import BankReconciliation from '@/pages/BankReconciliation'

View File

@@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo, useState } from "react"
import { useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
@@ -26,6 +25,7 @@ import { Form } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { DateField } from "@/components/ui/form-elements"
import { Empty, EmptyMedia, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty"
import MarkdownRenderer from "@/components/ui/markdown"
const BankClearanceSummary = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
@@ -203,14 +203,14 @@ const BankClearanceSummaryView = () => {
[accountCurrency, bankAccount, companyID, mutate, onCopy],
)
const content = _("Below is a list of all accounting entries posted against the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
return <div className="space-y-4 py-2">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("Below is a list of all accounting entries posted against the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
</Paragraph>
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
</div>
{error && <ErrorBanner error={error} />}

View File

@@ -18,6 +18,7 @@ import { useMultiFileUploadProgress } from "@/hooks/useMultiFileUploadProgress"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Checkbox } from "@/components/ui/checkbox"
import { ArrowDownRight, ArrowUpRight, Plus, Trash2 } from "lucide-react"
import { evaluateAmountFormula } from "@/lib/amountFormula"
import { flt, formatCurrency } from "@/lib/numbers"
import { cn } from "@/lib/utils"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
@@ -215,38 +216,13 @@ const BankEntryForm = ({ selectedTransaction }: { selectedTransaction: Unreconci
})
} else {
/**
* The debit and credit amounts can also be expressions - like "transaction_amount * 0.5"
* So we need to compute the value of the expression
* We can use the eval function to do this. But we need to expose certain variables to the expression.
* One of them is transaction_amount which is the unallocated amount of the selected transaction
* @param expression - The expression to compute
* @returns The computed value
*/
const computeExpression = (expression: string) => {
const script = `
const transaction_amount = ${selectedTransaction.unallocated_amount ?? 0}
${expression};
`
let value = 0;
try {
value = window.eval(script);
} catch (error: unknown) {
console.error(error);
value = 0;
}
return value;
}
const transactionAmount = selectedTransaction.unallocated_amount ?? 0
if (!acc?.debit && !acc?.credit) {
hasTotallyEmptyRowEarlier = true;
}
const computedDebit = acc?.debit ? flt(computeExpression(acc.debit), 2) : 0
const computedCredit = acc?.credit ? flt(computeExpression(acc.credit), 2) : 0
const computedDebit = acc?.debit ? flt(evaluateAmountFormula(acc.debit, transactionAmount), 2) : 0
const computedCredit = acc?.credit ? flt(evaluateAmountFormula(acc.credit, transactionAmount), 2) : 0
totalDebits = flt(totalDebits + computedDebit, 2)
totalCredits = flt(totalCredits + computedCredit, 2)

View File

@@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import { useCallback, useMemo } from "react"
import type { ColumnDef } from "@tanstack/react-table"
import { useFrappeGetCall } from "frappe-react-sdk"
@@ -19,6 +18,7 @@ import _ from "@/lib/translate"
import { toast } from "sonner"
import { useCopyToClipboard } from "usehooks-ts"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import MarkdownRenderer from "@/components/ui/markdown"
const BankReconciliationStatement = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
@@ -189,14 +189,14 @@ const BankReconciliationStatementView = () => {
return data.message.result.filter((row: BankClearanceSummaryEntry) => Boolean(row.payment_entry))
}, [data])
const content = _("Below is a list of all entries posted against the bank account {0} which have not been cleared till {1}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formatDate(dates.toDate)}</strong>`])
return <div className="space-y-4 py-2">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("Below is a list of all entries posted against the bank account {0} which have not been cleared till {1}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formatDate(dates.toDate)}</strong>`])
}} />
</Paragraph>
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
</div>
{error && <ErrorBanner error={error} />}

View File

@@ -1,7 +1,6 @@
import { useAtomValue, useSetAtom } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { Paragraph } from "@/components/ui/typography"
import { formatDate } from "@/lib/date"
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers"
@@ -23,6 +22,7 @@ import { useCallback, useMemo, useState } from "react"
import { Link } from "react-router"
import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription, EmptyContent } from "@/components/ui/empty"
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
import MarkdownRenderer from "@/components/ui/markdown"
const BankTransactions = () => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
@@ -243,14 +243,14 @@ const BankTransactionListView = () => {
}, [data, search, amountFilter, typeFilter, status])
const content = _("Below is a list of all bank transactions imported in the system for the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account_name}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
return <div className="space-y-2 py-2">
<div className="flex gap-2 justify-between items-center">
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("Below is a list of all bank transactions imported in the system for the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account_name}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
</Paragraph>
<span className="text-p-sm">
<MarkdownRenderer content={content} />
</span>
<Button size='md' variant='subtle' asChild>
<Link to="/statement-importer">

View File

@@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo } from "react"
import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk"
@@ -18,6 +17,7 @@ import { PartyPopper } from "lucide-react"
import ErrorBanner from "@/components/ui/error-banner"
import _ from "@/lib/translate"
import { Empty, EmptyTitle, EmptyDescription, EmptyMedia, EmptyHeader } from "@/components/ui/empty"
import MarkdownRenderer from "@/components/ui/markdown"
const IncorrectlyClearedEntries = () => {
const companyID = useCurrentCompany()
@@ -177,22 +177,22 @@ const IncorrectlyClearedEntriesView = () => {
[accountCurrency, onClearClick],
)
const content = _("This report shows all entries in the system where the <strong>clearance date is before the posting date</strong> which is incorrect.")
const entriesContent = _("Entries below have a posting date after {0} but the clearance date is before {1}.", [`<strong>${formattedToDate}</strong>`, `<strong>${formattedToDate}</strong>`])
return <div className="space-y-4 py-2">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("This report shows all entries in the system where the <strong>clearance date is before the posting date</strong> which is incorrect.")
}} />
<span className="text-p-sm">
<MarkdownRenderer content={content} />
<br />
{data && data.message.result.length > 0 && <span>
<span dangerouslySetInnerHTML={{
__html: _("Entries below have a posting date after {0} but the clearance date is before {1}.", [`<strong>${formattedToDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
<MarkdownRenderer content={entriesContent} />
<br />
{_("You can reset the clearing dates of these entries here.")}
</span>}
</Paragraph>
</span>
</div>
{error && <ErrorBanner error={error} />}

View File

@@ -11,6 +11,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { H4, Paragraph } from "@/components/ui/typography"
import { today } from "@/lib/date"
import { evaluateAmountFormula } from "@/lib/amountFormula"
import _ from "@/lib/translate"
import { cn } from "@/lib/utils"
import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule"
@@ -445,11 +446,10 @@ const AmountFormulaRenderer = ({ value }: { value?: string }) => {
// If it's a string and cannot be a number, then show it as a formula
if (isNaN(Number(value))) {
let calculatedValue = "";
try {
calculatedValue = window.eval(`const transaction_amount = 200; ${value}`);
calculatedValue = String(evaluateAmountFormula(value ?? "", 200));
} catch (error: unknown) {
console.error(error);
calculatedValue = "Error";

View File

@@ -14,7 +14,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { useFrappeEventListener, useFrappePostCall } from 'frappe-react-sdk'
import { toast } from 'sonner'
import ErrorBanner from '@/components/ui/error-banner'
import { Link, useNavigate } from 'react-router-dom'
import { Link, useNavigate } from 'react-router'
import { useMemo, useState } from 'react'
import { Progress } from '@/components/ui/progress'
import { useSetAtom } from 'jotai'

View File

@@ -0,0 +1,26 @@
import { Parser } from 'safe-expr-eval'
const parser = new Parser()
const PLAIN_NUMBER_PATTERN = /^-?\d+(\.\d+)?$/
export function evaluateAmountFormula(expression: string, transactionAmount: number): number {
const trimmed = expression.trim()
if (!trimmed) {
return 0
}
if (PLAIN_NUMBER_PATTERN.test(trimmed)) {
return Number(trimmed)
}
try {
const result = parser.parse(trimmed).evaluate({ transaction_amount: transactionAmount })
if (typeof result !== 'number' || !Number.isFinite(result)) {
return 0
}
return result
} catch {
return 0
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,3 +12,5 @@ append_commit_message: false
languages_mapping:
two_letters_code:
pt-BR: pt_BR
zh-CN: zh
zh-TW: zh_TW

View File

@@ -179,7 +179,12 @@ def normalize_ctx_input(T: type) -> callable:
def decorator(func: callable):
# conserve annotations for frappe.utils.typing_validations
@functools.wraps(func, assigned=(a for a in functools.WRAPPER_ASSIGNMENTS if a != "__annotations__"))
@functools.wraps(
func,
assigned=(
a for a in functools.WRAPPER_ASSIGNMENTS if a not in ("__annotations__", "__annotate__")
),
)
def wrapper(ctx: T | Document | dict | str, *args, **kwargs):
if isinstance(ctx, Document):
ctx = T(**ctx.as_dict())

View File

@@ -6,88 +6,148 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_outstanding_reference_documents,
get_payment_entry,
)
from erpnext.utilities.bulk_transaction import transaction_processing
@frappe.whitelist(methods=["POST"])
def create_payment_entries(
grouped_invoices: str | list | None = None,
ungrouped_invoices: str | list | None = None,
):
def create_payment_entries(invoices: str | list | None = None):
"""Create draft Payment Entries from AP report invoice selection."""
frappe.has_permission("Payment Entry", "create", throw=True)
grouped_invoices = [d for d in frappe.parse_json(grouped_invoices or "[]") if d.get("voucher_no")]
ungrouped_invoices = [d for d in frappe.parse_json(ungrouped_invoices or "[]") if d.get("voucher_no")]
if not grouped_invoices and not ungrouped_invoices:
names = [d["voucher_no"] for d in frappe.parse_json(invoices or "[]") if d.get("voucher_no")]
if not names:
frappe.throw(_("No Purchase Invoices selected"))
if ungrouped_invoices:
data = [{"name": d["voucher_no"]} for d in ungrouped_invoices]
transaction_processing(data, "Purchase Invoice", "Payment Entry")
payable, excluded = _partition_payable_invoices(names)
if not payable:
frappe.throw(_("None of the selected invoices are payable"))
if grouped_invoices:
groups = {}
for d in grouped_invoices:
key = (d["supplier"], d["party_account"])
groups.setdefault(
key, {"supplier": d["supplier"], "party_account": d["party_account"], "vouchers": []}
)["vouchers"].append(d["voucher_no"])
# invoices sharing a (supplier, payable account) are combined into one Payment Entry
groups = {}
for d in payable:
key = (d["supplier"], d["party_account"])
groups.setdefault(
key, {"supplier": d["supplier"], "party_account": d["party_account"], "vouchers": []}
)["vouchers"].append(d["voucher_no"])
frappe.msgprint(
_("Started a background job to create {0} Grouped Payment Entries").format(len(groups))
)
frappe.enqueue(
make_grouped_payment_entries,
queue="long",
timeout=1500,
groups=list(groups.values()),
)
def make_grouped_payment_entries(groups):
created, failed = 0, 0
for group in groups:
supplier = group["supplier"]
try:
frappe.db.savepoint("bulk_pe")
pe = _build_grouped_payment_entry(supplier, group["party_account"], group["vouchers"])
if not pe:
frappe.db.rollback(save_point="bulk_pe")
failed += 1
frappe.log_error(
title=_("Bulk Payment Entry skipped for {0}").format(supplier),
message=_(
"No outstanding invoices found for the selected vouchers in account {0}"
).format(group["party_account"]),
)
continue
pe.flags.ignore_validate = True
pe.set_title_field()
pe.insert(ignore_mandatory=True)
for group in groups.values():
if _create_payment_entry(group):
created += 1
except Exception:
frappe.db.rollback(save_point="bulk_pe")
else:
failed += 1
frappe.log_error(title=_("Bulk Payment Entry creation failed for {0}").format(supplier))
message = _("Created {0} draft Grouped Payment Entries").format(created)
message = _("Created {0} draft Payment Entries").format(created)
if excluded:
message += "" + _("{0} excluded (not payable)").format(len(excluded))
if failed:
message += "" + _("{0} skipped (see Error Log)").format(failed)
message += "" + _("{0} failed (see Error Log)").format(failed)
frappe.msgprint(message, title=_("Bulk Payment Entries"), indicator="green")
frappe.publish_realtime(
"msgprint",
{"message": message, "title": _("Bulk Payment Entries"), "indicator": "green"},
user=frappe.session.user,
after_commit=True,
@frappe.whitelist()
def get_payable_invoices(invoices: str | list | None = None):
"""Return the live payable subset of the selected invoices for the report dialog."""
frappe.has_permission("Payment Entry", "create", throw=True)
names = [d["voucher_no"] for d in frappe.parse_json(invoices or "[]") if d.get("voucher_no")]
payable, excluded = _partition_payable_invoices(names)
currency = None
if payable:
company = frappe.get_cached_value("Purchase Invoice", payable[0]["voucher_no"], "company")
currency = frappe.get_cached_value("Company", company, "default_currency")
return {"payable": payable, "excluded": excluded, "currency": currency}
def _partition_payable_invoices(names):
"""Split submitted Purchase Invoices into payable ones and excluded ones (with reason).
Returns are debit notes, internal transfers are inter-company, and non-positive
outstanding means already settled — none are valid targets for a supplier payment.
"""
if not names:
return [], []
rows = frappe.get_list(
"Purchase Invoice",
filters={"name": ["in", names], "docstatus": 1},
fields=[
"name",
"supplier",
"credit_to",
"outstanding_amount",
"conversion_rate",
"is_return",
"is_internal_supplier",
],
limit_page_length=0,
)
payable, excluded = [], []
for r in rows:
if r.is_return:
excluded.append({"voucher_no": r.name, "reason": _("Debit Note")})
elif r.is_internal_supplier:
excluded.append({"voucher_no": r.name, "reason": _("Internal Transfer")})
elif flt(r.outstanding_amount) <= 0:
excluded.append({"voucher_no": r.name, "reason": _("Already Paid")})
else:
payable.append(
{
"voucher_no": r.name,
"supplier": r.supplier,
"party_account": r.credit_to,
"outstanding": flt(r.outstanding_amount) * flt(r.conversion_rate or 1),
}
)
# names not returned were cancelled/deleted or no longer readable after the report loaded
found = {r.name for r in rows}
for name in names:
if name not in found:
excluded.append({"voucher_no": name, "reason": _("Not available")})
return payable, excluded
def _create_payment_entry(group):
supplier = group["supplier"]
try:
frappe.db.savepoint("bulk_pe")
if len(group["vouchers"]) == 1:
pe = _build_single_payment_entry(group["vouchers"][0])
else:
pe = _build_grouped_payment_entry(supplier, group["party_account"], group["vouchers"])
if not pe:
frappe.db.rollback(save_point="bulk_pe")
frappe.log_error(
title=_("Bulk Payment Entry skipped for {0}").format(supplier),
message=_("No outstanding amount for the selected invoice(s)."),
)
return False
pe.flags.ignore_validate = True
pe.set_title_field()
pe.insert(ignore_mandatory=True)
return True
except Exception:
frappe.db.rollback(save_point="bulk_pe")
frappe.log_error(title=_("Bulk Payment Entry creation failed for {0}").format(supplier))
return False
def _build_single_payment_entry(name):
pe = get_payment_entry("Purchase Invoice", name)
# guard against a stale report row: nothing to allocate means the invoice is already settled
if not pe.references or not any(flt(r.allocated_amount) for r in pe.references):
return None
return pe
def _build_grouped_payment_entry(supplier, party_account, names):
name_set = set(names)
pe = get_payment_entry("Purchase Invoice", names[0])
pe.set("references", [])
@@ -101,8 +161,9 @@ def _build_grouped_payment_entry(supplier, party_account, names):
}
)
# get_negative_outstanding_invoices ignores the vouchers filter, so bound refs to the selection
for r in refs:
if r.voucher_type != "Purchase Invoice":
if r.voucher_type != "Purchase Invoice" or r.voucher_no not in name_set:
continue
pe.append(
"references",

View File

@@ -17,7 +17,7 @@ class ERPNextAddress(Address):
def link_address(self):
"""Link address based on owner"""
if self.is_your_company_address:
if self.get("is_your_company_address"):
return
return super().link_address()
@@ -28,7 +28,9 @@ class ERPNextAddress(Address):
self.is_your_company_address = 1
def validate_reference(self):
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
if self.get("is_your_company_address") and not [
row for row in self.links if row.link_doctype == "Company"
]:
frappe.throw(
_(
"Address needs to be linked to a Company. Please add a row for Company in the Links table."

View File

@@ -582,6 +582,7 @@ def make_gl_entries(
frappe.db.commit()
except Exception as e:
if frappe.in_test:
frappe.db.rollback()
doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
raise e
else:

View File

@@ -121,6 +121,7 @@ class Account(NestedSet):
self.validate_account_currency()
self.validate_root_company_and_sync_account_to_children()
self.validate_receivable_payable_account_type()
self.validate_stock_account_type_change()
def validate_parent_child_account_type(self):
if self.parent_account:
@@ -212,6 +213,36 @@ class Account(NestedSet):
frappe.msgprint(msg)
self.add_comment("Comment", msg)
def validate_stock_account_type_change(self):
doc_before_save = self.get_doc_before_save()
if not (doc_before_save and doc_before_save.account_type == "Stock"):
return
if self.account_type == "Stock":
return
if self.stock_ledger_entry_exists():
frappe.throw(
_(
"The account type of {0} cannot be changed from {1} because stock ledger entries exist against it."
).format(frappe.bold(self.name), frappe.bold(_("Stock")))
)
def stock_ledger_entry_exists(self):
from erpnext.stock import get_warehouse_account_map
warehouse_account = get_warehouse_account_map(self.company)
warehouses = [wh for wh, details in warehouse_account.items() if details.account == self.name]
if not warehouses:
return False
return bool(
frappe.db.count(
"Stock Ledger Entry",
filters={"warehouse": ("in", warehouses), "is_cancelled": 0},
)
)
def validate_root_details(self):
doc_before_save = self.get_doc_before_save()
@@ -659,8 +690,15 @@ def _ensure_idle_system():
last_gl_update = None
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
if frappe.db.db_type == "postgres":
# The MariaDB branch blocks new GL inserts via the gap lock its for_update read takes;
# a postgres row lock never blocks inserts, so take an EXCLUSIVE table lock instead --
# writers block until the rename commits, readers don't. NOWAIT mirrors wait=False.
frappe.db.sql("LOCK TABLE `tabGL Entry` IN EXCLUSIVE MODE NOWAIT")
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified")
else:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
except frappe.QueryTimeoutError:
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)

View File

@@ -24,7 +24,8 @@
"account_number": "11530"
},
"account_number": "115",
"is_group": 1
"is_group": 1,
"account_type": "Bank"
},
"Trade Receivables": {
"Trade Debtors": {
@@ -529,6 +530,13 @@
"account_number": "630",
"is_group": 1
},
"Accrued Manufacturing Expenses": {
"Accrued Expenses - Manufacturing": {
"account_number": "63510"
},
"account_number": "635",
"is_group": 1
},
"account_number": "63",
"is_group": 1
},
@@ -814,4 +822,4 @@
"root_type": "Expense"
}
}
}
}

View File

@@ -37,6 +37,10 @@
"account_type": "Stock",
"account_category": "Stock Assets"
},
"Stock Delivered But Not Billed": {
"account_type": "Stock Delivered But Not Billed",
"account_category": "Stock Assets"
},
"account_type": "Stock",
"account_category": "Stock Assets"
},
@@ -223,10 +227,6 @@
"Stock Received But Not Billed": {
"account_type": "Stock Received But Not Billed",
"account_category": "Trade Payables"
},
"Stock Delivered But Not Billed": {
"account_type": "Stock Delivered But Not Billed",
"account_category": "Trade Payables"
}
},
"Duties and Taxes": {

View File

@@ -22,12 +22,12 @@
"account_type": "Cash"
},
"Petty Cash Fund": {
"account_number": "1200",
"account_number": "1110",
"is_group": 1,
"root_type": "Asset",
"account_type": "Cash",
"Petty Cash Fund": {
"account_number": "1201",
"account_number": "1111",
"is_group": 0,
"root_type": "Asset",
"account_type": "Cash"
@@ -35,10 +35,16 @@
}
},
"Bank Accounts": {
"account_number": "1102",
"account_number": "1200",
"is_group": 1,
"root_type": "Asset",
"account_type": "Bank"
"account_type": "Bank",
"Cash in Bank - Checking Account": {
"account_number": "1201",
"is_group": 0,
"root_type": "Asset",
"account_type": "Bank"
}
},
"Advances to Officers & Employees": {
"account_number": "1290",
@@ -104,25 +110,20 @@
"account_number": "1511",
"is_group": 0,
"root_type": "Asset"
},
"Factory Overhead Variance": {
"account_number": "1512",
"is_group": 0,
"root_type": "Asset"
}
},
"Finished Goods": {
"account_number": "1520",
"account_number": "1540",
"is_group": 1,
"root_type": "Asset",
"Finished Goods Inventory": {
"account_number": "1531",
"account_number": "1541",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock"
},
"Inventory in Transit": {
"account_number": "1532",
"account_number": "1542",
"is_group": 0,
"root_type": "Asset",
"account_type": "Stock Adjustment"
@@ -268,7 +269,7 @@
"root_type": "Asset"
}
},
"System Development": {
"Intangible Assets": {
"account_number": "1940",
"is_group": 1,
"root_type": "Asset",
@@ -277,6 +278,17 @@
"is_group": 0,
"root_type": "Asset"
}
},
"Accumulated Amortization - Intangible Assets": {
"account_number": "1950",
"is_group": 1,
"root_type": "Asset",
"Accum Amortization - System Development": {
"account_number": "1951",
"is_group": 0,
"root_type": "Asset",
"account_type": "Accumulated Depreciation"
}
}
}
},
@@ -406,8 +418,7 @@
"Customer Deposits": {
"account_number": "2500",
"is_group": 0,
"root_type": "Liability",
"account_type": "Payable"
"root_type": "Liability"
}
},
"Non Current Liabilities": {
@@ -563,6 +574,28 @@
"is_group": 0,
"root_type": "Income"
}
},
"Exchange Gain": {
"account_number": "6030",
"is_group": 1,
"root_type": "Income",
"Exchange Gain - Detail": {
"account_number": "6031",
"is_group": 0,
"root_type": "Income",
"account_type": "Indirect Income"
}
},
"Gain on Asset Disposal": {
"account_number": "6040",
"is_group": 1,
"root_type": "Income",
"Gain on Asset Disposal - Detail": {
"account_number": "6041",
"is_group": 0,
"root_type": "Income",
"account_type": "Indirect Income"
}
}
}
},
@@ -575,7 +608,7 @@
"is_group": 1,
"root_type": "Expense",
"Cost of Goods Sold": {
"account_number": "5010",
"account_number": "5002",
"is_group": 0,
"root_type": "Expense",
"account_type": "Cost of Goods Sold"
@@ -828,20 +861,61 @@
"root_type": "Expense"
}
},
"Stock Adjustment": {
"Other Expenses": {
"account_number": "5200",
"is_group": 1,
"root_type": "Expense",
"Bank Charges": {
"account_number": "5201",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Interest Expenses Bank": {
"account_number": "5202",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Write Off": {
"account_number": "5203",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Exchange Loss": {
"account_number": "5204",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
},
"Loss on Asset Disposal": {
"account_number": "5205",
"is_group": 0,
"root_type": "Expense",
"account_type": "Indirect Expense"
}
},
"Provision For Income Tax": {
"account_number": "5300",
"is_group": 0,
"root_type": "Expense",
"account_type": "Tax"
},
"Stock Adjustment": {
"account_number": "5400",
"is_group": 0,
"root_type": "Expense",
"account_type": "Stock Adjustment"
},
"Round Off": {
"account_number": "5300",
"account_number": "5500",
"is_group": 0,
"root_type": "Expense",
"account_type": "Round Off"
},
"Expenses Included In Valuation": {
"account_number": "5400",
"account_number": "5600",
"is_group": 0,
"root_type": "Expense",
"account_type": "Expenses Included In Valuation"

View File

@@ -306,6 +306,31 @@ class TestAccount(ERPNextTestSuite):
acc.account_currency = "USD"
self.assertRaises(frappe.ValidationError, acc.save)
def test_stock_account_type_change_with_ledger_entries(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
stock_account = get_warehouse_account(frappe.get_doc("Warehouse", warehouse))
make_stock_entry(
item_code="_Test Item",
target=warehouse,
company=company,
qty=5,
basic_rate=100,
)
account = frappe.get_doc("Account", stock_account)
self.assertEqual(account.account_type, "Stock")
account.account_type = ""
self.assertRaises(frappe.ValidationError, account.save)
account.reload()
account.account_name = f"{account.account_name} Updated"
account.save() # non-type change stays allowed
def test_account_balance(self):
from erpnext.accounts.utils import get_balance_on

View File

@@ -1,10 +1,59 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
aggregate_with_last_account_closing_balance,
generate_key,
)
from erpnext.tests.utils import ERPNextTestSuite
def entry(**overrides):
row = {"debit": 0, "credit": 0, "debit_in_account_currency": 0, "credit_in_account_currency": 0}
row.update(overrides)
return row
class TestAccountClosingBalance(ERPNextTestSuite):
pass
"""The closing-balance snapshot is built by merging this period's entries with the
previous period's. These lock the merge/key logic that drives that carry-forward."""
def test_matching_entries_are_summed(self):
# this is how a prior-period balance carries forward into the current one
merged = aggregate_with_last_account_closing_balance(
[
entry(account="Cash - _TC", debit=100, debit_in_account_currency=100),
entry(
account="Cash - _TC",
debit=50,
credit=20,
debit_in_account_currency=50,
credit_in_account_currency=20,
),
],
[],
)
self.assertEqual(len(merged), 1)
row = next(iter(merged.values()))
self.assertEqual(row["debit"], 150)
self.assertEqual(row["credit"], 20)
# the account-currency columns are accumulated in the same pass
self.assertEqual(row["debit_in_account_currency"], 150)
self.assertEqual(row["credit_in_account_currency"], 20)
def test_entries_are_kept_separate_per_dimension(self):
merged = aggregate_with_last_account_closing_balance(
[
entry(account="Cash - _TC", cost_center="CC1", debit=100, debit_in_account_currency=100),
entry(account="Cash - _TC", cost_center="CC2", debit=40, debit_in_account_currency=40),
],
[],
)
self.assertEqual(len(merged), 2)
def test_period_closing_flag_is_part_of_the_key(self):
# a P&L reversal (flag 0) and a closing-account entry (flag 1) for the same
# account must not merge, so the flag has to distinguish their keys
key_reversal, _ = generate_key(entry(account="Sales - _TC", is_period_closing_voucher_entry=0), [])
key_closing, _ = generate_key(entry(account="Sales - _TC", is_period_closing_voucher_entry=1), [])
self.assertNotEqual(key_reversal, key_closing)

View File

@@ -359,3 +359,13 @@ def create_accounting_dimensions_for_doctype(doctype):
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)
def get_dimension_fieldname(dim_doctype: str) -> str:
"""
Return the `GL Entry` fieldname for a given dimension.
"""
if dim_doctype in ("Cost Center", "Project"):
return frappe.scrub(dim_doctype)
return frappe.db.get_value("Accounting Dimension", {"document_type": dim_doctype}, "fieldname")

View File

@@ -6,7 +6,7 @@ frappe.ui.form.on("Accounting Dimension Filter", {
let help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
{{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
</p>
</td></tr>

View File

@@ -22,6 +22,8 @@
"allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date",
"preview_mode",
"stock_expense_section",
"book_stock_expense_gl_entries",
"analytics_section",
"enable_discounts_and_margin",
"enable_accounting_dimensions",
@@ -76,6 +78,8 @@
"over_billing_allowance",
"credit_controller",
"role_allowed_to_over_bill",
"enable_overdue_billing_threshold",
"role_allowed_to_bypass_overdue_billing",
"column_break_11",
"assets_tab",
"asset_settings_section",
@@ -272,6 +276,21 @@
"label": "Role Allowed to over bill ",
"options": "Role"
},
{
"default": "0",
"description": "Block a new Sales Invoice when the customer's overdue amount exceeds the Overdue Limit set on the customer.",
"fieldname": "enable_overdue_billing_threshold",
"fieldtype": "Check",
"label": "Restrict Customer Over Billing"
},
{
"depends_on": "eval:doc.enable_overdue_billing_threshold",
"description": "Users with this role can still submit invoices for customers who have crossed their Overdue Limit.",
"fieldname": "role_allowed_to_bypass_overdue_billing",
"fieldtype": "Link",
"label": "Role Allowed to Bypass Over Billing Restriction",
"options": "Role"
},
{
"fieldname": "period_closing_settings_section",
"fieldtype": "Section Break"
@@ -757,6 +776,18 @@
"description": "Changing the account in any transaction of the DocTypes listed below will trigger a repost. To prevent reposting, remove the relevant DocType from the list.",
"fieldname": "column_break_mfor",
"fieldtype": "Column Break"
},
{
"fieldname": "stock_expense_section",
"fieldtype": "Section Break",
"label": "Stock Expense Accounting"
},
{
"default": "0",
"description": "Books Purchase Expense and Expenses Added To Stock account pairs against stock value. On enabling this, the accounts become mandatory in Company or Item Defaults for Purchase Receipt, Purchase Invoice, Stock Entry, Stock Reconciliation and Landed Cost Voucher",
"fieldname": "book_stock_expense_gl_entries",
"fieldtype": "Check",
"label": "Book Stock Expense GL Entries"
}
],
"grid_page_length": 50,
@@ -765,7 +796,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-24 12:59:41.868865",
"modified": "2026-07-15 17:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -62,6 +62,7 @@ class AccountsSettings(Document):
book_asset_depreciation_entry_automatically: DF.Check
book_deferred_entries_based_on: DF.Literal["Days", "Months"]
book_deferred_entries_via_journal_entry: DF.Check
book_stock_expense_gl_entries: DF.Check
book_tax_discount_loss: DF.Check
calculate_depr_using_total_days: DF.Check
check_supplier_invoice_uniqueness: DF.Check
@@ -77,6 +78,7 @@ class AccountsSettings(Document):
enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_overdue_billing_threshold: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
@@ -96,6 +98,7 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
role_allowed_to_bypass_overdue_billing: DF.Link | None
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
@@ -151,6 +154,10 @@ class AccountsSettings(Document):
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if old_doc.enable_overdue_billing_threshold != self.enable_overdue_billing_threshold:
toggle_overdue_billing_threshold_field(not self.enable_overdue_billing_threshold)
clear_cache = True
if clear_cache:
frappe.clear_cache()
@@ -242,6 +249,10 @@ def toggle_subscription_sections(hide):
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def toggle_overdue_billing_threshold_field(hide):
create_property_setter_for_hiding_field("Customer Credit Limit", "overdue_billing_threshold", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,

View File

@@ -107,7 +107,7 @@ def get_party_bank_account(party_type, party):
)
def get_default_company_bank_account(company, party_type, party):
def get_default_company_bank_account(company, party_type, party, ignore_permissions=True):
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
if default_company_bank_account:
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
@@ -118,6 +118,14 @@ def get_default_company_bank_account(company, party_type, party):
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
)
if not ignore_permissions:
default_company_bank_account = (
default_company_bank_account
if default_company_bank_account
and frappe.get_cached_doc("Bank Account", default_company_bank_account).has_permission("select")
else None
)
return default_company_bank_account
@@ -188,7 +196,7 @@ def get_closing_balance_as_per_statement(bank_account: str, date: str):
return {"balance": 0, "date": None}
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def set_closing_balance_as_per_statement(bank_account: str, date: str | datetime.date, balance: float):
"""
Set the closing balance as per statement for a bank account and date

View File

@@ -1,8 +1,76 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.utils import flt
from erpnext.accounts.doctype.bank_guarantee.bank_guarantee import get_voucher_details
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
BANK = "_Test BG Bank"
class TestBankGuarantee(ERPNextTestSuite):
pass
"""Bank Guarantee records a guarantee issued/received against a customer or
supplier. validate() needs a party; on_submit() needs the bank details filled in."""
def setUp(self):
frappe.set_user("Administrator")
if not frappe.db.exists("Bank", BANK):
frappe.get_doc({"doctype": "Bank", "bank_name": BANK}).insert()
def make_bg(self, **args):
args = frappe._dict(args)
doc = frappe.new_doc("Bank Guarantee")
doc.bg_type = args.bg_type or "Receiving"
doc.amount = args.amount if args.amount is not None else 1000
doc.start_date = args.start_date or "2026-06-01"
if args.end_date:
doc.end_date = args.end_date
doc.customer = args.get("customer", "_Test Customer")
doc.supplier = args.get("supplier")
# fields on_submit requires — present by default, cleared per-test to assert the guard
doc.bank_guarantee_number = args.get("bank_guarantee_number", "BG-001")
doc.name_of_beneficiary = args.get("name_of_beneficiary", "Test Beneficiary")
doc.bank = args.get("bank", BANK)
return doc
def test_validate_requires_customer_or_supplier(self):
doc = self.make_bg(customer=None)
self.assertRaises(frappe.ValidationError, doc.insert)
def test_submit_requires_guarantee_number(self):
doc = self.make_bg(bank_guarantee_number="")
doc.insert()
self.assertRaises(frappe.ValidationError, doc.submit)
def test_submit_requires_beneficiary_name(self):
doc = self.make_bg(name_of_beneficiary="")
doc.insert()
self.assertRaises(frappe.ValidationError, doc.submit)
def test_submit_requires_bank(self):
doc = self.make_bg(bank="")
doc.insert()
self.assertRaises(frappe.ValidationError, doc.submit)
def test_valid_guarantee_submits(self):
doc = self.make_bg()
doc.insert()
doc.submit()
self.assertEqual(frappe.db.get_value("Bank Guarantee", doc.name, "docstatus"), 1)
def test_get_voucher_details_for_receiving(self):
so = make_sales_order()
details = get_voucher_details("Receiving", so.name)
self.assertEqual(details.customer, so.customer)
self.assertEqual(flt(details.grand_total), flt(so.grand_total))
def test_end_date_before_start_date_is_not_validated(self):
# SUSPECTED BUG: validate() never checks that end_date >= start_date, so a
# guarantee that expires before it starts saves cleanly. Locking the current
# (wrong) behaviour so a future fix that adds the check trips this test.
doc = self.make_bg(start_date="2026-06-30", end_date="2026-06-01")
doc.insert()
self.assertTrue(frappe.db.exists("Bank Guarantee", doc.name))

View File

@@ -116,7 +116,7 @@ def get_account_balance(bank_account: str, till_date: str | date, company: str):
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def update_bank_transaction(
bank_transaction_name: str, reference_number: str, party_type: str | None = None, party: str | None = None
):
@@ -146,7 +146,7 @@ def update_bank_transaction(
)[0]
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_journal_entry_bts(
bank_transaction_name: str,
reference_number: str | None = None,
@@ -305,7 +305,7 @@ def create_journal_entry_bts(
return reconcile_vouchers(bank_transaction_name, vouchers, is_new_voucher=True)
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_payment_entry_bts(
bank_transaction_name: str,
reference_number: str | None = None,
@@ -500,7 +500,7 @@ def create_bulk_internal_transfer(bank_transaction_names: list[str | int], bank_
return output
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_internal_transfer(
bank_transaction_name: str | int,
posting_date: str | date,
@@ -1057,7 +1057,7 @@ def get_auto_reconcile_message(partially_reconciled, reconciled):
return alert_message, indicator
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def reconcile_vouchers(bank_transaction_name: str | int, vouchers: str | list, is_new_voucher: bool = False):
# updated clear date of all the vouchers based on the bank transaction
vouchers = frappe.parse_json(vouchers)

View File

@@ -8,6 +8,7 @@ from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
auto_reconcile_vouchers,
get_auto_reconcile_message,
get_bank_transactions,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
@@ -97,3 +98,40 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
# assert API output post reconciliation
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
self.assertEqual(len(transactions), 0)
def make_bank_transaction(self, date, deposit=100):
return (
frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": date,
"deposit": deposit,
"bank_account": self.bank_account,
"currency": "INR",
}
)
.save()
.submit()
)
def test_get_bank_transactions_excludes_dates_after_to_date(self):
self.make_bank_transaction(date=today())
names = [t.name for t in get_bank_transactions(self.bank_account, to_date=add_days(today(), -1))]
self.assertEqual(names, [])
def test_auto_reconcile_message_for_no_matches(self):
message, indicator = get_auto_reconcile_message([], [])
self.assertEqual(indicator, "blue")
self.assertIn("No matches", message)
def test_auto_reconcile_message_counts_and_pluralizes(self):
# reconciled count is reported and the indicator turns green
message, indicator = get_auto_reconcile_message([], ["t1", "t2"])
self.assertEqual(indicator, "green")
self.assertIn("2 Transaction(s) Reconciled", message)
# partially-reconciled label is singular for one, plural for many
singular, _ = get_auto_reconcile_message(["p1"], [])
self.assertIn("1 Transaction Partially Reconciled", singular)
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
self.assertIn("2 Transactions Partially Reconciled", plural)

View File

@@ -142,6 +142,30 @@ def preprocess_mt940_content(content: str) -> str:
return processed_content
MT940_CUSTOMER_REFERENCE_MAX_LEN = 16
def get_transaction_reference(txn_data: dict) -> str:
"""Extract the per-transaction reference from an MT940 :61: tag.
The mt940 library exposes ``transaction_reference`` from the :20: tag, which is the
statement-level reference and identical for every transaction in a statement. The
real per-transaction reference is ``customer_reference`` (with any overflow captured
into ``extra_details`` when a bank emits a single-line :61: longer than 16 chars).
"""
customer_reference = (txn_data.get("customer_reference") or "").strip()
if len(customer_reference) == MT940_CUSTOMER_REFERENCE_MAX_LEN:
customer_reference += (txn_data.get("extra_details") or "").strip()
if customer_reference and customer_reference.upper() != "NONREF":
return customer_reference
return (txn_data.get("bank_reference") or "").strip() or (
txn_data.get("transaction_reference") or ""
).strip()
@frappe.whitelist()
def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
doc = frappe.get_doc("Bank Statement Import", data_import)
@@ -189,8 +213,8 @@ def convert_mt940_to_csv(data_import: str, mt940_file_path: str):
deposit = amount_value if amount_value > 0 else ""
withdrawal = abs(amount_value) if amount_value < 0 else ""
description = txn.data.get("extra_details") or ""
reference = txn.data.get("transaction_reference") or ""
description = txn.data.get("transaction_details") or txn.data.get("extra_details") or ""
reference = get_transaction_reference(txn.data)
currency = txn.data.get("currency", "")
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
@@ -307,7 +331,7 @@ def add_bank_account(data, bank_account):
bank_account_loc = loc
for row in data[1:]:
if bank_account_loc:
if bank_account_loc is not None:
row[bank_account_loc] = bank_account
else:
row.append(bank_account)

View File

@@ -1,7 +1,10 @@
# Copyright (c) 2020, Frappe Technologies and Contributors
# See license.txt
import mt940
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
get_transaction_reference,
is_mt940_format,
preprocess_mt940_content,
)
@@ -188,6 +191,135 @@ class TestBankStatementImport(ERPNextTestSuite):
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
def test_get_transaction_reference_uses_customer_reference(self):
"""Per-transaction reference must come from :61: customer_reference, not :20:."""
self.assertEqual(
get_transaction_reference(
{"customer_reference": "UPI-100000000001", "transaction_reference": "STMTREF12345"}
),
"UPI-100000000001",
)
def test_get_transaction_reference_rejoins_overflow(self):
"""When a bank emits a single-line :61: with >16-char reference, the regex
splits the tail into extra_details. We must rejoin them."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NEFTINW-12345678",
"extra_details": "90",
"transaction_reference": "STMTREF12345",
}
),
"NEFTINW-1234567890",
)
def test_get_transaction_reference_falls_back_to_bank_reference_on_nonref(self):
"""NONREF is the MT940 'no customer reference' sentinel; prefer bank_reference."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NONREF",
"bank_reference": "1234567890123456",
"transaction_reference": "STMTREF12345",
}
),
"1234567890123456",
)
def test_get_transaction_reference_falls_back_to_bank_reference_on_nonref_with_extra_details(self):
"""NONREF sentinel must trigger the bank_reference fallback even when
extra_details is populated. Without the 16-char gate, the old naive concat
would produce a junk reference like 'NONREFsome info' and bypass the check."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NONREF",
"extra_details": "some info",
"bank_reference": "1234567890123456",
"transaction_reference": "STMTREF12345",
}
),
"1234567890123456",
)
def test_get_transaction_reference_does_not_append_extra_details_below_16_chars(self):
"""When customer_reference is below the 16-char cap, extra_details is a
genuine supplementary-info field from :61: — not overflow — and must not
be appended to the reference."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "TBMS-123456789",
"extra_details": "note field",
"transaction_reference": "STMTREF12345",
}
),
"TBMS-123456789",
)
def test_get_transaction_reference_keeps_noref_literal(self):
"""Bare 'NOREF' (without bank_reference) stays as-is; still better than the
statement-level reference which is identical across all transactions."""
self.assertEqual(
get_transaction_reference(
{
"customer_reference": "NOREF",
"bank_reference": None,
"transaction_reference": "STMTREF12345",
}
),
"NOREF",
)
def test_mt940_parse_per_transaction_reference_mapping(self):
"""End-to-end: every transaction in a statement must get its own distinct
reference from :61: customer_reference, never the statement-level :20: reference."""
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
:20:STMTREF12345
:25:1234567890
:28C:12345/1
:60F:C250716INR88123,38
:61:2509280928D5000,00NMSCUPI-100000000001
:86:UPI/TEST PAYEE ONE/111111111111/TestApp
:61:2509190919D2606,00NMSCUPI-100000000002
:86:UPI/TEST PAYEE TWO/222222222222/TestApp
:61:2509190919D900,00NMSCUPI-100000000003
:86:UPI/TEST PAYEE THREE/333333333333/TestApp
:61:2508140814D5000,00NMSCUPI-100000000004
:86:UPI/TEST PAYEE FOUR/444444444444/TestApp
:61:2508060806D2000,00NMSCUPI-100000000005
:86:UPI/TEST PAYEE FIVE/555555555555/TestApp
:61:2508030803D1066,00NMSC123456789012
:86:PCD/1234/TEST MERCHANT/01234567890123/12:00
:61:2507310731D305,62NMSCTBMS-123456789
:86:Chrg: Debit Card Annual Fee 1234 for 2025
:61:2507240724C1,00NMSCNEFTINW-1234567890
:86:NEFT TEST123456789 TEST SERVICES
:61:2507170717C100000,00NMSCNOREF
:86:BY CLG INST 123456/01-01-25/TESTBANK/TESTCITY
:62F:C250930INR100000,00
-}"""
transactions = list(mt940.parse(preprocess_mt940_content(mt940_content)))
references = [get_transaction_reference(t.data) for t in transactions]
self.assertEqual(
references,
[
"UPI-100000000001",
"UPI-100000000002",
"UPI-100000000003",
"UPI-100000000004",
"UPI-100000000005",
"123456789012",
"TBMS-123456789",
"NEFTINW-1234567890",
"NOREF",
],
)
# No transaction should carry the statement-level reference from :20:
self.assertNotIn("STMTREF12345", references)
def test_preprocess_mt940_content_whitespace_variants(self):
"""Test handling of whitespace and different line endings"""
# Test with trailing spaces

View File

@@ -54,7 +54,6 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Closing Balance",
"non_negative": 1,
"options": "currency"
},
{
@@ -191,7 +190,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-05-08 17:55:25.615942",
"modified": "2026-07-09 17:55:25.615942",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import Log",

View File

@@ -557,7 +557,7 @@ class BankStatementImportLog(Document):
docname=self.name,
)
if self.closing_balance and self.closing_balance > 0 and self.end_date:
if self.closing_balance is not None and self.end_date:
set_closing_balance_as_per_statement(
self.bank_account, frappe.utils.getdate(self.end_date), self.closing_balance
)
@@ -829,7 +829,9 @@ def compute_final_transactions(transaction_rows: list, date_format: str, amount_
if amount_format == 'Amount column has "CR"/"DR" values':
amount = transaction_row.get("amount")
float_amount = get_float_amount(amount)
# If the amount column has CR/DR in it - we should remove any signs (negative or positive) from the amount
float_amount = abs(get_float_amount(amount) or 0)
if "cr" in amount.lower():
return 0, float_amount
else:
@@ -932,14 +934,18 @@ def extract_pdf_tables(content: bytes, password: str | None = None) -> list[dict
from pypdf import PdfReader
reader = PdfReader(io.BytesIO(content))
if reader.is_encrypted and (not password or not reader.decrypt(password)):
frappe.throw(
_(
"This PDF is password protected. Please set the correct statement password on the"
" Bank Account and try again."
),
title=_("Password Required"),
)
if reader.is_encrypted:
# Try opening the PDF with a password - if no password is provided, try with a blank password
if not password:
password = ""
if not reader.decrypt(password):
frappe.throw(
_(
"This PDF is password protected. Please set the correct statement password on the"
" Bank Account and try again."
),
title=_("Password Required"),
)
text_settings = {"vertical_strategy": "text", "horizontal_strategy": "text"}
tables = []

View File

@@ -397,7 +397,7 @@ def unreconcile_transaction(transaction_name: str | int):
frappe.get_doc(voucher["doctype"], voucher["name"]).cancel()
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def unreconcile_transaction_entry(bank_transaction_id: str | int, voucher_type: str, voucher_id: str | int):
"""
Removes a single payment entry from a bank transaction - for example only undoing one voucher instead of undoing the entire transaction

View File

@@ -34,7 +34,7 @@ def upload_bank_statement():
return {"columns": columns, "data": data}
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_bank_entries(columns: str, data: str | list, bank_account: str):
header_map = get_header_mapping(columns, bank_account)
@@ -47,6 +47,7 @@ def create_bank_entries(columns: str, data: str | list, bank_account: str):
for key, value in header_map.items():
fields.update({key: d[int(value) - 1]})
frappe.db.savepoint("bank_entry")
try:
bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"})
bank_transaction.update(fields)
@@ -56,7 +57,8 @@ def create_bank_entries(columns: str, data: str | list, bank_account: str):
bank_transaction.submit()
success += 1
except Exception:
bank_transaction.log_error("Bank entry creation failed")
frappe.db.rollback(save_point="bank_entry")
frappe.log_error(title="Bank entry creation failed")
errors += 1
return {"success": success, "errors": errors}

View File

@@ -9,6 +9,48 @@ from frappe.model.document import Document
from erpnext.accounts.doctype.bank_transaction.bank_transaction import BankTransaction
PLAIN_NUMBER_PATTERN = re.compile(r"^-?\d+(\.\d+)?$")
# Tokens accepted by safe-expr-eval on the frontend (must stay in sync).
ALLOWED_FORMULA_TOKEN = re.compile(r"\s+|transaction_amount|\d+(?:\.\d+)?|[+\-*/%^()]")
PYTHON_ONLY_OPERATORS = ("**", "//")
def _is_expr_eval_formula(formula: str) -> bool:
position = 0
while position < len(formula):
match = ALLOWED_FORMULA_TOKEN.match(formula, position)
if not match:
return False
position = match.end()
return formula.count("(") == formula.count(")")
def validate_amount_formula(formula: str) -> None:
if not formula:
return
stripped = formula.strip()
if PLAIN_NUMBER_PATTERN.match(stripped):
return
if any(operator in stripped for operator in PYTHON_ONLY_OPERATORS):
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
if not _is_expr_eval_formula(stripped):
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
# expr-eval uses ^ for exponentiation; translate for a smoke-test evaluation only.
python_formula = stripped.replace("^", "**")
try:
result = frappe.safe_eval(python_formula, eval_globals=None, eval_locals={"transaction_amount": 1})
except Exception:
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
if not isinstance(result, (int | float)):
frappe.throw(_("Invalid debit/credit formula: {0}").format(formula))
class BankTransactionRule(Document):
# begin: auto-generated types
@@ -86,6 +128,11 @@ class BankTransactionRule(Document):
frappe.throw(
_("The last account row must not have any debit or credit amounts set.")
)
else:
if account.debit:
validate_amount_formula(account.debit)
if account.credit:
validate_amount_formula(account.credit)
# Validate regex
for rule in self.description_rules:
@@ -110,12 +157,9 @@ class BankTransactionRule(Document):
"""
Delete the matched rule from the bank transaction
"""
try:
frappe.db.set_value(
"Bank Transaction", {"matched_transaction_rule": self.name}, "matched_transaction_rule", None
)
except Exception:
pass
frappe.db.set_value(
"Bank Transaction", {"matched_transaction_rule": self.name}, "matched_transaction_rule", None
)
def after_delete(self):
"""

View File

@@ -231,3 +231,45 @@ class TestBankTransactionRule(ERPNextTestSuite, AccountsTestMixin):
doc = self._rule("bad_rx", [{"check": "Regex", "value": "["}])
with self.assertRaises(ValidationError):
doc.insert()
def _multiple_accounts_rule(self, prefix: str, accounts, **fields):
return self._rule(
prefix,
[{"check": "Contains", "value": "x"}],
classify_as="Bank Entry",
bank_entry_type="Multiple Accounts",
accounts=accounts,
**fields,
)
def test_validate_bank_entry_multiple_valid_amount_formulas(self):
doc = self._multiple_accounts_rule(
"be_formula",
accounts=[
{"account": self.bank, "debit": "200", "credit": ""},
{"account": self.cash, "debit": "", "credit": "transaction_amount * 0.25"},
{"account": self.cash, "debit": "", "credit": ""},
],
)
doc.insert()
self.assertTrue(doc.name)
def test_validate_bank_entry_multiple_invalid_amount_formulas(self):
malicious_formulas = [
"__import__('os')",
"eval('1+1')",
"open('/etc/passwd')",
"transaction_amount ** 2",
"transaction_amount // 2",
]
for formula in malicious_formulas:
with self.subTest(formula=formula):
doc = self._multiple_accounts_rule(
"be_bad_formula",
accounts=[
{"account": self.bank, "debit": formula, "credit": ""},
{"account": self.cash, "debit": "", "credit": ""},
],
)
with self.assertRaises(ValidationError):
doc.insert()

View File

@@ -184,7 +184,7 @@ class BisectAccountingStatements(Document):
self.get_report_summary()
self.update_node()
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def bisect_left(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
@@ -198,7 +198,7 @@ class BisectAccountingStatements(Document):
else:
frappe.msgprint(_("No more children on Left"))
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def bisect_right(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
@@ -212,7 +212,7 @@ class BisectAccountingStatements(Document):
else:
frappe.msgprint(_("No more children on Right"))
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def move_up(self):
if self.current_node is not None:
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)

View File

@@ -1,11 +1,47 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import datetime
import frappe
from frappe.utils import getdate
from erpnext.tests.utils import ERPNextTestSuite
class TestBisectAccountingStatements(ERPNextTestSuite):
pass
"""The tool bisects a date range into a tree of Bisect Nodes down to single days.
These cover the date validation and that the bisection cleanly partitions the range."""
def setUp(self):
frappe.set_user("Administrator")
frappe.db.delete("Bisect Nodes")
def _leaf_days(self):
leaves = frappe.get_all(
"Bisect Nodes",
filters={"left_child": ["is", "not set"]},
fields=["period_from_date", "period_to_date"],
)
# every leaf spans a single day
for leaf in leaves:
self.assertEqual(getdate(leaf.period_from_date), getdate(leaf.period_to_date))
return sorted(getdate(leaf.period_from_date) for leaf in leaves)
def test_validate_dates_rejects_reversed_range(self):
doc = frappe.new_doc("Bisect Accounting Statements")
doc.from_date = "2026-01-08"
doc.to_date = "2026-01-01"
self.assertRaises(frappe.ValidationError, doc.validate)
def test_bfs_partitions_range_into_single_days(self):
doc = frappe.new_doc("Bisect Accounting Statements")
doc.bfs(datetime.datetime(2026, 1, 1), datetime.datetime(2026, 1, 8))
# the 8-day span Jan 1..Jan 8 becomes exactly 8 contiguous single-day leaves
self.assertEqual(self._leaf_days(), [getdate(f"2026-01-0{n}") for n in range(1, 9)])
def test_dfs_produces_the_same_partition_as_bfs(self):
doc = frappe.new_doc("Bisect Accounting Statements")
doc.dfs(datetime.datetime(2026, 1, 1), datetime.datetime(2026, 1, 8))
self.assertEqual(self._leaf_days(), [getdate(f"2026-01-0{n}") for n in range(1, 9)])

View File

@@ -878,7 +878,7 @@ def get_fiscal_year_date_range(from_fiscal_year, to_fiscal_year):
return from_year.year_start_date, to_year.year_end_date
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def revise_budget(budget_name: str):
old_budget = frappe.get_doc("Budget", budget_name)

View File

@@ -1,8 +1,67 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite
DATE = "2026-06-15"
class TestCashierClosing(ERPNextTestSuite):
pass
"""Cashier Closing reconciles a shift: it pulls outstanding invoices in a
date/time window and rolls payments, expense, custody and returns into net_amount."""
def setUp(self):
frappe.set_user("Administrator")
def make_invoice_in_window(self, rate=100):
si = create_sales_invoice(rate=rate, qty=1, posting_date=DATE, do_not_submit=True)
si.posting_time = "10:30:00"
si.submit()
si.reload() # read outstanding_amount as persisted after submit
return si
def make_closing(self, user="Administrator", payments=None, **args):
doc = frappe.new_doc("Cashier Closing")
doc.user = user
doc.date = args.get("date", DATE)
doc.from_time = args.get("from_time", "09:00:00")
doc.time = args.get("time", "18:00:00")
for amount in payments or []:
doc.append("payments", {"mode_of_payment": "Cash", "amount": amount})
doc.expense = args.get("expense", 0)
doc.custody = args.get("custody", 0)
doc.returns = args.get("returns", 0)
return doc
def test_from_time_must_be_before_to_time(self):
doc = self.make_closing(from_time="18:00:00", time="09:00:00")
self.assertRaises(frappe.ValidationError, doc.save)
def test_equal_from_and_to_time_is_rejected(self):
# validate_time uses >=, so a zero-length window is also blocked
doc = self.make_closing(from_time="09:00:00", time="09:00:00")
self.assertRaises(frappe.ValidationError, doc.save)
def test_net_amount_rolls_up_outstanding_and_adjustments(self):
si = self.make_invoice_in_window(rate=100)
doc = self.make_closing(payments=[500], expense=50, custody=30, returns=20)
doc.save()
# the in-window invoice is picked up as outstanding
self.assertEqual(doc.outstanding_amount, si.outstanding_amount)
# net = payments + outstanding + expense - custody + returns
self.assertEqual(doc.net_amount, 500 + si.outstanding_amount + 50 - 30 + 20)
def test_outstanding_is_scoped_to_the_invoice_owner(self):
# The invoice is created by Administrator; a closing for a different user does
# not see it. NOTE: get_outstanding keys on Sales Invoice.owner (the document
# creator) rather than an explicit cashier/POS-user field, which is fragile when
# invoices are created by a shared or system user.
self.make_invoice_in_window(rate=100)
doc = self.make_closing(user="Guest", payments=[500])
doc.save()
self.assertEqual(doc.outstanding_amount, 0)
self.assertEqual(doc.net_amount, 500)

View File

@@ -220,6 +220,7 @@ def build_forest(data):
for row in data:
account_name, parent_account, account_number, parent_account_number = row[0:4]
if account_number:
account_number = cstr(account_number).strip()
account_name = f"{account_number} - {account_name}"
if parent_account_number:
parent_account_number = cstr(parent_account_number).strip()

View File

@@ -1,8 +1,54 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import (
build_forest,
validate_columns,
validate_missing_roots,
)
from erpnext.tests.utils import ERPNextTestSuite
# columns: account_name, parent_account, account_number, parent_account_number,
# is_group, account_type, root_type, account_currency
ROOT = ["Assets", "Assets", "", "", 1, "", "Asset", "INR"]
CHILD = ["Cash", "Assets", "", "", 0, "Cash", "Asset", "INR"]
class TestChartofAccountsImporter(ERPNextTestSuite):
pass
"""The importer parses an uploaded CoA into a nested tree and validates its
shape. These cover the parsing/validation helpers without a file upload."""
def test_validate_columns_rejects_blank_file(self):
self.assertRaises(frappe.ValidationError, validate_columns, [])
def test_validate_columns_requires_eight_columns(self):
self.assertRaises(frappe.ValidationError, validate_columns, [["a", "b", "c"]])
# the standard template width passes
validate_columns([ROOT])
def test_build_forest_nests_child_under_parent(self):
forest = build_forest([ROOT, CHILD])
self.assertIn("Assets", forest)
self.assertIn("Cash", forest["Assets"])
def test_build_forest_rejects_unknown_parent(self):
orphan = ["Cash", "Missing Parent", "", "", 0, "Cash", "Asset", "INR"]
self.assertRaises(frappe.ValidationError, build_forest, [orphan])
def test_build_forest_requires_account_name(self):
nameless = ["", "Assets", "", "", 0, "Cash", "Asset", "INR"]
self.assertRaises(frappe.ValidationError, build_forest, [ROOT, nameless])
def test_validate_missing_roots_requires_all_root_types(self):
present = ("Asset", "Liability", "Expense", "Income") # Equity missing
self.assertRaises(
frappe.ValidationError,
validate_missing_roots,
[{"root_type": rt} for rt in present],
)
# all five root types present -> no error
validate_missing_roots(
[{"root_type": rt} for rt in ("Asset", "Liability", "Expense", "Income", "Equity")]
)

View File

@@ -46,7 +46,7 @@ class ChequePrintTemplate(Document):
pass
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def create_or_update_cheque_print_format(template_name: str):
frappe.only_for("System Manager")

View File

@@ -169,23 +169,10 @@ frappe.ui.form.on("Dunning", {
},
get_dunning_letter_text: function (frm) {
if (frm.doc.dunning_type) {
frappe.call({
method: "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: {
dunning_type: frm.doc.dunning_type,
language: frm.doc.language,
doc: frm.doc,
},
callback: function (r) {
if (r.message) {
frm.set_value("body_text", r.message.body_text);
frm.set_value("closing_text", r.message.closing_text);
frm.set_value("language", r.message.language);
} else {
frm.set_value("body_text", "");
frm.set_value("closing_text", "");
}
},
frm.call("get_dunning_letter_text").then((r) => {
if (!r.exc) {
frm.refresh_fields();
}
});
}
},

View File

@@ -163,6 +163,46 @@ class Dunning(AccountsController):
"Serial and Batch Bundle",
]
@frappe.whitelist()
def get_dunning_letter_text(self):
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if not self.dunning_type:
return
filters = {"parent": self.dunning_type, "is_default_language": 1}
if self.language:
filters.pop("is_default_language")
filters["language"] = self.language
letter_text = frappe.db.get_value(DOCTYPE, filters, FIELDS, as_dict=True)
if not letter_text:
msg = (
_("Dunning Letter for Dunning Type {0} in language '{1}' not found.").format(
frappe.bold(self.dunning_type), frappe.bold(self.language)
)
if self.language
else _("Dunning Letter for Dunning Type {0} not found.").format(
frappe.bold(self.dunning_type)
)
)
frappe.msgprint(msg, alert=True, indicator="yellow")
self.body_text = (
frappe.render_template(letter_text.body_text, self.as_dict(), restrict_globals=True)
if letter_text
else None
)
self.closing_text = (
frappe.render_template(letter_text.closing_text, self.as_dict(), restrict_globals=True)
if letter_text
else None
)
self.language = letter_text.language if letter_text else self.language
def update_linked_dunnings(doc, previous_outstanding_amount):
if (
@@ -241,34 +281,3 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
& (overdue_payment.sales_invoice == sales_invoice)
)
).run(as_dict=True)
@frappe.whitelist()
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
doc = frappe.parse_json(doc)
if not language:
language = doc.get("language")
letter_text = None
if language:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.doctype.sales_invoice.mapper import (
create_dunning as create_dunning_from_sales_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice,
create_sales_invoice_against_cost_center,
)
from erpnext.tests.utils import ERPNextTestSuite
@@ -152,6 +153,37 @@ class TestDunning(ERPNextTestSuite):
dunning.reload()
self.assertEqual(dunning.status, "Unresolved")
@ERPNextTestSuite.change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)
def test_dunning_outstanding_uses_transaction_currency(self):
"""
Regression for #56006: dunning outstanding must be in the invoice transaction
currency, not in the party account currency.
A USD invoice posted against an INR receivable account stores
outstanding_amount in INR (party account currency). The overdue payment
row on the resulting Dunning must carry the USD amount, not the INR amount.
"""
si = create_sales_invoice(
posting_date=add_days(today(), -10),
currency="USD",
conversion_rate=50,
rate=100,
debit_to="Debtors - _TC",
)
# Sanity-check the invoice state before creating the dunning
self.assertEqual(si.currency, "USD")
self.assertEqual(si.outstanding_amount, 5000.0) # INR (party account currency)
self.assertEqual(si.payment_schedule[0].outstanding, 100.0) # USD (transaction currency)
dunning = create_dunning_from_sales_invoice(si.name)
self.assertEqual(len(dunning.overdue_payments), 1)
# Must reflect 100 USD, not 5000 INR mislabelled as USD
self.assertEqual(dunning.overdue_payments[0].outstanding, 100.0)
def test_dunning_not_affected_by_standalone_credit_note(self):
"""
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.

View File

@@ -3,7 +3,10 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import comma_and
from frappe.utils.jinja import validate_template
class DunningType(Document):
@@ -30,3 +33,134 @@ class DunningType(Document):
def autoname(self):
company_abbr = frappe.get_value("Company", self.company, "abbr")
self.name = f"{self.dunning_type} - {company_abbr}"
def validate(self):
self.validate_dunning_letter_text()
self.validate_income_account()
self.validate_cost_center()
self.set_default_dunning_type()
def validate_dunning_letter_text(self):
self.validate_languages()
self.validate_is_default_language()
self.validate_dunning_letter_text_templates()
def validate_income_account(self):
if not self.income_account:
return
account = frappe.get_cached_doc("Account", self.income_account)
msg = []
if account.company != self.company:
msg.append(
_(
"{0} doesn't belong to Company {1}. Please select an Income Account that belongs to Company {1}."
).format(frappe.bold(self.income_account), frappe.bold(self.company))
)
if account.disabled:
msg.append(
_("{0} is disabled. Please select a valid Income Account.").format(
frappe.bold(self.income_account)
)
)
if account.root_type != "Income":
msg.append(
_("{0} is not an Income Account. Please select a valid Income Account.").format(
frappe.bold(self.income_account)
)
)
if account.is_group:
msg.append(
_("{0} is a group account. Please select a non-group Income Account.").format(
frappe.bold(self.income_account)
)
)
if msg:
frappe.msgprint(
msg,
title=_("Income Account Validation Error"),
as_list=True,
raise_exception=frappe.ValidationError,
)
def validate_cost_center(self):
if not self.cost_center:
return
cost_center = frappe.get_cached_doc("Cost Center", self.cost_center)
msg = []
if cost_center.company != self.company:
msg.append(
_(
"{0} doesn't belong to Company {1}. Please select a Cost Center that belongs to Company {1}."
).format(frappe.bold(self.cost_center), frappe.bold(self.company))
)
if cost_center.disabled:
msg.append(
_("{0} is disabled. Please select an enabled Cost Center.").format(
frappe.bold(self.cost_center)
)
)
if cost_center.is_group:
msg.append(
_("{0} is a group Cost Center. Please select a non-group Cost Center.").format(
frappe.bold(self.cost_center)
)
)
if msg:
frappe.msgprint(
msg,
title=_("Cost Center Validation Error"),
as_list=True,
raise_exception=frappe.ValidationError,
)
def validate_languages(self):
languages = [d.language for d in self.dunning_letter_text]
if len(languages) == len(set(languages)):
return
frappe.throw(_("Duplicate languages found on Dunning Letter Text. Keep only one of them."))
def validate_is_default_language(self):
is_default_language_list = [
d.language for d in self.dunning_letter_text if d.is_default_language == 1
]
if len(is_default_language_list) <= 1:
return
frappe.throw(
_("{0} languages are marked as default languages. Please select only one of them.").format(
comma_and(is_default_language_list, add_quotes=True)
)
)
def validate_dunning_letter_text_templates(self):
for d in self.dunning_letter_text:
if d.body_text:
validate_template(d.body_text, restrict_globals=True)
if d.closing_text:
validate_template(d.closing_text, restrict_globals=True)
def set_default_dunning_type(self):
if self.is_default != 1:
return
frappe.db.set_value(
"Dunning Type",
{"company": self.company, "is_default": 1, "name": ["!=", self.name]},
"is_default",
0,
)

View File

@@ -1,9 +1,200 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.tests.utils import ERPNextTestSuite
def make_dunning_type(dunning_type, company="_Test Company", **kwargs):
doc = frappe.new_doc("Dunning Type")
doc.dunning_type = dunning_type
doc.company = company
doc.dunning_fee = kwargs.get("dunning_fee", 100)
doc.rate_of_interest = kwargs.get("rate_of_interest", 5)
doc.is_default = kwargs.get("is_default", 0)
if "income_account" in kwargs:
doc.income_account = kwargs["income_account"]
elif kwargs.get("income_account") is not False:
doc.income_account = "Sales - _TC" if company == "_Test Company" else "Sales - _TC1"
if "cost_center" in kwargs:
doc.cost_center = kwargs["cost_center"]
elif kwargs.get("cost_center") is not False:
doc.cost_center = "Main - _TC" if company == "_Test Company" else "Main - _TC1"
for row in kwargs.get("dunning_letter_text", [{"language": "en", "body_text": "Test body"}]):
doc.append("dunning_letter_text", row)
return doc
class TestDunningType(ERPNextTestSuite):
pass
def test_income_account_must_belong_to_company(self):
doc = make_dunning_type("_Test Dunning Wrong Company Account", income_account="Sales - _TC1")
self.assertRaisesRegex(frappe.ValidationError, "doesn't belong to Company", doc.insert)
def test_income_account_must_not_be_disabled(self):
disabled_account = frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Disabled Income Account",
"parent_account": "Direct Income - _TC",
"company": "_Test Company",
"account_type": "Income Account",
"disabled": 1,
}
).insert()
doc = make_dunning_type("_Test Dunning Disabled Account", income_account=disabled_account.name)
self.assertRaisesRegex(frappe.ValidationError, "is disabled", doc.insert)
def test_income_account_must_be_income_type(self):
doc = make_dunning_type("_Test Dunning Non Income Account", income_account="Debtors - _TC")
self.assertRaisesRegex(frappe.ValidationError, "is not an Income Account", doc.insert)
def test_income_account_must_not_be_group(self):
doc = make_dunning_type("_Test Dunning Group Account", income_account="Income - _TC")
self.assertRaisesRegex(frappe.ValidationError, "is a group account", doc.insert)
def test_income_account_is_optional(self):
doc = make_dunning_type("_Test Dunning No Income Account", income_account=False)
doc.insert()
self.assertFalse(doc.income_account)
def test_valid_income_account_passes(self):
doc = make_dunning_type("_Test Dunning Valid Income Account", income_account="Sales - _TC")
doc.insert()
self.assertEqual(doc.income_account, "Sales - _TC")
def test_cost_center_must_belong_to_company(self):
doc = make_dunning_type("_Test Dunning Wrong Company CC", cost_center="Main - _TC1")
self.assertRaisesRegex(frappe.ValidationError, "doesn't belong to Company", doc.insert)
def test_cost_center_must_not_be_disabled(self):
disabled_cc = frappe.get_doc(
{
"doctype": "Cost Center",
"cost_center_name": "_Test Disabled Cost Center",
"parent_cost_center": "_Test Company - _TC",
"company": "_Test Company",
"disabled": 1,
}
).insert()
doc = make_dunning_type("_Test Dunning Disabled CC", cost_center=disabled_cc.name)
self.assertRaisesRegex(frappe.ValidationError, "is disabled", doc.insert)
def test_cost_center_must_not_be_group(self):
doc = make_dunning_type("_Test Dunning Group CC", cost_center="_Test Company - _TC")
self.assertRaisesRegex(frappe.ValidationError, "is a group Cost Center", doc.insert)
def test_cost_center_is_optional(self):
doc = make_dunning_type("_Test Dunning No CC", cost_center=False)
doc.insert()
self.assertFalse(doc.cost_center)
def test_valid_cost_center_passes(self):
doc = make_dunning_type("_Test Dunning Valid CC", cost_center="Main - _TC")
doc.insert()
self.assertEqual(doc.cost_center, "Main - _TC")
def test_duplicate_languages_not_allowed(self):
doc = make_dunning_type(
"_Test Dunning Duplicate Language",
dunning_letter_text=[
{"language": "en", "body_text": "Body one"},
{"language": "en", "body_text": "Body two"},
],
)
self.assertRaisesRegex(frappe.ValidationError, "Duplicate languages found", doc.insert)
def test_unique_languages_allowed(self):
doc = make_dunning_type(
"_Test Dunning Unique Languages",
dunning_letter_text=[
{"language": "en", "body_text": "Body one"},
{"language": "de", "body_text": "Body two"},
],
)
doc.insert()
self.assertEqual(len(doc.dunning_letter_text), 2)
def test_only_one_default_language_allowed(self):
doc = make_dunning_type(
"_Test Dunning Multiple Default Language",
dunning_letter_text=[
{"language": "en", "body_text": "Body one", "is_default_language": 1},
{"language": "de", "body_text": "Body two", "is_default_language": 1},
],
)
self.assertRaisesRegex(
frappe.ValidationError, "languages are marked as default languages", doc.insert
)
def test_single_default_language_allowed(self):
doc = make_dunning_type(
"_Test Dunning Single Default Language",
dunning_letter_text=[
{"language": "en", "body_text": "Body one", "is_default_language": 1},
{"language": "de", "body_text": "Body two", "is_default_language": 0},
],
)
doc.insert()
self.assertEqual(doc.dunning_letter_text[0].is_default_language, 1)
def test_invalid_jinja_template_in_body_text_raises(self):
doc = make_dunning_type(
"_Test Dunning Invalid Body Template",
dunning_letter_text=[{"language": "en", "body_text": "{{ unclosed"}],
)
self.assertRaisesRegex(frappe.ValidationError, "Syntax error in template", doc.insert)
def test_invalid_jinja_template_in_closing_text_raises(self):
doc = make_dunning_type(
"_Test Dunning Invalid Closing Template",
dunning_letter_text=[
{"language": "en", "body_text": "Valid body", "closing_text": "{{ unclosed"}
],
)
self.assertRaisesRegex(frappe.ValidationError, "Syntax error in template", doc.insert)
def test_valid_jinja_template_passes(self):
doc = make_dunning_type(
"_Test Dunning Valid Template",
dunning_letter_text=[
{
"language": "en",
"body_text": "Outstanding amount is {{ outstanding_amount }}",
"closing_text": "Regards, {{ company }}",
}
],
)
doc.insert()
self.assertTrue(doc.name)
def test_set_default_dunning_type_unsets_previous_default(self):
first = make_dunning_type("_Test Dunning Default One", is_default=1)
first.insert()
self.assertEqual(frappe.db.get_value("Dunning Type", first.name, "is_default"), 1)
second = make_dunning_type("_Test Dunning Default Two", is_default=1)
second.insert()
self.assertEqual(frappe.db.get_value("Dunning Type", first.name, "is_default"), 0)
self.assertEqual(frappe.db.get_value("Dunning Type", second.name, "is_default"), 1)
def test_set_default_dunning_type_scoped_per_company(self):
company_1 = make_dunning_type("_Test Dunning Default Co1", is_default=1)
company_1.insert()
company_2 = make_dunning_type(
"_Test Dunning Default Co2",
company="_Test Company 1",
is_default=1,
)
company_2.insert()
self.assertEqual(frappe.db.get_value("Dunning Type", company_1.name, "is_default"), 1)
self.assertEqual(frappe.db.get_value("Dunning Type", company_2.name, "is_default"), 1)

View File

@@ -22,17 +22,27 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
refresh: function (frm) {
if (frm.doc.docstatus == 1) {
frappe.call({
method: "check_journal_entry_condition",
method: "check_journal_and_reversal",
doc: frm.doc,
callback: function (r) {
if (r.message) {
frm.add_custom_button(
__("Journal Entries"),
function () {
return frm.events.make_jv(frm);
},
__("Create")
);
if (!r.message.journals_posted) {
frm.add_custom_button(
__("Journal Entries"),
function () {
return frm.events.make_jv(frm);
},
__("Create")
);
} else if (!r.message.reversals_posted) {
frm.add_custom_button(
__("Reversal Journal Entries"),
function () {
return frm.events.make_reverse_journal(frm);
},
__("Create")
);
}
}
},
});
@@ -100,6 +110,14 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
},
});
},
make_reverse_journal: function (frm) {
frappe.call({
method: "make_reverse_journal",
doc: frm.doc,
freeze: true,
freeze_message: __("Reversing Journals..."),
});
},
});
frappe.ui.form.on("Exchange Rate Revaluation Account", {

View File

@@ -9,7 +9,7 @@ from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import Max, NullIf, Sum
from frappe.utils import flt, get_link_to_form
from frappe.utils import flt, get_link_to_form, nowdate
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@@ -91,25 +91,31 @@ class ExchangeRateRevaluation(Document):
)
def on_cancel(self):
self.ignore_linked_doctypes = "GL Entry"
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
@frappe.whitelist()
def check_journal_entry_condition(self):
def check_journal_and_reversal(self):
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journals_posted = False
reversals_posted = False
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(jea)
.select(jea.parent)
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.isnull()) # omit journals that have reversals
)
.run()
.run(pluck="name")
)
if journals:
gle = qb.DocType("GL Entry")
total_amt = (
@@ -124,12 +130,31 @@ class ExchangeRateRevaluation(Document):
.run()
)
if total_amt and total_amt[0][0] != self.total_gain_loss:
return True
if total_amt and total_amt[0][0] == self.total_gain_loss:
journals_posted = True
else:
return False
journals_posted = False
return True
# reverse journals
reverse_journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.notnull())
)
.run(pluck="name")
)
if reverse_journals:
reversals_posted = True
else:
reversals_posted = False
return {"journals_posted": journals_posted, "reversals_posted": reversals_posted}
def fetch_and_calculate_accounts_data(self):
accounts = self.get_accounts_data()
@@ -347,6 +372,7 @@ class ExchangeRateRevaluation(Document):
@frappe.whitelist()
def make_jv_entries(self):
frappe.has_permission("Journal Entry", "write", throw=True)
zero_balance_jv = self.make_jv_for_zero_balance()
if zero_balance_jv:
frappe.msgprint(
@@ -575,6 +601,50 @@ class ExchangeRateRevaluation(Document):
journal_entry.save()
return journal_entry
@frappe.whitelist()
def make_reverse_journal(self):
frappe.has_permission("Journal Entry", "write", throw=True)
je = qb.DocType("Journal Entry")
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.name)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
& (je.reversal_of.isnull()) # omit journals that have reversals
)
.run(pluck="name")
)
if journals:
from erpnext.accounts.doctype.journal_entry.mapper import make_reverse_journal_entry
if drafts := frappe.db.get_all(
"Journal Entry",
filters={"docstatus": 0, "reversal_of": ["in", journals]},
pluck="name",
as_list=1,
):
part = "journals are" if len(drafts) > 1 else "journal is"
doc_links = ", ".join(["{}".format(get_link_to_form("Journal Entry", x)) for x in drafts])
frappe.throw(
msg=_("Reverse {0} already available in draft status: {1}").format(part, doc_links),
)
else:
for x in journals:
reversal = make_reverse_journal_entry(x)
reversal.posting_date = nowdate()
reversal.save()
frappe.msgprint(
_("A draft reverse journal for {0} has been created: {1}").format(
frappe.bold(x), get_link_to_form("Journal Entry", reversal.name)
)
)
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
"""
@@ -601,6 +671,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
.select(gl.voucher_type, gl.voucher_no)
.where(Criterion.all(conditions))
.orderby(gl.posting_date, order=Order.desc)
.orderby(gl.name, order=Order.desc)
.limit(1)
.run()[0]
)
@@ -615,6 +686,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
(gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account)
)
.orderby(gl.posting_date, order=Order.desc)
.orderby(gl.name, order=Order.desc)
.limit(1)
.run()[0][0]
)

View File

@@ -9,11 +9,10 @@ from frappe.utils import add_days, flt, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
class TestExchangeRateRevaluation(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company"
self.item = "_Test Item"
@@ -23,14 +22,6 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
self.set_system_and_company_settings()
def set_system_and_company_settings(self):
# set number and currency precision
system_settings = frappe.get_doc("System Settings")
system_settings.float_precision = 2
system_settings.currency_precision = 2
system_settings.language = "en"
system_settings.time_zone = "Asia/Kolkata"
system_settings.save()
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
@@ -132,7 +123,8 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
@@ -221,7 +213,8 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
@@ -298,3 +291,159 @@ class TestExchangeRateRevaluation(ERPNextTestSuite, AccountsTestMixin):
for key, _val in expected_data.items():
self.assertEqual(expected_data.get(key), account_details.get(key))
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_05_revaluation_journal_reversal(self):
"""
Test reversing of revaluation journals
"""
si = create_sales_invoice(
item=self.item,
company=self.company,
customer="_Test Customer 1",
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = self.company
err.posting_date = today()
err.fetch_and_calculate_accounts_data()
self.assertEqual(len(err.accounts), 1)
err.save().submit()
gain_loss_account = err.get_for_unrealized_gain_loss_account()
usd_account = err.accounts[0].account
old_balance = err.accounts[0].balance_in_base_currency
new_balance = err.accounts[0].new_balance_in_base_currency
total_gain_loss = err.total_gain_loss
# Create JV for ERR
ret = err.check_journal_and_reversal()
self.assertFalse(ret.get("journals_posted"))
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
self.assertEqual(len(je.accounts), 3)
# A gain is credited to the gain/loss account, a loss is debited. The current
# exchange rate (from master data) may sit either side of the booked rate, so
# derive the column from the sign instead of assuming a gain.
gain_loss_debit = abs(total_gain_loss) if total_gain_loss < 0 else 0.0
gain_loss_credit = total_gain_loss if total_gain_loss > 0 else 0.0
expected = [
(usd_account, new_balance, 0.0, 100.0, 0.0),
(usd_account, 0.0, old_balance, 0.0, 100.0),
(gain_loss_account, gain_loss_debit, gain_loss_credit, gain_loss_debit, gain_loss_credit),
]
actual = []
for acc in je.accounts:
actual.append(
(
acc.account,
acc.debit,
acc.credit,
acc.debit_in_account_currency,
acc.credit_in_account_currency,
)
)
self.assertEqual(expected, actual)
# Assert reversals are not posted
ret = err.check_journal_and_reversal()
self.assertTrue(ret.get("journals_posted"))
self.assertFalse(ret.get("reversals_posted"))
err.make_reverse_journal()
# submit
draft = frappe.db.get_all(
"Journal Entry",
filters={"docstatus": 0, "reversal_of": je.name, "voucher_type": "Exchange Rate Revaluation"},
pluck="name",
as_list=1,
)
self.assertIsNotNone(draft)
frappe.get_doc("Journal Entry", draft[0]).submit()
ret = err.check_journal_and_reversal()
self.assertTrue(ret.get("journals_posted"))
self.assertTrue(ret.get("reversals_posted"))
reverse_jv = frappe.db.get_all(
"Journal Entry", filters={"reversal_of": err_journals.get("revaluation_jv")}, pluck="name"
)
self.assertIsNotNone(reverse_jv)
class TestExchangeRateRevaluationValidation(ERPNextTestSuite):
"""Validation and gain/loss calculation paths, exercised on the document directly
so they don't need the multi-currency GL setup the integration tests above build."""
def setUp(self):
frappe.set_user("Administrator")
self.company = "_Test Company"
def _revaluation_with_rows(self, rows, rounding_loss_allowance=0.05):
doc = frappe.new_doc("Exchange Rate Revaluation")
doc.company = self.company
doc.posting_date = today()
doc.rounding_loss_allowance = rounding_loss_allowance
for row in rows:
doc.append("accounts", row)
return doc
def test_rounding_loss_allowance_must_be_between_0_and_1(self):
for bad in (-0.1, 1, 1.5):
doc = self._revaluation_with_rows([], rounding_loss_allowance=bad)
self.assertRaises(frappe.ValidationError, doc.validate)
# values inside [0, 1) are accepted, at the lower bound and mid-range
for good in (0.0, 0.5):
self._revaluation_with_rows([], rounding_loss_allowance=good).validate()
def test_gain_loss_computed_and_split_by_zero_balance(self):
doc = self._revaluation_with_rows(
[
# open (unbooked) row: base balance moved 1000 -> 1100, a 100 gain
{"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100},
# already-settled (zero_balance) row carries a booked loss of 40
{"zero_balance": 1, "gain_loss": -40},
]
)
doc.validate()
# gain_loss is derived only for open rows; the zero-balance row keeps its value
self.assertEqual(doc.accounts[0].gain_loss, 100)
self.assertEqual(doc.gain_loss_unbooked, 100)
self.assertEqual(doc.gain_loss_booked, -40)
self.assertEqual(doc.total_gain_loss, 60)
def test_before_submit_drops_rows_without_gain_loss(self):
doc = self._revaluation_with_rows(
[
{"zero_balance": 0, "balance_in_base_currency": 1000, "new_balance_in_base_currency": 1100},
{"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500},
]
)
doc.validate() # second row nets to a 0 gain_loss
doc.remove_accounts_without_gain_loss()
self.assertEqual(len(doc.accounts), 1)
self.assertEqual(doc.accounts[0].gain_loss, 100)
def test_before_submit_requires_at_least_one_gain_loss_row(self):
doc = self._revaluation_with_rows(
[{"zero_balance": 0, "balance_in_base_currency": 500, "new_balance_in_base_currency": 500}]
)
doc.validate()
self.assertRaises(frappe.ValidationError, doc.remove_accounts_without_gain_loss)

View File

@@ -255,16 +255,27 @@ class FinancialReportEngine:
if filters.get("presentation_currency"):
frappe.msgprint(
title=_("Unsupported Feature"),
msg=_("Currency filters are currently unsupported in Custom Financial Report."),
indicator="orange",
title=_("Not Supported"),
msg=_("Currency filters are currently unsupported in Custom Financial Report"),
)
# Margin view is dependent on first row being an income account. Hence not supported.
# Way to implement this would be using calculated rows with formulas.
supported_views = ("Report", "Growth")
if (view := filters.get("selected_view")) and view not in supported_views:
frappe.msgprint(_("{0} view is currently unsupported in Custom Financial Report.").format(view))
frappe.msgprint(
indicator="orange",
title=_("Not Supported"),
msg=_("{0} view is currently unsupported in Custom Financial Report").format(view),
)
if filters.get("group_by_dimension"):
frappe.msgprint(
indicator="orange",
title=_("Not Supported"),
msg=_("Dimension-based grouping is currently unsupported in Custom Financial Report"),
)
def _initialize_context(self, filters: dict[str, Any]) -> ReportContext:
template_name = filters.get("report_template")
@@ -1860,28 +1871,51 @@ class GrowthViewTransformer:
self.formatted_rows = context.raw_data.get("formatted_data", [])
self.period_list = context.period_list
def transform(self) -> None:
def transform(self):
for row_data in self.formatted_rows:
if row_data.get("is_blank_line"):
continue
transformed_values = {}
for i in range(len(self.period_list)):
current_period = self.period_list[i]["key"]
if row_data.get("segment_values"):
self._transform_segmented_row(row_data)
else:
self._transform_single_row(row_data)
current_value = row_data[current_period]
previous_value = row_data[self.period_list[i - 1]["key"]] if i != 0 else 0
def _compute_growth_values(self, source: dict) -> dict:
transformed = {}
if i == 0:
transformed_values[current_period] = current_value
else:
growth_percent = self._calculate_growth(previous_value, current_value)
transformed_values[current_period] = growth_percent
for i, period in enumerate(self.period_list):
current_period = period["key"]
current_value = source.get(current_period)
row_data.update(transformed_values)
if current_value in (None, ""):
continue
if i == 0:
transformed[current_period] = current_value
else:
previous_period = self.period_list[i - 1]["key"]
previous_value = source.get(previous_period) or 0
transformed[current_period] = self._calculate_growth(previous_value, current_value)
return transformed
def _transform_single_row(self, row_data: dict):
row_data.update(self._compute_growth_values(row_data))
def _transform_segmented_row(self, row_data: dict):
for seg_id, seg_data in row_data.get("segment_values", {}).items():
if seg_data.get("is_blank_line"):
continue
transformed = self._compute_growth_values(seg_data)
seg_data.update(transformed)
for period_key, value in transformed.items():
row_data[f"{seg_id}_{period_key}"] = value
def _calculate_growth(self, previous_value: float, current_value: float) -> float | None:
if current_value is None:
if current_value in (None, ""):
return None
if previous_value == 0 and current_value > 0:

View File

@@ -107,6 +107,9 @@ def auto_create_fiscal_year():
)
for d in fiscal_year:
# savepoint so a duplicate-year INSERT (Fiscal Year autoname=field:year) that aborts the
# statement doesn't poison the whole scheduler transaction on Postgres and kill the next iteration
frappe.db.savepoint("auto_create_fiscal_year")
try:
current_fy = frappe.get_doc("Fiscal Year", d[0])
@@ -127,7 +130,7 @@ def auto_create_fiscal_year():
new_fy.insert(ignore_permissions=True)
except frappe.NameError:
pass
frappe.db.rollback(save_point="auto_create_fiscal_year")
def get_from_and_to_date(fiscal_year):

View File

@@ -471,6 +471,25 @@ def on_doctype_update():
frappe.db.add_index("GL Entry", ["posting_date", "company"])
frappe.db.add_index("GL Entry", ["party_type", "party"])
if frappe.db.db_type == "postgres":
# Postgres-only partial/covering indexes for the financial reports (General Ledger, Trial
# Balance, Balance Sheet, P&L), which always filter `is_cancelled = 0` and scope by company.
# `where`/`include` are no-ops on MariaDB and its optimizer ignores these anyway, so they are
# added only on postgres to avoid dead write overhead on this insert-hot table.
frappe.db.add_index(
"GL Entry",
["company", "posting_date", "account"],
index_name="gle_active_detail",
where="is_cancelled = 0",
)
frappe.db.add_index(
"GL Entry",
["company", "account", "posting_date"],
index_name="gle_active_cover",
where="is_cancelled = 0",
include=["debit", "credit"],
)
def rename_gle_sle_docs():
for doctype in ["GL Entry", "Stock Ledger Entry"]:

View File

@@ -1,8 +1,62 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
TAX_ACCOUNT = "_Test Account VAT - _TC"
RECEIVABLE_ACCOUNT = "Debtors - _TC"
class TestItemTaxTemplate(ERPNextTestSuite):
pass
"""Item Tax Template validates its tax rows: each account must belong to the
company, be a tax-like account type, and appear only once."""
def setUp(self):
frappe.set_user("Administrator")
def make_template(self, rows, title="_Test ITT"):
doc = frappe.new_doc("Item Tax Template")
doc.title = f"{title} {frappe.generate_hash(length=6)}"
doc.company = COMPANY
for account, rate, not_applicable in rows:
doc.append(
"taxes",
{"tax_type": account, "tax_rate": rate, "not_applicable": not_applicable},
)
return doc
def test_valid_template_saves_and_is_named_with_abbr(self):
doc = self.make_template([(TAX_ACCOUNT, 9, 0)])
doc.insert()
self.assertTrue(doc.name.endswith(" - _TC"))
self.assertTrue(doc.name.startswith(doc.title))
def test_duplicate_tax_type_throws(self):
doc = self.make_template([(TAX_ACCOUNT, 9, 0), (TAX_ACCOUNT, 5, 0)])
self.assertRaises(frappe.ValidationError, doc.insert)
def test_account_of_wrong_company_throws(self):
other_account = frappe.db.get_value("Account", {"company": "_Test Company 1", "is_group": 0}, "name")
self.assertTrue(other_account, "need a non-group account in _Test Company 1")
doc = self.make_template([(other_account, 9, 0)])
self.assertRaises(frappe.ValidationError, doc.insert)
def test_disallowed_account_type_throws(self):
# a Receivable account is not Tax/Chargeable/Income/Expense
doc = self.make_template([(RECEIVABLE_ACCOUNT, 9, 0)])
self.assertRaises(frappe.ValidationError, doc.insert)
def test_not_applicable_row_has_rate_zeroed(self):
doc = self.make_template([(TAX_ACCOUNT, 18, 1)])
doc.insert()
self.assertEqual(doc.taxes[0].tax_rate, 0)
def test_negative_tax_rate_is_accepted(self):
# SUSPECTED BUG: validate never bounds tax_rate, so a negative (or >100) rate
# saves silently. Locking the current (wrong) behaviour.
doc = self.make_template([(TAX_ACCOUNT, -5, 0)])
doc.insert()
self.assertEqual(doc.taxes[0].tax_rate, -5)

View File

@@ -29,7 +29,7 @@ frappe.ui.form.on("Journal Entry", {
refresh(frm) {
if (frm.doc.reversal_of && (frm.is_new() || frm.doc.docstatus == 0)) {
frm.set_read_only();
erpnext.journal_entry.lock_reversal_entry(frm);
}
erpnext.toggle_naming_series();
@@ -232,6 +232,14 @@ Object.assign(erpnext.journal_entry, {
}
},
lock_reversal_entry(frm) {
frm.fields
.filter((field) => field.has_input)
.filter((field) => field.df.fieldname != "posting_date")
.forEach((field) => frm.set_df_property(field.df.fieldname, "read_only", 1));
frm.set_df_property("accounts", "read_only", 1);
},
add_custom_buttons(frm) {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(

View File

@@ -1,7 +1,10 @@
frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"],
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark", "reversal_of"],
get_indicator: function (doc) {
if (doc.docstatus === 1) {
if (doc.reversal_of && doc.voucher_type == "Exchange Rate Revaluation") {
return [__("Reversal Of Exchange Rate Revaluation"), "blue"];
}
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];
}
},

View File

@@ -220,7 +220,7 @@ 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 | Document | None = None) -> Document:
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)."""
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
if existing_reverse:

View File

@@ -94,11 +94,12 @@ class AssetService:
def update_journal_entry_link_on_depr_schedule(self, asset, je_row) -> None:
"""Stamp this entry onto the matching (date + amount) depreciation schedule row."""
depr_schedule = get_depr_schedule(asset.name, "Active", self.doc.finance_book)
precision = je_row.precision("debit")
for d in depr_schedule or []:
if (
d.schedule_date == self.doc.posting_date
and not d.journal_entry
and d.depreciation_amount == flt(je_row.debit)
and flt(d.depreciation_amount, precision) == flt(je_row.debit, precision)
):
frappe.db.set_value("Depreciation Schedule", d.name, "journal_entry", self.doc.name)

View File

@@ -45,6 +45,20 @@ class JournalEntryTemplate(Document):
def validate(self):
self.validate_party()
self.validate_account_company()
def validate_account_company(self):
"""Each row's account must belong to the template's company."""
for account in self.accounts:
if (
account.account
and frappe.get_cached_value("Account", account.account, "company") != self.company
):
frappe.throw(
_("Row {0}: Account {1} does not belong to company {2}").format(
account.idx, account.account, self.company
)
)
def validate_party(self):
"""

View File

@@ -1,9 +1,45 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
class TestJournalEntryTemplate(ERPNextTestSuite):
pass
"""Journal Entry Template's only real rule is validate_party: party_type is
allowed only on Receivable/Payable accounts, and a party needs a party_type."""
def setUp(self):
frappe.set_user("Administrator")
def make_template(self, rows, company=COMPANY):
doc = frappe.new_doc("Journal Entry Template")
doc.template_title = f"_Test JET {frappe.generate_hash(length=6)}"
doc.company = company
doc.voucher_type = "Journal Entry"
doc.naming_series = frappe.get_meta("Journal Entry").get_field("naming_series").options.split("\n")[0]
for row in rows:
doc.append("accounts", row)
return doc
def test_party_type_only_on_receivable_or_payable_account(self):
# Cash is neither Receivable nor Payable, so a party_type here is invalid
doc = self.make_template([{"account": "Cash - _TC", "party_type": "Customer"}])
self.assertRaises(frappe.ValidationError, doc.validate)
def test_party_requires_party_type(self):
doc = self.make_template([{"account": "Debtors - _TC", "party": "_Test Customer"}])
self.assertRaises(frappe.ValidationError, doc.validate)
def test_account_from_other_company_is_rejected(self):
other_receivable = frappe.db.get_value(
"Account", {"company": "_Test Company 1", "account_type": "Receivable", "is_group": 0}, "name"
)
self.assertTrue(other_receivable, "need a receivable account in _Test Company 1")
doc = self.make_template(
[{"account": other_receivable, "party_type": "Customer", "party": "_Test Customer"}]
)
self.assertRaises(frappe.ValidationError, doc.insert)

View File

@@ -65,6 +65,7 @@ def start_merge(docname):
total = len(ledger_merge.merge_accounts)
for row in ledger_merge.merge_accounts:
if not row.merged:
frappe.db.savepoint("ledger_merge_row")
try:
merge_account(
row.account,
@@ -79,8 +80,7 @@ def start_merge(docname):
{"ledger_merge": ledger_merge.name, "current": successful_merges, "total": total},
)
except Exception:
if not frappe.in_test:
frappe.db.rollback()
frappe.db.rollback(save_point="ledger_merge_row")
ledger_merge.log_error("Ledger merge failed")
finally:
if successful_merges == total:

View File

@@ -8,7 +8,7 @@ frappe.ui.form.on("Loyalty Program", {
var help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
${__("Notes")}
</h4>
<ul>

View File

@@ -5,9 +5,59 @@ import frappe
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
class TestModeofPayment(ERPNextTestSuite):
pass
"""Mode of Payment validates its per-company default accounts (account company
must match the row, no company twice) and blocks disabling while a POS Profile
still references it."""
def setUp(self):
frappe.set_user("Administrator")
def make_mop(self, accounts=None, enabled=1):
doc = frappe.new_doc("Mode of Payment")
doc.mode_of_payment = f"_Test MoP {frappe.generate_hash(length=6)}"
doc.type = "General"
doc.enabled = enabled
for company, account in accounts or []:
doc.append("accounts", {"company": company, "default_account": account})
return doc
def test_valid_mode_of_payment_saves(self):
doc = self.make_mop(accounts=[(COMPANY, "Cash - _TC")])
doc.insert()
self.assertTrue(doc.name)
def test_account_of_wrong_company_throws(self):
other_account = frappe.db.get_value("Account", {"company": "_Test Company 1", "is_group": 0}, "name")
self.assertTrue(other_account, "need a non-group account in _Test Company 1")
doc = self.make_mop(accounts=[(COMPANY, other_account)])
self.assertRaises(frappe.ValidationError, doc.insert)
def test_repeating_company_throws(self):
doc = self.make_mop(accounts=[(COMPANY, "Cash - _TC"), (COMPANY, "Debtors - _TC")])
self.assertRaises(frappe.ValidationError, doc.insert)
def test_disabling_mode_referenced_by_pos_profile_is_not_blocked(self):
# SUSPECTED BUG: validate_pos_mode_of_payment queries "Sales Invoice Payment"
# rows with parenttype "POS Profile", but a POS Profile's payments are stored
# as "POS Payment Method" rows. The filter never matches, so the guard is dead
# and a mode still referenced by a POS Profile disables without complaint.
# Locking the current (wrong) behaviour so a fix to the guard trips this test.
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
make_pos_profile() # its payments row references the "Cash" mode of payment
cash = frappe.get_doc("Mode of Payment", "Cash")
cash.enabled = 0
cash.save()
self.assertEqual(frappe.db.get_value("Mode of Payment", "Cash", "enabled"), 0)
def test_disabling_unreferenced_mode_succeeds(self):
doc = self.make_mop(accounts=[(COMPANY, "Cash - _TC")], enabled=0)
doc.insert()
self.assertEqual(doc.enabled, 0)
def set_default_account_for_mode_of_payment(mode_of_payment, company, account):

View File

@@ -1,8 +1,67 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.utils import getdate
from erpnext.accounts.doctype.monthly_distribution.monthly_distribution import (
get_percentage,
get_periodwise_distribution_data,
)
from erpnext.tests.utils import ERPNextTestSuite
class TestMonthlyDistribution(ERPNextTestSuite):
pass
"""Monthly Distribution spreads an amount across months. validate() enforces a
100% total; get_percentage() sums the months that fall inside a period window."""
def setUp(self):
frappe.set_user("Administrator")
def make_distribution(self, allocations):
doc = frappe.new_doc("Monthly Distribution")
doc.distribution_id = f"_Test MD {frappe.generate_hash(length=6)}"
for month, pct in allocations:
doc.append("percentages", {"month": month, "percentage_allocation": pct})
return doc
def test_get_months_populates_twelve_even_rows(self):
doc = frappe.new_doc("Monthly Distribution")
doc.distribution_id = "_Test MD Even"
doc.get_months()
self.assertEqual(len(doc.percentages), 12)
self.assertEqual(doc.percentages[0].month, "January")
self.assertEqual(doc.percentages[-1].month, "December")
self.assertEqual([d.idx for d in doc.percentages], list(range(1, 13)))
for d in doc.percentages:
self.assertAlmostEqual(d.percentage_allocation, 100.0 / 12, places=4)
# the auto-populated rows round to exactly 100 and pass validation
doc.validate()
def test_validate_rejects_total_other_than_100(self):
doc = self.make_distribution([("January", 50), ("February", 30)]) # sums to 80
self.assertRaises(frappe.ValidationError, doc.insert)
def test_get_percentage_sums_period_window(self):
doc = self.make_distribution([("January", 50), ("February", 30), ("March", 20)])
doc.insert() # total is 100, so validate passes
# a quarter starting in January covers Jan+Feb+Mar
self.assertEqual(get_percentage(doc, getdate("2026-01-01"), 3), 100)
# a single month picks up only that month
self.assertEqual(get_percentage(doc, getdate("2026-02-01"), 1), 30)
# months with no row simply contribute 0 (there is no guard that all 12 exist)
self.assertEqual(get_percentage(doc, getdate("2026-04-01"), 1), 0)
def test_periodwise_distribution_maps_each_period(self):
doc = self.make_distribution([("January", 50), ("February", 30), ("March", 20)])
doc.insert()
period_list = [
frappe._dict(key="q1", from_date=getdate("2026-01-01")),
frappe._dict(key="q2", from_date=getdate("2026-04-01")),
]
data = get_periodwise_distribution_data(doc.name, period_list, "Quarterly")
self.assertEqual(data["q1"], 100) # Jan+Feb+Mar
self.assertEqual(data["q2"], 0) # Apr+May+Jun carry no allocation

View File

@@ -24,15 +24,22 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
setTimeout(
() => {
frm.doc.import_in_progress = false;
frm.clear_table("invoices");
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress();
if (frm.doc.invoice_type == "Sales") {
frappe.msgprint(__("Opening Sales Invoices have been created."));
if (!data.errors) {
frm.clear_table("invoices");
frm.refresh_fields();
const message =
frm.doc.invoice_type == "Sales"
? __("Opening Sales Invoice(s) have been created.")
: __("Opening Purchase Invoice(s) have been created.");
frappe.show_alert({
message: message,
indicator: "green",
});
} else {
frappe.msgprint(__("Opening Purchase Invoices have been created."));
frm.refresh_fields();
}
},
1500,

View File

@@ -281,6 +281,7 @@ class OpeningInvoiceCreationTool(Document):
def start_import(invoices):
errors = 0
names = []
total = len(invoices)
for idx, d in enumerate(invoices):
# Scope each invoice to a savepoint so a failure only undoes that invoice.
# A plain rollback() would discard the whole transaction — including invoices
@@ -289,11 +290,11 @@ def start_import(invoices):
# postgres they would be lost). Rolling back to a savepoint keeps both.
savepoint = f"opening_invoice_{frappe.generate_hash(length=8)}"
frappe.db.savepoint(savepoint)
is_last = idx == total - 1
try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert(set_name=invoice_number)
@@ -301,10 +302,12 @@ def start_import(invoices):
if not frappe.in_test:
frappe.db.commit()
names.append(doc.name)
publish(idx, total, d.doctype, errors=errors if is_last else None)
except Exception:
errors += 1
frappe.db.rollback(save_point=savepoint)
doc.log_error("Opening invoice creation failed")
publish(idx, total, d.doctype, errors=errors if is_last else None)
if errors:
frappe.msgprint(
_("You had {0} errors while creating opening invoices. Check {1} for more details").format(
@@ -316,7 +319,7 @@ def start_import(invoices):
return names
def publish(index, total, doctype):
def publish(index, total, doctype, errors=None):
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
@@ -324,6 +327,7 @@ def publish(index, total, doctype):
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
count=index + 1,
total=total,
errors=errors,
),
user=frappe.session.user,
)

View File

@@ -82,6 +82,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
@@ -136,7 +137,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-04-29 17:08:15.617047",
"modified": "2026-07-02 15:17:11.938499",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",

View File

@@ -1,9 +1,67 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.tests.utils import ERPNextTestSuite
CUSTOMER = "_Test Customer"
SUPPLIER = "_Test Supplier"
SUPPLIER_2 = "_Test Supplier 1"
class TestPartyLink(ERPNextTestSuite):
pass
"""Party Link ties a Customer and a Supplier together as one underlying party.
validate() constrains the primary role and blocks duplicate links."""
def setUp(self):
frappe.set_user("Administrator")
def test_create_party_link_with_customer_primary(self):
link = create_party_link("Customer", CUSTOMER, SUPPLIER)
self.assertEqual(link.primary_role, "Customer")
self.assertEqual(link.secondary_role, "Supplier")
self.assertEqual(link.primary_party, CUSTOMER)
self.assertEqual(link.secondary_party, SUPPLIER)
self.assertTrue(frappe.db.exists("Party Link", link.name))
def test_create_party_link_with_supplier_primary(self):
link = create_party_link("Supplier", SUPPLIER, CUSTOMER)
self.assertEqual(link.primary_role, "Supplier")
self.assertEqual(link.secondary_role, "Customer")
self.assertEqual(link.primary_party, SUPPLIER)
self.assertEqual(link.secondary_party, CUSTOMER)
self.assertTrue(frappe.db.exists("Party Link", link.name))
def test_primary_role_must_be_customer_or_supplier(self):
doc = frappe.new_doc("Party Link")
doc.primary_role = "Employee"
doc.primary_party = CUSTOMER
doc.secondary_role = "Supplier"
doc.secondary_party = SUPPLIER
# validate() alone isolates the role rule from the dynamic-link checks
self.assertRaises(frappe.ValidationError, doc.validate)
def test_duplicate_link_throws(self):
create_party_link("Customer", CUSTOMER, SUPPLIER)
dup = frappe.new_doc("Party Link")
dup.primary_role = "Customer"
dup.primary_party = CUSTOMER
dup.secondary_role = "Supplier"
dup.secondary_party = SUPPLIER
self.assertRaises(frappe.ValidationError, dup.insert)
def test_party_can_wrongly_be_primary_in_two_links(self):
# SUSPECTED BUG: the uniqueness checks are asymmetric - a party already a
# *primary* in another link isn't blocked, so one customer can be linked to two
# different suppliers, breaking the 1:1 mapping. Locking the current (wrong)
# behaviour so a fix that blocks primary reuse trips this test.
create_party_link("Customer", CUSTOMER, SUPPLIER)
link2 = frappe.new_doc("Party Link")
link2.primary_role = "Customer"
link2.primary_party = CUSTOMER
link2.secondary_role = "Supplier"
link2.secondary_party = SUPPLIER_2
link2.insert()
self.assertTrue(frappe.db.exists("Party Link", link2.name))

View File

@@ -414,21 +414,17 @@ frappe.ui.form.on("Payment Entry", {
show_general_ledger: function (frm) {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
"fa fa-table"
);
frm.add_custom_button(__("Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
});
}
},

View File

@@ -514,10 +514,12 @@ class PaymentEntry(AccountsController):
invoice_names.add((ref.reference_doctype, ref.reference_name))
for doctype, name in invoice_names:
frappe.db.savepoint("subscription_update")
try:
doc = frappe.get_doc(doctype, name)
doc.refresh_subscription_status()
except Exception:
frappe.db.rollback(save_point="subscription_update")
frappe.log_error(_("Failed to update subscription status for {0} {1}").format(doctype, name))
def set_missing_values(self):
@@ -2422,6 +2424,9 @@ def get_party_details(company: str, party_type: str, party: str, date: str, cost
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
ptype = "select" if frappe.only_has_select_perm(party_type) else "read"
frappe.has_permission(party_type, ptype, party, throw=True)
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
@@ -2429,7 +2434,7 @@ def get_party_details(company: str, party_type: str, party: str, date: str, cost
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party, ignore_permissions=False)
return {
"party_account": party_account,
@@ -2525,9 +2530,7 @@ def get_reference_details(
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
else:
exchange_rate = 1
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
reference_name, party_type, party
)
outstanding_amount, total_amount = get_outstanding_on_journal_entry(reference_name, party_type, party)
elif reference_doctype == "Payment Entry":
if reverse_payment_details := frappe.db.get_all(
@@ -2793,6 +2796,9 @@ def get_open_payment_requests_for_references(references=None):
.where(PR.docstatus == 1)
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
# unique tiebreaker so PRs sharing a transaction_date allocate in the same order on both engines
.orderby(PR.creation, order=frappe.qb.asc)
.orderby(PR.name, order=frappe.qb.asc)
).run(as_dict=True)
if not response:
@@ -3039,13 +3045,11 @@ def set_paid_amount_and_received_amount(
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
if bank and company_currency != bank.account_currency:
# doc currency can be different from bank currency
posting_date = doc.get("posting_date") or doc.get("transaction_date")
conversion_rate = get_exchange_rate(
bank.account_currency, party_account_currency, posting_date
)
conversion_rate = get_exchange_rate(bank.account_currency, party_account_currency)
received_amount = paid_amount / conversion_rate
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
conversion_rate = get_exchange_rate(doc.get("currency", company_currency), company_currency)
received_amount = paid_amount * conversion_rate
# if payment type is pay, then paid amount and received amount are swapped
if payment_type == "Pay":
@@ -3271,7 +3275,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
@frappe.whitelist()
def make_payment_order(source_name: str, target_doc: str | Document | None = None):
def make_payment_order(source_name: str, target_doc: str | dict | Document | None = None):
from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target):

View File

@@ -246,6 +246,62 @@ class TestPaymentEntry(ERPNextTestSuite):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_pay_multiple_purchase_invoices_in_one_entry(self):
pi1 = make_purchase_invoice() # outstanding 250
pi2 = make_purchase_invoice() # outstanding 250
pe = get_payment_entry("Purchase Invoice", pi1.name, bank_account="_Test Cash - _TC")
pe.append(
"references",
{
"reference_doctype": "Purchase Invoice",
"reference_name": pi2.name,
"total_amount": pi2.grand_total,
"outstanding_amount": pi2.outstanding_amount,
"allocated_amount": pi2.outstanding_amount,
},
)
pe.paid_amount = pe.received_amount = (
pe.references[0].allocated_amount + pe.references[1].allocated_amount
)
pe.insert()
pe.submit()
self.assertEqual(pe.total_allocated_amount, 500)
self.assertEqual(frappe.db.get_value("Purchase Invoice", pi1.name, "outstanding_amount"), 0)
self.assertEqual(frappe.db.get_value("Purchase Invoice", pi2.name, "outstanding_amount"), 0)
def test_unallocated_amount_on_overpaid_purchase_payment(self):
pi = make_purchase_invoice() # outstanding 250
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.references[0].allocated_amount + 200 # overpay -> 200 advance
pe.received_amount = pe.paid_amount
pe.insert()
pe.submit()
self.assertEqual(pe.docstatus, 1)
self.assertEqual(pe.unallocated_amount, 200)
# end-to-end: submitting posts a balanced GL for the full paid amount (250
# settling the invoice + 200 advance)
gl_entries = frappe.get_all(
"GL Entry",
filters={"voucher_no": pe.name, "is_cancelled": 0},
fields=["debit", "credit"],
)
self.assertTrue(gl_entries, "Submitted payment produced no GL entries")
self.assertEqual(flt(sum(e.debit for e in gl_entries)), flt(sum(e.credit for e in gl_entries)))
self.assertEqual(flt(sum(e.debit for e in gl_entries)), 450)
def test_overallocation_against_purchase_invoice_throws(self):
pi = make_purchase_invoice() # outstanding 250
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Cash - _TC")
pe.references[0].allocated_amount += 100 # 350 > 250 outstanding
pe.paid_amount = pe.received_amount = pe.references[0].allocated_amount
self.assertRaises(frappe.ValidationError, pe.insert)
def test_payment_against_sales_invoice_to_check_status(self):
si = create_sales_invoice(
customer="_Test Customer USD",
@@ -2317,3 +2373,65 @@ def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer.save()
customer = customer.name
return customer
class TestPaymentEntryValidation(ERPNextTestSuite):
"""Field-level validations invoked on the document directly, covering branches the
integration suite above doesn't reach (no GL / reconciliation setup needed)."""
def make_pe(self, **fields):
doc = frappe.new_doc("Payment Entry")
doc.update(fields)
return doc
def test_payment_type_must_be_a_known_value(self):
self.assertRaises(frappe.ValidationError, self.make_pe(payment_type="Foo").validate_payment_type)
self.make_pe(payment_type="Receive").validate_payment_type() # valid value passes
def test_nonexistent_party_is_rejected(self):
doc = self.make_pe(party_type="Customer", party="__No Such Customer__")
self.assertRaises(frappe.ValidationError, doc.validate_party_details)
def test_amount_and_exchange_rate_fields_are_mandatory(self):
# every field but target_exchange_rate is set, so that missing one raises
doc = self.make_pe(
paid_amount=100, received_amount=100, source_exchange_rate=1, target_exchange_rate=0
)
self.assertRaises(frappe.ValidationError, doc.validate_mandatory)
def test_received_amount_cannot_exceed_paid_in_same_currency(self):
doc = self.make_pe(
paid_from_account_currency="INR",
paid_to_account_currency="INR",
paid_amount=100,
received_amount=150,
)
self.assertRaises(frappe.ValidationError, doc.validate_received_amount)
# received <= paid is fine
doc.received_amount = 50
doc.validate_received_amount()
def test_duplicate_reference_rows_are_rejected(self):
doc = self.make_pe()
for _ in range(2):
doc.append(
"references",
{"reference_doctype": "Sales Invoice", "reference_name": "SI-X", "allocated_amount": 100},
)
self.assertRaises(frappe.ValidationError, doc.validate_duplicate_entry)
def test_receive_from_customer_against_negative_outstanding_is_rejected(self):
doc = self.make_pe(party_type="Customer", payment_type="Receive")
doc.append(
"references",
{"reference_doctype": "Sales Invoice", "reference_name": "SI-Y", "allocated_amount": -100},
)
self.assertRaises(frappe.ValidationError, doc.validate_payment_type_with_outstanding)
def test_bank_transaction_requires_a_reference_number(self):
doc = self.make_pe(payment_type="Pay", paid_from="_Test Bank - _TC")
self.assertRaises(frappe.ValidationError, doc.validate_transaction_reference)
# supplying the reference details clears the requirement
doc.reference_no = "TXN-1"
doc.reference_date = "2026-06-15"
doc.validate_transaction_reference()

View File

@@ -75,7 +75,10 @@ class PaymentReconciliation(Document):
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
self.dimensions = get_dimensions(with_cost_center_and_project=True)[0]
self.user_permissions = get_user_permissions(frappe.session.user)
@property
def user_permissions(self):
return get_user_permissions(frappe.session.user)
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
@@ -833,10 +836,17 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
)
for inv in dr_cr_notes:
if (
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
flt(
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
- inv.allocated_amount,
allocated_amount_precision,
)
< 0
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)

View File

@@ -48,6 +48,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
sinv = create_sales_invoice(
qty=qty,
rate=rate,
posting_date=posting_date,
company=self.company,
customer=self.customer,
item_code=self.item,
@@ -2110,7 +2111,7 @@ class TestPaymentReconciliation(ERPNextTestSuite):
pr.reconcile()
si.reload()
self.assertEqual(si.status, "Partly Paid")
self.assertEqual(si.status, "Overdue")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120)
@@ -2506,6 +2507,76 @@ class TestPaymentReconciliation(ERPNextTestSuite):
self.assertEqual(flt(pr.allocation[0].difference_amount), 5000.0)
pr.reconcile()
def test_cr_note_split_across_invoices_floating_point_precision(self):
"""Regression: when a credit note is split across multiple invoices, floating-point
arithmetic (150 - 8.45 - 90.72 = 50.83000000000001) must not cause reconcile() to fail.
The test environment rounds INR totals to whole rupees (smallest_currency_fraction_value=0),
so the invoices are created with round-number totals (100, 200, 100) and then partially paid
down to the decimal outstanding amounts (8.45, 90.72, 72.57) via payment entries.
"""
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
# Create invoices on different posting dates to control sort-order in Payment Reconciliation
# (invoices are sorted by posting_date ascending, so si_a is processed first).
# Processing order 8.45 → 90.72 → 72.57 produces the float chain:
# 150 - 8.45 = 141.55 → 141.55 - 90.72 = 50.83000000000001
# The last allocation row will therefore carry allocated_amount = 50.83000000000001.
si_a = self.create_sales_invoice(qty=1, rate=100, posting_date=add_days(nowdate(), -2))
si_b = self.create_sales_invoice(qty=1, rate=200, posting_date=add_days(nowdate(), -1))
si_c = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate())
# Partially pay each invoice so the remaining outstanding is a clean decimal value.
# INR rounds the invoice total to a whole rupee, so we achieve decimal outstandings
# by subtracting a decimal-valued payment from the integer total:
# 100 - 91.55 = 8.45
# 200 - 109.28 = 90.72
# 100 - 27.43 = 72.57
for si, partial_paid in ((si_a, 91.55), (si_b, 109.28), (si_c, 27.43)):
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = partial_paid
pe.received_amount = partial_paid
pe.references[0].allocated_amount = partial_paid
pe.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=150, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
# Widen date range so all three invoices (oldest is -2 days) are fetched
pr.from_invoice_date = add_days(nowdate(), -2)
pr.to_invoice_date = nowdate()
pr.from_payment_date = nowdate()
pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 3)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Credit note (150) covers all of si_a (8.45) and si_b (90.72), then partially si_c
self.assertEqual(len(pr.allocation), 3)
last_row = pr.allocation[-1]
# Last allocated amount should be ~50.83 (possibly 50.83000000000001 due to float arithmetic)
self.assertAlmostEqual(flt(last_row.allocated_amount), 50.83, places=2)
# reconcile() must not raise "has been modified after you pulled it" due to float imprecision
pr.reconcile()
si_a.reload()
si_b.reload()
si_c.reload()
self.assertEqual(si_a.outstanding_amount, 0)
self.assertEqual(si_b.outstanding_amount, 0)
# si_c is only partially settled: 72.57 - 50.83 = 21.74
self.assertAlmostEqual(si_c.outstanding_amount, 21.74, places=2)
def create_fiscal_year(company, year_start_date, year_end_date):
fy_docname = frappe.db.exists(

View File

@@ -14,7 +14,8 @@
"section_break_mjlv",
"due_date",
"column_break_qghl",
"amount"
"amount",
"currency"
],
"fields": [
{
@@ -55,8 +56,18 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"precision": "2"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_lnjp",
"fieldtype": "Column Break"
@@ -74,7 +85,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-01-19 02:21:36.455830",
"modified": "2026-07-11 00:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reference",

View File

@@ -37,6 +37,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
frm.set_intro(__("Failure: {0}", [frm.doc.failed_reason]), "red");
}
let sending_email = false;
if (
frm.doc.payment_request_type == "Inward" &&
frm.doc.payment_channel !== "Phone" &&
@@ -45,16 +47,16 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
frm.doc.docstatus == 1
) {
frm.add_custom_button(__("Resend Payment Email"), function () {
frappe.call({
method: "erpnext.accounts.doctype.payment_request.payment_request.resend_payment_email",
args: { docname: frm.doc.name },
freeze: true,
freeze_message: __("Sending"),
callback: function (r) {
if (!r.exc) {
frappe.msgprint(__("Message Sent"));
}
},
if (sending_email) {
frappe.show_alert({ message: __("Sending Email"), indicator: "blue" });
return;
}
sending_email = true;
frappe.show_alert({ message: __("Sending Email"), indicator: "blue" });
frm.call("resend_payment_email").then((r) => {
const msg = !r.exc ? __("Email Sent") : __("Email couldn't be sent.");
frappe.show_alert({ message: msg, indicator: !r.exc ? "green" : "red" });
sending_email = false;
});
});
}

View File

@@ -459,6 +459,11 @@ class PaymentRequest(Document):
else:
return True
except Exception:
frappe.log_error(
title=f"Payment Gateway validation failed: {self.payment_gateway}",
reference_doctype=self.doctype,
reference_name=self.name,
)
return False
def set_payment_request_url(self):
@@ -542,6 +547,7 @@ class PaymentRequest(Document):
bank_amount=bank_amount,
created_from_payment_request=True,
)
payment_entry.set_missing_ref_details(force=True)
payment_entry.update(
{
@@ -583,6 +589,18 @@ class PaymentRequest(Document):
return payment_entry
@frappe.whitelist(methods=["POST"])
def resend_payment_email(self):
if not (
self.docstatus == 1
and self.payment_request_type == "Inward"
and self.payment_channel != "Phone"
and self.status not in ["Initiated", "Paid"]
):
frappe.throw(_("Payment Link couldn't be sent."))
self.send_email()
def send_email(self):
"""send email with payment link"""
email_args = {
@@ -600,11 +618,14 @@ class PaymentRequest(Document):
)
],
}
job_id = f"send_payment_email::{self.name}"
enqueue(
method=frappe.sendmail,
queue="short",
timeout=300,
is_async=True,
job_id=job_id,
deduplicate=True,
enqueue_after_commit=True,
**email_args,
)
@@ -718,7 +739,7 @@ class PaymentRequest(Document):
row_number += TO_SKIP_NEW_ROW
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def make_payment_request(**args):
"""Make payment request"""
@@ -942,6 +963,7 @@ def set_payment_references(payment_schedules):
"description": row.get("description"),
"due_date": row.get("due_date"),
"amount": row.get("payment_amount"),
"currency": row.get("currency"),
}
)
@@ -1108,11 +1130,6 @@ def get_print_format_list(ref_doctype: str):
return {"print_format": print_format_list}
@frappe.whitelist()
def resend_payment_email(docname: str):
return frappe.get_doc("Payment Request", docname).send_email()
@frappe.whitelist()
def make_payment_entry(docname: str):
doc = frappe.get_doc("Payment Request", docname)
@@ -1225,7 +1242,7 @@ def get_subscription_details(reference_doctype: str, reference_name: str):
@frappe.whitelist()
def make_payment_order(source_name: str, target_doc: str | Document | None = None):
def make_payment_order(source_name: str, target_doc: str | dict | Document | None = None):
from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target):

View File

@@ -774,6 +774,22 @@ class TestPaymentRequest(ERPNextTestSuite):
pi.load_from_db()
self.assertEqual(pr_2.grand_total, pi.outstanding_amount)
def test_payment_entry_reference_details_fetched_from_invoice(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=94500)
pi.submit()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1, submit_doc=0, return_doc=1)
pr.grand_total = 94000
pr.submit()
pe = pr.create_payment_entry(submit=False)
self.assertEqual(pe.references[0].reference_name, pi.name)
self.assertEqual(pe.references[0].total_amount, pi.grand_total)
self.assertEqual(pe.references[0].outstanding_amount, pi.outstanding_amount)
self.assertEqual(pe.references[0].allocated_amount, 94000)
self.assertEqual(pe.paid_amount, 94000)
def test_consider_journal_entry_and_return_invoice(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry

View File

@@ -41,21 +41,17 @@ frappe.ui.form.on("Period Closing Voucher", {
refresh: function (frm) {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
"fa fa-table"
);
frm.add_custom_button(__("Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
});
}
},
});

View File

@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.account_closing_balance.account_closing_balance im
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import check_freezing_date, is_immutable_ledger_enabled
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
@@ -45,6 +46,14 @@ class PeriodClosingVoucher(AccountsController):
self.block_if_future_closing_voucher_exists()
self.check_closing_account_type()
self.check_closing_account_currency()
self.validate_accounts_not_frozen()
def validate_accounts_not_frozen(self, for_cancellation=False):
posting_date = self.period_end_date
if for_cancellation and is_immutable_ledger_enabled():
posting_date = getdate()
check_freezing_date(posting_date, self.company)
def validate_start_and_end_date(self):
self.fy_start_date, self.fy_end_date = frappe.db.get_value(
@@ -149,6 +158,7 @@ class PeriodClosingVoucher(AccountsController):
"Process Period Closing Voucher",
)
self.block_if_future_closing_voucher_exists()
self.validate_accounts_not_frozen(for_cancellation=True)
if not frappe.get_single_value("Accounts Settings", "use_legacy_controller_for_pcv"):
self.cancel_process_pcv_docs()

View File

@@ -315,6 +315,77 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
repost_doc.posting_date = today()
repost_doc.save()
def test_dimension_grouped_opening_balance_matches_gl_scan(self):
"""
A dimension-grouped Balance Sheet must produce identical per-dimension
figures whether opening balances come from
- Account Closing Balance (the fast path) or
- from a full GL scan (the fallback).
"""
from frappe.utils import add_days, getdate
from erpnext.accounts.report.balance_sheet.balance_sheet import execute
from erpnext.accounts.report.financial_statements import build_period_list
company = "Test PCV Company"
cc1 = create_cost_center("Test Cost Center 1")
cc2 = create_cost_center("Test Cost Center 2")
# Post to two cost centers, then close the year so balances land in Account Closing Balance.
for amount, cost_center in ((400, cc1), (200, cc2)):
jv = make_journal_entry(
posting_date="2021-03-15",
amount=amount,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
save=False,
)
jv.company = company
jv.save()
jv.submit()
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
report_date = add_days(getdate(pcv.period_end_date), 1)
report_filters = frappe._dict(
company=company,
period_start_date=report_date,
period_end_date=report_date,
periodicity="Yearly",
filter_based_on="Date Range",
accumulated_values=True,
group_by_dimension="Cost Center",
)
period_list = build_period_list(report_filters)
period_keys = [p.key for p in period_list]
def key_for(cost_center):
return next(p.key for p in period_list if p.dimension_value == cost_center)
def figures(data):
return {
row["account_name"]: {k: row.get(k) for k in period_keys}
for row in data
if row.get("account_name")
}
# Fast path: opening balance sourced from Account Closing Balance.
acb_figures = figures(execute(report_filters)[1])
# Fallback: force a full GL scan and expect the same numbers.
with self.change_settings("Accounts Settings", {"ignore_account_closing_balance": 1}):
gl_figures = figures(execute(report_filters)[1])
self.assertEqual(acb_figures, gl_figures)
# the fast path must carry per-dimension opening balances, not aggregates or zeros
self.assertEqual(acb_figures["Cash"][key_for(cc1)], 400)
self.assertEqual(acb_figures["Cash"][key_for(cc2)], 200)
def make_period_closing_voucher(self, posting_date, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
@@ -360,12 +431,15 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.make_period_closing_voucher(posting_date="2021-03-31")
# Passed posting_date is after PCV end date, so cancellation should not fail.
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
posting_date="2022-01-01",
)
frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", "2021-12-31")
try:
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
)
finally:
frappe.db.set_value("Company", "Test PCV Company", "accounts_frozen_till_date", None)
totals_after_cancel = frappe.get_all(
"GL Entry",

View File

@@ -219,7 +219,8 @@ class POSClosingEntry(StatusUpdater):
self.update_sales_invoices_closing_entry()
def before_cancel(self):
self.check_pce_is_cancellable()
if self.status != "Failed":
self.check_pce_is_cancellable()
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)

View File

@@ -663,7 +663,6 @@ class POSInvoice(SalesInvoice):
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
from erpnext.stock.get_item_details import (
ItemDetailsCtx,
get_pos_profile,
get_pos_profile_item_details_,
)
@@ -736,7 +735,7 @@ class POSInvoice(SalesInvoice):
for item in self.get("items"):
if item.get("item_code"):
profile_details = get_pos_profile_item_details_(
ItemDetailsCtx(item.as_dict()), profile.get("company"), profile
frappe._dict(item.as_dict()), profile.get("company"), profile
)
for fname, val in profile_details.items():
if (not for_validate) or (for_validate and not item.get(fname)):
@@ -1026,7 +1025,7 @@ def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
@frappe.whitelist()
def make_sales_return(source_name: str, target_doc: Document | str | None = None):
def make_sales_return(source_name: str, target_doc: str | dict | Document | None = None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return make_return_doc("POS Invoice", source_name, target_doc)

View File

@@ -0,0 +1,34 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# Regression test for https://github.com/frappe/erpnext/issues/56501
# AttributeError: 'POSInvoice' object has no attribute 'is_created_using_pos'
# when calling reset_mode_of_payments on a draft POS Invoice.
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import (
POSInvoiceTestMixin,
create_pos_invoice,
)
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
class TestPOSInvoiceResetModeOfPayments(POSInvoiceTestMixin):
def setUp(self):
super().setUp()
create_opening_entry(self.pos_profile, self.test_user.name)
def test_reset_mode_of_payments_does_not_raise_attribute_error(self):
"""Calling reset_mode_of_payments on a draft POS Invoice must not raise
AttributeError for the missing is_created_using_pos attribute.
update_multi_mode_option accesses doc.is_created_using_pos, which is a
field on SalesInvoice but does not exist on POSInvoice, causing the error
reported in #56501 when a user tries to edit a saved draft order.
"""
inv = create_pos_invoice(do_not_submit=True)
# This call must not raise AttributeError on the missing field.
inv.reset_mode_of_payments()
# Payments should have been repopulated from the POS profile.
self.assertTrue(len(inv.payments) > 0, "Payments should be populated after reset")

View File

@@ -89,6 +89,8 @@
"item_tax_rate",
"actual_batch_qty",
"actual_qty",
"serial_batch_entries_section",
"serial_batch_entries_html",
"section_break_tlhi",
"serial_no",
"column_break_ciit",
@@ -859,6 +861,15 @@
"fieldtype": "Check",
"label": "Use Serial No / Batch Fields"
},
{
"fieldname": "serial_batch_entries_section",
"fieldtype": "Section Break",
"label": "Serial / Batch Entries"
},
{
"fieldname": "serial_batch_entries_html",
"fieldtype": "HTML"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 1",
"fieldname": "section_break_tlhi",
@@ -877,7 +888,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-06-08 20:00:00.000000",
"modified": "2026-07-18 10:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -40,7 +40,7 @@ frappe.ui.form.on("Pricing Rule", {
var help_content = `<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>
<svg class="icon icon-sm"><use href="#icon-info"></use></svg>
${__("Notes")}
</h4>
<ul>
@@ -63,7 +63,7 @@ frappe.ui.form.on("Pricing Rule", {
</ul>
</td></tr>
<tr><td>
<h4><i class="fa fa-question-sign"></i>
<h4><svg class="icon icon-sm"><use href="#icon-circle-question-mark"></use></svg>
${__("How Pricing Rule is applied?")}
</h4>
<ol>

View File

@@ -156,6 +156,24 @@ class PricingRule(Document):
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
if self.apply_on == "Item Code":
self.validate_template_with_variant(values)
def validate_template_with_variant(self, item_codes):
# throws if a template and its variant both exist in one rule
variants = frappe.get_all(
"Item",
filters={"name": ("in", item_codes), "variant_of": ("in", item_codes)},
fields=["name", "variant_of"],
)
if variants:
variant = variants[0]
frappe.throw(
_("Variant {0} and its template {1} cannot both be added to the same Pricing Rule").format(
frappe.bold(variant.name), frappe.bold(variant.variant_of)
)
)
def validate_mandatory(self):
if self.has_priority and not self.priority:
throw(_("Priority is mandatory"), frappe.MandatoryError, _("Please Set Priority"))

View File

@@ -333,6 +333,31 @@ class TestPricingRule(ERPNextTestSuite):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 17.5)
def test_pricing_rule_with_template_and_its_variant(self):
if not frappe.db.exists("Item", "Test Variant PRT"):
variant = frappe.new_doc("Item")
variant.item_code = "Test Variant PRT"
variant.item_name = "Test Variant PRT"
variant.item_group = "_Test Item Group"
variant.is_stock_item = 1
variant.variant_of = "_Test Variant Item"
variant.stock_uom = "_Test UOM"
variant.append("attributes", {"attribute": "Test Size", "attribute_value": "Medium"})
variant.insert()
rule = frappe.new_doc("Pricing Rule")
rule.title = "_Test Pricing Rule Template Variant"
rule.apply_on = "Item Code"
rule.currency = "USD"
rule.selling = 1
rule.rate_or_discount = "Discount Percentage"
rule.discount_percentage = 10
rule.company = "_Test Company"
rule.append("items", {"item_code": "_Test Variant Item"})
rule.append("items", {"item_code": "Test Variant PRT"})
self.assertRaises(frappe.ValidationError, rule.insert)
def test_pricing_rule_for_stock_qty(self):
test_record = {
"doctype": "Pricing Rule",

View File

@@ -89,7 +89,11 @@ def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
if frappe.safe_eval(pricing_rule.condition, None, doc.as_dict()):
filtered_pricing_rules.append(pricing_rule)
except Exception:
pass
frappe.log_error(
title=f"Pricing Rule condition failed to evaluate: {pricing_rule.name}",
reference_doctype="Pricing Rule",
reference_name=pricing_rule.name,
)
else:
filtered_pricing_rules.append(pricing_rule)
else:

View File

@@ -106,6 +106,8 @@ def get_pr_instance(doc: str):
"party",
"receivable_payable_account",
"default_advance_account",
"bank_cash_account",
"cost_center",
"from_invoice_date",
"to_invoice_date",
"from_payment_date",

View File

@@ -1,11 +1,73 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
get_pr_instance,
)
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
class TestProcessPaymentReconciliation(ERPNextTestSuite):
pass
"""Process Payment Reconciliation validates its accounts against the company,
moves to Queued on submit, and hands its filters to a Payment Reconciliation run."""
def setUp(self):
frappe.set_user("Administrator")
def make_ppr(self, **args):
args = frappe._dict(args)
doc = frappe.new_doc("Process Payment Reconciliation")
doc.company = COMPANY
doc.party_type = "Customer"
doc.party = "_Test Customer"
doc.receivable_payable_account = args.get("receivable_payable_account", "Debtors - _TC")
doc.bank_cash_account = args.get("bank_cash_account")
doc.from_invoice_date = args.get("from_invoice_date")
doc.to_invoice_date = args.get("to_invoice_date")
return doc
def other_company_account(self, **extra):
filters = {"company": "_Test Company 1", "is_group": 0, **extra}
account = frappe.db.get_value("Account", filters, "name")
self.assertTrue(account, "need a matching account in _Test Company 1")
return account
def test_receivable_account_must_belong_to_company(self):
doc = self.make_ppr(receivable_payable_account=self.other_company_account(account_type="Receivable"))
self.assertRaises(frappe.ValidationError, doc.insert)
def test_bank_cash_account_must_belong_to_company(self):
doc = self.make_ppr(bank_cash_account=self.other_company_account())
self.assertRaises(frappe.ValidationError, doc.insert)
def test_submit_sets_status_to_queued(self):
doc = self.make_ppr()
doc.insert()
doc.submit()
self.assertEqual(doc.status, "Queued")
def test_get_pr_instance_copies_filters_and_caps_limits(self):
doc = self.make_ppr(from_invoice_date="2026-01-01", to_invoice_date="2026-06-30")
doc.insert()
pr = get_pr_instance(doc.name)
self.assertEqual(pr.company, COMPANY)
self.assertEqual(pr.party, "_Test Customer")
self.assertEqual(pr.receivable_payable_account, "Debtors - _TC")
self.assertEqual(str(pr.from_invoice_date), "2026-01-01")
# the tool run is capped so a single process can't fetch unbounded rows
self.assertEqual(pr.invoice_limit, 1000)
self.assertEqual(pr.payment_limit, 1000)
def test_get_pr_instance_copies_bank_cash_and_cost_center(self):
doc = self.make_ppr(bank_cash_account="Cash - _TC")
doc.cost_center = "_Test Cost Center - _TC"
doc.insert()
pr = get_pr_instance(doc.name)
self.assertEqual(pr.bank_cash_account, "Cash - _TC")
self.assertEqual(pr.cost_center, "_Test Cost Center - _TC")

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