Compare commits

..

25 Commits

Author SHA1 Message Date
Frappe PR Bot
866688931b chore(release): Bumped to Version 15.119.0
# [15.119.0](https://github.com/frappe/erpnext/compare/v15.118.3...v15.119.0) (2026-08-04)

### Bug Fixes

* **accounts:** fetch deferred invoice docs on non-empty `sales_docs` or `purchase_docs` in repost accounting ledger (backport [#57753](https://github.com/frappe/erpnext/issues/57753)) ([#57756](https://github.com/frappe/erpnext/issues/57756)) ([ee4e296](ee4e296ce6))
* **accounts:** skip party dashboard without invoice permission ([9d417da](9d417da3d8))
* **accounts:** update AU standard chart of accounts (backport [#57145](https://github.com/frappe/erpnext/issues/57145)) ([#57607](https://github.com/frappe/erpnext/issues/57607)) ([15c3817](15c381701d))
* **assets:** add permission checks on whitelisted methods on `asset_capitalization` ([338fff2](338fff20db))
* do not fetch a random inventory account when multiple inventory accounts exist (backport [#57626](https://github.com/frappe/erpnext/issues/57626)) ([#57631](https://github.com/frappe/erpnext/issues/57631)) ([972a990](972a990b01))
* escape data in multiple templates (backport [#57742](https://github.com/frappe/erpnext/issues/57742)) ([#57769](https://github.com/frappe/erpnext/issues/57769)) ([928f984](928f984198))
* filter Accounts Receivable by invoice sales partner (backport [#57628](https://github.com/frappe/erpnext/issues/57628)) ([#57646](https://github.com/frappe/erpnext/issues/57646)) ([ebf5a46](ebf5a462b3))
* guard against None row in get_stock_balance_for (backport [#57567](https://github.com/frappe/erpnext/issues/57567)) ([72f293f](72f293f131))
* Ignore permission while deleting user permission ([faa7c46](faa7c466b1))
* incorrect batch-wise valuation rate for entries with same posting datetime ([#57794](https://github.com/frappe/erpnext/issues/57794)) ([431dc2e](431dc2e5f1))
* **item_variant:** added permission checks on `enqueue_multiple_variant_creation` ([9cd5997](9cd5997500))
* let Purchase Receipt cancel defer to Frappe's linked-document check (backport [#57592](https://github.com/frappe/erpnext/issues/57592)) ([#57602](https://github.com/frappe/erpnext/issues/57602)) ([7cecff9](7cecff9fa4))
* **opportunity:** add validation for positive item quantities ([f47346a](f47346aa90))
* **payment reconciliation:** correct supplier gain/loss posting ([78cd25d](78cd25de04))
* **payment_request:** added permission checks on `resend_payment_email` ([c38c9d5](c38c9d5d9b))
* **plant_floor:** add missing perm check on `get_stock_summary` (backport [#57667](https://github.com/frappe/erpnext/issues/57667)) ([#57669](https://github.com/frappe/erpnext/issues/57669)) ([42d5378](42d53783bb))
* preserve UOM conversion factor precision in transactions ([4373e29](4373e295de))
* prevent duplicate shipping charges without cost center ([a6dff3f](a6dff3fc47))
* **projects:** add project filter ([54390bd](54390bdeb9))
* **projects:** include on hold status in project filters and reports ([478426b](478426b436))
* **purchase:** reject purchase returns where every item has zero quantity ([8676add](8676add875))
* **quotation:** carry forward communications from opportunity at after_insert (backport [#57639](https://github.com/frappe/erpnext/issues/57639)) ([#57642](https://github.com/frappe/erpnext/issues/57642)) ([cf42c52](cf42c52530))
* resolve backport conflicts for version-15 ([1602639](1602639a80))
* resolve version 15 backport conflict ([00df865](00df8652e3))
* resolve version-15 backport conflict ([70da05e](70da05edb7))
* respect quantity precision in material transfer validation ([a5ed3a5](a5ed3a5945))
* **sales:** reject sales returns where every item has zero quantity ([48beb2e](48beb2ee23))
* seed standard Item Groups under the existing tree root ([8483350](848335086c)), closes [#57581](https://github.com/frappe/erpnext/issues/57581)
* set transaction currency on payment entry gl entries ([#57613](https://github.com/frappe/erpnext/issues/57613)) ([35f523e](35f523e2dd))
* source manually created asset value from valuation rate ([455d6d4](455d6d4ac1))
* **stock:** pick list serial batch posting date ([#57015](https://github.com/frappe/erpnext/issues/57015)) ([a51750d](a51750db56)), closes [#56951](https://github.com/frappe/erpnext/issues/56951)
* **stock:** validate only the variant attributes that changed ([2993747](2993747636))
* **stock:** value batched packed-item returns from the original bundle  (backport [#57327](https://github.com/frappe/erpnext/issues/57327)) ([#57510](https://github.com/frappe/erpnext/issues/57510)) ([94d63eb](94d63ebb49))
* use payment entry posting date for received amount exchange rate (backport [#57660](https://github.com/frappe/erpnext/issues/57660)) ([#57662](https://github.com/frappe/erpnext/issues/57662)) ([b826b7c](b826b7c3e6))
* validate account frozen date ([310b9d4](310b9d4e65))

### Features

* auto-fill subscription accounting dimensions from plan with item fallback (backport [#57615](https://github.com/frappe/erpnext/issues/57615)) ([#57621](https://github.com/frappe/erpnext/issues/57621)) ([bb36a4f](bb36a4fd08))
* make Shipping Rule Cost Center optional with company default fallback (backport [#57355](https://github.com/frappe/erpnext/issues/57355)) ([#57402](https://github.com/frappe/erpnext/issues/57402)) ([b2918b8](b2918b8bb3))
* status based bar colors in Work Order gantt view (backport [#57634](https://github.com/frappe/erpnext/issues/57634)) ([#57635](https://github.com/frappe/erpnext/issues/57635)) ([68c24f3](68c24f3767))
2026-08-04 20:31:08 +00:00
Diptanil Saha
f0d1887e6e Merge pull request #57782 from frappe/version-15-hotfix
chore: release v15
2026-08-05 01:59:03 +05:30
Frappe PR Bot
7098602dcc chore(release): Bumped to Version 15.118.3
## [15.118.3](https://github.com/frappe/erpnext/compare/v15.118.2...v15.118.3) (2026-07-30)

### Bug Fixes

* **italy:** skip e-invoicing for opening invoices ([4f07e25](4f07e2503b))
* **stock:** keep manufactured item rate at zero when inputs are free  (backport [#57334](https://github.com/frappe/erpnext/issues/57334)) ([#57512](https://github.com/frappe/erpnext/issues/57512)) ([ade3f79](ade3f791a4))
2026-07-30 05:19:18 +00:00
Diptanil Saha
32b56ac505 Merge pull request #57611 from frappe/version-15-hotfix
chore: release v15
2026-07-30 10:47:30 +05:30
mergify[bot]
9f1bdba9a7 refactor(accounts): repost accounting ledger (backport #56442) (#57598)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-07-30 10:15:40 +05:30
Frappe PR Bot
c630226846 chore(release): Bumped to Version 15.118.2
## [15.118.2](https://github.com/frappe/erpnext/compare/v15.118.1...v15.118.2) (2026-07-28)

### Bug Fixes

* add is_active filter ([bb1320f](bb1320f8df))
* add permission check for `get_item_details` (backport [#57515](https://github.com/frappe/erpnext/issues/57515)) ([#57550](https://github.com/frappe/erpnext/issues/57550)) ([41effcf](41effcf754))
* check if item is variant when creating WO from MR ([745513d](745513d0c2))
* **crm:** align Opportunity status checks with Quotation statuses (backport [#57489](https://github.com/frappe/erpnext/issues/57489)) ([#57490](https://github.com/frappe/erpnext/issues/57490)) ([53d9d1c](53d9d1c50d))
* **crm:** clarify the reason why an opportunity cannot be declared as lost (backport [#57495](https://github.com/frappe/erpnext/issues/57495)) ([#57497](https://github.com/frappe/erpnext/issues/57497)) ([98a0fd8](98a0fd814e))
* detect the currency column by fieldtype in trends total row ([8cceb6a](8cceb6af10))
* enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report ([#57458](https://github.com/frappe/erpnext/issues/57458)) ([4e8f5de](4e8f5de5cb))
* guard against missing is_your_company_address custom field on address ([6fa522d](6fa522d031))
* Incorrect creation time at the time cancelling an entry causing an issue especially same posting datetime  (backport [#57380](https://github.com/frappe/erpnext/issues/57380)) ([#57396](https://github.com/frappe/erpnext/issues/57396)) ([ae0cd16](ae0cd164f3))
* **manufacturing:** fall back to UOM Conversion Factor in Production Plan (backport [#57553](https://github.com/frappe/erpnext/issues/57553)) ([#57555](https://github.com/frappe/erpnext/issues/57555)) ([12b4c13](12b4c134ca))
* **manufacturing:** update cost of BOMs created via BOM Creator ([25e5b10](25e5b107be))
* map pick list customer to delivery note when no sales order ([0dc5894](0dc5894499)), closes [#57412](https://github.com/frappe/erpnext/issues/57412)
* migrate stored AR/AP ageing filter to renamed field ([e2319c3](e2319c3ffe))
* pool batch slot values on every run, not only when negative ([f38b3b4](f38b3b422d))
* **ppcv:** replace incorrect usage of `frappe.in_test` with `frappe.flags.in_test` in version-15 ([#57579](https://github.com/frappe/erpnext/issues/57579)) ([abc53b0](abc53b0d39))
* **quotation:** carry forward communications from opportunity (backport [#57507](https://github.com/frappe/erpnext/issues/57507)) ([#57508](https://github.com/frappe/erpnext/issues/57508)) ([ceb6778](ceb677844f))
* rebalance batch slot values at the pooled rate when driven negative ([4808202](48082020e8))
* recalculate operating cost on hour rate change in routing ([4fe91bd](4fe91bd8b8))
* rename misleading filter labels in AR/AP reports ([60b4e60](60b4e6053d))
* resolve backport conflict in stock ageing test imports ([a2ace9d](a2ace9d394))
* respect child warehouse account override in Stock and Account Value Comparison ([#57552](https://github.com/frappe/erpnext/issues/57552)) ([20b6dd3](20b6dd3d0f))
* respect selected BOM when creating work order for variant item ([#57359](https://github.com/frappe/erpnext/issues/57359)) ([c5235af](c5235af6bf))
* respect user permissions in party dashboard company list ([348555b](348555b127)), closes [frappe/erpnext#57428](https://github.com/frappe/erpnext/issues/57428)
* restore Save button on reverse journal entry ([d937385](d9373850ad))
* seed cancelled voucher replay from before its posting datetime ([1ff297e](1ff297e9da))
* **stock:** narrow legacy serial ledger lookup by item (backport [#57499](https://github.com/frappe/erpnext/issues/57499)) ([#57505](https://github.com/frappe/erpnext/issues/57505)) ([c70cf8e](c70cf8e554))
* stop storing "{supplier_name}" / "{customer_name}" as the document title ([d96999d](d96999de7f))
* **subcontracting:** release raw-material reservation when closing a subcontracting order ([d970627](d9706271ff))
* **tnc:** `get_terms_and_conditions` render_template with `safe_exec` (backport [#56944](https://github.com/frappe/erpnext/issues/56944)) (backport [#56977](https://github.com/frappe/erpnext/issues/56977)) ([#57106](https://github.com/frappe/erpnext/issues/57106)) ([1d60ab4](1d60ab449c))
* typeerror in get_batches_by_oldest for mixed batch expiry ([56bd024](56bd024f39))
* update operating cost when propagating workstation hour rate to routing ([#57504](https://github.com/frappe/erpnext/issues/57504)) ([8f68b7e](8f68b7ed20))
* use company currency instead of global default in report (backport [#56561](https://github.com/frappe/erpnext/issues/56561)) ([0ad0d77](0ad0d7733b))
2026-07-28 23:31:43 +00:00
Diptanil Saha
d5b49cd66e Merge pull request #57549 from frappe/version-15-hotfix
chore: release v15
2026-07-29 04:59:58 +05:30
Frappe PR Bot
b9c9b76f5b chore(release): Bumped to Version 15.118.1
## [15.118.1](https://github.com/frappe/erpnext/compare/v15.118.0...v15.118.1) (2026-07-23)

### Bug Fixes

* seed cancelled voucher replay from before its posting datetime ([3d41984](3d4198494b))
2026-07-23 08:44:24 +00:00
Mihir Kandoi
da0e3b5882 Merge pull request #57395 from frappe/mergify/bp/version-15/pr-57347
fix: seed cancelled voucher replay from before its posting datetime (v15) (backport #57347)
2026-07-23 14:12:42 +05:30
Mihir Kandoi
3d4198494b fix: seed cancelled voucher replay from before its posting datetime
On cancel, update_entries_after replays every live SLE at the voucher's
posting datetime, but get_previous_sle_of_current_voucher seeded the
replay with the reversal SLE's creation, which resolves to the bucket's
own closing row. The bucket's net qty got double-counted into every
same-datetime row, so later submissions passed negative stock validation
against inflated balances, and the queued repost then rewrote correct
values with allow_negative_stock forced on, silently creating negative
stock. Backports the missing guard from eca71dce54.

(cherry picked from commit 1ff297e9da)
2026-07-23 08:22:04 +00:00
Frappe PR Bot
e6b4799b1a chore(release): Bumped to Version 15.118.0
# [15.118.0](https://github.com/frappe/erpnext/compare/v15.117.0...v15.118.0) (2026-07-22)

### Bug Fixes

* add fetch from in production plan material request child table ([6ffd759](6ffd759968))
* added missing validations for `Dunning Type` (backport [#57224](https://github.com/frappe/erpnext/issues/57224)) ([#57226](https://github.com/frappe/erpnext/issues/57226)) ([fd01171](fd01171df5))
* apply default accounting dimensions reliably on new documents ([88abe11](88abe119c3))
* apply user permissions via build_match_conditions ([202f522](202f52271c))
* block changing Stock account type when stock ledger entries exist (backport [#57283](https://github.com/frappe/erpnext/issues/57283)) ([#57284](https://github.com/frappe/erpnext/issues/57284)) ([2cd531d](2cd531d099))
* consider min order qty in the purchase/transfer flow of production plan (backport [#57204](https://github.com/frappe/erpnext/issues/57204)) ([#57209](https://github.com/frappe/erpnext/issues/57209)) ([77ec644](77ec6447c3))
* correct typo in allow_negative_stock parameter ([f24e09c](f24e09cc97))
* dont overwrite rate with 0 if not found ([74e9718](74e9718871))
* pass ctx keys get_price_list_rate_for reads, skip rate update on insert ([302cbbe](302cbbe5d8))
* **payments:** ensure `payments` app installed on the site in `payment_app_import_guard` (backport [#57342](https://github.com/frappe/erpnext/issues/57342)) ([#57343](https://github.com/frappe/erpnext/issues/57343)) ([3efddfd](3efddfd270))
* permission issue (backport [#57112](https://github.com/frappe/erpnext/issues/57112)) ([#57142](https://github.com/frappe/erpnext/issues/57142)) ([6b23b00](6b23b007a4))
* project % complete field allowing modification when manual method (backport [#57274](https://github.com/frappe/erpnext/issues/57274)) ([#57275](https://github.com/frappe/erpnext/issues/57275)) ([21e5620](21e5620e92))
* **project:** improved access control for project users (backport [#56675](https://github.com/frappe/erpnext/issues/56675)) ([#57180](https://github.com/frappe/erpnext/issues/57180)) ([ff6c8bb](ff6c8bbb44))
* read serial and batch flags from Item in Stock Balance's SLE query ([e7f0461](e7f0461b57))
* rescale batch FIFO slot values on stock reconciliation ([72b3210](72b3210cbb))
* rescale stock ageing FIFO slot values on stock reconciliation ([d51f907](d51f9076b5))
* resolve float precision before streaming stock ledger entries ([b9ff5be](b9ff5be43e))
* resolve merge conflicts ([81e865f](81e865f6c7))
* restrict jinja globals in process statement of accounts templates (backport [#56458](https://github.com/frappe/erpnext/issues/56458)) ([#57231](https://github.com/frappe/erpnext/issues/57231)) ([88443e4](88443e4a97))
* revalue batch reco slots only when the entry covers the full batch ([2673029](2673029bd4))
* set correct currency in supplier quotation net rate field ([30ab2db](30ab2dba6e))
* strip account number when building account name in COA importer ([314dd16](314dd16aa3))
* sync process loss percentage when fg qty changes ([f0e24e2](f0e24e2f53))
* use account currency in Bank Reconciliation Statement print ([d6f797d](d6f797d077))
* use system float precision for batch qty comparison ([1679bde](1679bdecdc))
* validate buying price list on material request and update item rates on change ([96dc408](96dc408484))

### Features

* add on hold status to project ([5e2e154](5e2e15436d))
* **stock:** automatically link portal users to their associated contact profiles for customers and suppliers ([4f9ea98](4f9ea989c4))
* **stock:** expose all Bin qty fields in Stock Summary and Stock Projected Qty ([90009a4](90009a4687))
2026-07-22 00:17:40 +00:00
Diptanil Saha
49eb2366cd Merge pull request #57323 from frappe/version-15-hotfix
chore: release v15
2026-07-22 05:45:46 +05:30
Frappe PR Bot
fb2a4e5f98 chore(release): Bumped to Version 15.117.0
# [15.117.0](https://github.com/frappe/erpnext/compare/v15.116.0...v15.117.0) (2026-07-15)

### Bug Fixes

* allow barcode scan to add and increment items in pick list ([bb5be10](bb5be10372))
* **asset:** allow asset repair creation for fully depreciated assets (backport [#55276](https://github.com/frappe/erpnext/issues/55276)) ([e4d2a96](e4d2a960d2))
* **journal-entry:** fetch outstanding on foreign currency ([ace51fc](ace51fccfa))
* **manufacturing:** preserve job card transferred quantity ([fc4ac7a](fc4ac7a441))
* **stock:** fix sqlparse token limit in get_bundle_wise_serial_nos ([cc45743](cc457433fe))
* **stock:** show qty (company) and qty (warehouse) in sales transactions ([8086542](8086542885))

### Features

* faster (synced) financial statements using duckdb (backport [#56304](https://github.com/frappe/erpnext/issues/56304)) ([#57093](https://github.com/frappe/erpnext/issues/57093)) ([a6a887f](a6a887f833))
2026-07-15 02:28:29 +00:00
ruthra kumar
0ce7917648 Merge pull request #57160 from frappe/version-15-hotfix
chore: release v15
2026-07-15 06:25:15 +05:30
Frappe PR Bot
41038979ec chore(release): Bumped to Version 15.116.0
# [15.116.0](https://github.com/frappe/erpnext/compare/v15.115.0...v15.116.0) (2026-07-13)

### Bug Fixes

* **`frappe_crm_api`:** handle failure for `after_app_install` and `after_app_uninstall` ([983ec56](983ec5660c))
* **accounts:** clear clearance date when amending reconciled voucher (backport [#55947](https://github.com/frappe/erpnext/issues/55947)) ([5f14f2c](5f14f2ccd6)), closes [#55972](https://github.com/frappe/erpnext/issues/55972)
* **accounts:** retain invoice table on opening invoice creation error (backport [#56353](https://github.com/frappe/erpnext/issues/56353)) ([#57057](https://github.com/frappe/erpnext/issues/57057)) ([cfc546c](cfc546cf3f))
* added permission checks on various whitelisted functions (backport [#56745](https://github.com/frappe/erpnext/issues/56745)) ([#56945](https://github.com/frappe/erpnext/issues/56945)) ([55e0d10](55e0d106c9))
* address review comments ([3c47adb](3c47adb021))
* block serialized to non-serialized item change when SABB exists (backport [#56773](https://github.com/frappe/erpnext/issues/56773)) ([#56774](https://github.com/frappe/erpnext/issues/56774)) ([44a7013](44a7013ab4))
* **company:** ignore user permissions for link fields having link to `Account` and `Cost Center` (backport [#56748](https://github.com/frappe/erpnext/issues/56748)) ([#56794](https://github.com/frappe/erpnext/issues/56794)) ([5666fd1](5666fd1334))
* **crm_settings:** skip allowed users check when frappe crm is installed locally ([95b3bd4](95b3bd4e1d))
* disallow editing on reversal journals ([ac2e69f](ac2e69fbfd))
* display outstanding amount using company default currency (backport [#56785](https://github.com/frappe/erpnext/issues/56785)) ([#57008](https://github.com/frappe/erpnext/issues/57008)) ([8688f1f](8688f1f51a))
* don't attempt to create SABB for non-serialized / non-batch items (backport [#56627](https://github.com/frappe/erpnext/issues/56627)) ([#56802](https://github.com/frappe/erpnext/issues/56802)) ([b0446b6](b0446b6a09))
* don't treat batch slot at FIFO queue head as qty slot ([a9330e8](a9330e8900))
* fetch asset category expense account on purchase receipt ([b7e0331](b7e0331d56))
* fetch payment entry reference amounts from invoice (backport [#56928](https://github.com/frappe/erpnext/issues/56928)) ([#57041](https://github.com/frappe/erpnext/issues/57041)) ([0f6b2b6](0f6b2b68fa))
* FIFO queue checks and incorrect entries filter in stock ledger reports ([19fc8b8](19fc8b8e6f)), closes [#43619](https://github.com/frappe/erpnext/issues/43619)
* for purchases do voucher based reposting (backport [#56601](https://github.com/frappe/erpnext/issues/56601)) ([#56607](https://github.com/frappe/erpnext/issues/56607)) ([6b022a5](6b022a5a7e))
* **inactive_customers:** add allowlist for doctype filter and migrate to qb ([29dd6e6](29dd6e6681))
* incorrect import path for FrappeTestCase ([cecfcf8](cecfcf82e0))
* **journal entry:** validate opening entry against pcv on save ([2ed7c84](2ed7c84d0a))
* make represents company field in purchase invoice ignore user permissions ([b7f2769](b7f276989d))
* make trend report based-on and group-by column labels translatable ([d2d5fae](d2d5fae033))
* **manufacturing:** update work order status on partial pick-list transfer (backport [#56800](https://github.com/frappe/erpnext/issues/56800)) ([afa7288](afa72884bb)), closes [56836/#57001](https://github.com/frappe/erpnext/issues/57001)
* map stock_qty in apply_price_list_on_item (backport [#56869](https://github.com/frappe/erpnext/issues/56869)) ([#57051](https://github.com/frappe/erpnext/issues/57051)) ([3e2a734](3e2a734414))
* partial delivery note against pick list ([#56985](https://github.com/frappe/erpnext/issues/56985)) ([53af4d5](53af4d53ef))
* **patch:** moved create_company_custom_fields from `pre_model_sync` to `post_model_sync` ([#56962](https://github.com/frappe/erpnext/issues/56962)) ([d0f2940](d0f29405d0))
* precision issue causing reconciliation error ([#54043](https://github.com/frappe/erpnext/issues/54043)) ([d62831d](d62831d885))
* prevent repeatable read related concurrency errors ([1ee8904](1ee8904a94))
* race condition and repeatable read in process pcv ([ae43f47](ae43f47c39))
* remove company default on cost center in stock entry detail ([b170913](b170913880))
* remove incorrect Payable account_type from Customer Deposits in Philippines CoA (backport [#57018](https://github.com/frappe/erpnext/issues/57018)) ([#57060](https://github.com/frappe/erpnext/issues/57060)) ([bd62699](bd6269918f))
* rename variant item_code/item_name when attribute abbreviation changes ([99bdef6](99bdef6baf))
* replay immutable SLE qty for serial/batch bundle valuation (backport [#56814](https://github.com/frappe/erpnext/issues/56814)) ([#56834](https://github.com/frappe/erpnext/issues/56834)) ([1cf94f5](1cf94f5501))
* resolve conflicts ([da623eb](da623eb124))
* **selling:** update sales order per billed on credit note submission ([63e85c6](63e85c6551))
* set customer group in inactive customers report test setup ([ede2a68](ede2a68afd))
* show only template items in Variant Of filter ([fde7cd5](fde7cd5345))
* **stock:** backfill transferred qty for existing pick lists ([cd61243](cd612434a4))
* **stock:** correct stock ageing value for moving average and lifo items (backport [#56693](https://github.com/frappe/erpnext/issues/56693)) ([#57016](https://github.com/frappe/erpnext/issues/57016)) ([9d654b7](9d654b7890))
* **stock:** pick list serial batch posting date ([#56957](https://github.com/frappe/erpnext/issues/56957)) ([#57014](https://github.com/frappe/erpnext/issues/57014)) ([4091188](4091188908)), closes [#56951](https://github.com/frappe/erpnext/issues/56951)
* **stock:** set stock received but not billed account for purchase ([#55149](https://github.com/frappe/erpnext/issues/55149)) ([38b7042](38b7042481))
* update BOM operations when routing is changed ([de0c75b](de0c75bcba))
* update events order by date asc (backport [#56963](https://github.com/frappe/erpnext/issues/56963)) ([#57055](https://github.com/frappe/erpnext/issues/57055)) ([1cb4307](1cb4307578))
* update modified timestamp in item.json ([902a3c5](902a3c5c55))
* use change_settings decorator matching version-15-hotfix test utils ([5cedc50](5cedc50c55))
* use correct import ([db4136b](db4136b258))
* use live source warehouse valuation for internal transfer purchse receipts ([#56812](https://github.com/frappe/erpnext/issues/56812)) ([ece1c98](ece1c9872f))
* validate planned end date is not before planned start date in work order ([de6a157](de6a157773))
* validate reverse GL entries on current date under immutable ledger (backport [#56709](https://github.com/frappe/erpnext/issues/56709)) ([#56751](https://github.com/frappe/erpnext/issues/56751)) ([71a2d6e](71a2d6e43d))
* validate template and its variant in the same Pricing Rule ([3df52c7](3df52c7fc6))

### Features

* **crm_settings:** auto-update crm sync settings on frappe crm install and uninstall ([0a5daee](0a5daeed57))
* **opening invoice creation tool:** add project to opening invoice child row (backport [#54662](https://github.com/frappe/erpnext/issues/54662)) ([#56400](https://github.com/frappe/erpnext/issues/56400)) ([808109a](808109abc0))
* **sla:** filter service level agreement link by document type (backport [#56954](https://github.com/frappe/erpnext/issues/56954)) ([#56955](https://github.com/frappe/erpnext/issues/56955)) ([5166950](51669501ab))
* **stock:** support partial transfer from pick list ([02168b7](02168b7b3d))

### Performance Improvements

* avoid per-row Warehouse doc fetches in auto reorder job ([c10b86d](c10b86d538))
* batch bin lookups in delivery note stock update ([989a6b5](989a6b507d))
2026-07-13 07:37:49 +00:00
Diptanil Saha
88b6779062 Merge pull request #56930 from frappe/version-15-hotfix
chore: release v15
2026-07-13 13:04:19 +05:30
Frappe PR Bot
b5f784612d chore(release): Bumped to Version 15.115.0
# [15.115.0](https://github.com/frappe/erpnext/compare/v15.114.0...v15.115.0) (2026-07-01)

### Bug Fixes

* add permission checks in whitelisted functions (backport [#53103](https://github.com/frappe/erpnext/issues/53103)) ([#56668](https://github.com/frappe/erpnext/issues/56668)) ([3455877](345587754f))
* allow rename for Quality Inspection Parameter ([9e6edea](9e6edea818))
* carry item-level project to Purchase Receipt GL entries (backport [#56568](https://github.com/frappe/erpnext/issues/56568)) ([#56618](https://github.com/frappe/erpnext/issues/56618)) ([83e8d1e](83e8d1eb2f))
* **company:** using `String.prototype.bold` method instead of `frappe.utils.bold` on company deletion prompt ([#56441](https://github.com/frappe/erpnext/issues/56441)) ([414b366](414b3665c1))
* **controllers:** fix supplier-RFQ portal list query (wrong column + Postgres DISTINCT) ([9125ab6](9125ab6c77))
* **crm:** using `get_list` instead of `get_all` in `get_opportunities` (backport [#56463](https://github.com/frappe/erpnext/issues/56463)) ([#56465](https://github.com/frappe/erpnext/issues/56465)) ([28e4bca](28e4bca4f2))
* do not allow closing the accounting period for future dates (backport [#56551](https://github.com/frappe/erpnext/issues/56551)) ([#56576](https://github.com/frappe/erpnext/issues/56576)) ([54fcd09](54fcd09995))
* extract stock ageing data helper in stock balance (backport [#55774](https://github.com/frappe/erpnext/issues/55774)) ([8abc278](8abc27863a)), closes [#55776](https://github.com/frappe/erpnext/issues/55776)
* gross profit calculation with rate adjustment entries ([17733a5](17733a5641))
* handle missing serial and batch bundle in print format ([9adfab1](9adfab19dc))
* ignored posting time 00:00:00 in RIV (backport [#56571](https://github.com/frappe/erpnext/issues/56571)) ([#56572](https://github.com/frappe/erpnext/issues/56572)) ([5ed946b](5ed946b3b9))
* **lead:** added missing read permission check on `get_lead_details` (backport [#56272](https://github.com/frappe/erpnext/issues/56272)) ([#56273](https://github.com/frappe/erpnext/issues/56273)) ([afddf70](afddf70905))
* negative stock issue ([#56616](https://github.com/frappe/erpnext/issues/56616)) ([ab98d19](ab98d19f26))
* precision issue causing COGS in inter transfer PR (backport [#56420](https://github.com/frappe/erpnext/issues/56420)) ([#56424](https://github.com/frappe/erpnext/issues/56424)) ([db8a26a](db8a26a3af))
* remove dead bundle helper call from purchase receipt print format ([0f9429f](0f9429fd3d))
* remove frappe.utils from jinja context in process statement of accounts ([b176fb2](b176fb292b))
* reserve projected stock for production plan based on BOM qty ([d3df0bf](d3df0bf387))
* set mr status to received when per_received is 100 even if per_ordered < 100 ([53bdcce](53bdccee5f))
* skip qty over-allowance check for non-stock items only ([bba7fe9](bba7fe9831))
* **stock:** value batch/serial return from ledger when original receipt has no bundle (backport [#56631](https://github.com/frappe/erpnext/issues/56631)) ([#56645](https://github.com/frappe/erpnext/issues/56645)) ([3479d65](3479d65bd4))
* sync Stock Reconciliation difference amount with GL after reposting (backport [#56574](https://github.com/frappe/erpnext/issues/56574)) ([#56584](https://github.com/frappe/erpnext/issues/56584)) ([5625635](562563553c))
* update qty in future SLEs when cancelling documents (backport [#56638](https://github.com/frappe/erpnext/issues/56638)) ([#56644](https://github.com/frappe/erpnext/issues/56644)) ([2576452](25764520c2))
* update_qty_in_future_sle skips SLEs with same posting datetime ([#56615](https://github.com/frappe/erpnext/issues/56615)) ([f50e529](f50e529f8a))
* Use correct doctype name for PCV perm-check (backport [#56606](https://github.com/frappe/erpnext/issues/56606)) ([#56610](https://github.com/frappe/erpnext/issues/56610)) ([52d04ad](52d04ad834))
* use correct variable to fetch valuation method ([c2b7718](c2b7718455))

### Features

* **accounts:** add configurable job timeout for Process Period Closing Voucher ([c97be8a](c97be8abe1))
2026-07-01 03:40:41 +00:00
Diptanil Saha
457424f7a4 Merge pull request #56651 from frappe/version-15-hotfix
chore: release v15
2026-07-01 09:08:59 +05:30
Frappe PR Bot
25ee3695f0 chore(release): Bumped to Version 15.114.0
# [15.114.0](https://github.com/frappe/erpnext/compare/v15.113.0...v15.114.0) (2026-06-24)

### Features

* **accounts:** add configurable job timeout for Process Period Closing Voucher ([82a8581](82a85818c2))
2026-06-24 10:25:43 +00:00
ruthra kumar
ff205da810 Merge pull request #56426 from frappe/mergify/bp/version-15/pr-56418
refactor: configurable timeout on process pcv (backport #56417) (backport #56418)
2026-06-24 15:48:58 +05:30
ruthra kumar
2980171007 chore: resolve conflicts
(cherry picked from commit df3c821f98)
2026-06-24 09:56:59 +00:00
ruthra kumar
c6c4815e8d refactor: patch, display depends on and json changes
(cherry picked from commit 3da7eefebb)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
#	erpnext/patches.txt
(cherry picked from commit c33d7e5d7b)
2026-06-24 09:56:59 +00:00
ruthra kumar
82a85818c2 feat(accounts): add configurable job timeout for Process Period Closing Voucher
Adds a `pcv_job_timeout` Int field (default 3600s) to Accounts Settings
so admins can tune the enqueue timeout for PCV background jobs without
a code change. All three `frappe.enqueue` calls in
`process_period_closing_voucher.py` now read this value at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 13b6c4a165)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
(cherry picked from commit c97be8abe1)
2026-06-24 09:56:58 +00:00
Frappe PR Bot
57a2be6b56 chore(release): Bumped to Version 15.113.0
# [15.113.0](https://github.com/frappe/erpnext/compare/v15.112.0...v15.113.0) (2026-06-23)

### Bug Fixes

* add dynamic links for customer and supplier dashboards ([690adf1](690adf1051))
* Add likely missing escapes (backport [#55574](https://github.com/frappe/erpnext/issues/55574)) ([#55580](https://github.com/frappe/erpnext/issues/55580)) ([ce8fce7](ce8fce78f1))
* add partially transferred status and fix button visibility for partial material transfer on job card ([1f5283d](1f5283da58))
* add validation and tests for set_status ([7bea925](7bea925230))
* apply docstatus filter to exclude cancelled Work Orders in Serial No ([90fd057](90fd057fb3))
* attribute error because of missing margin_type field in Supplier Quotation (backport [#48089](https://github.com/frappe/erpnext/issues/48089))  ([506658c](506658c3a6))
* **budget:** ambiguous error message for budget assignment validation (backport [#56390](https://github.com/frappe/erpnext/issues/56390)) ([#56391](https://github.com/frappe/erpnext/issues/56391)) ([53a1122](53a11229ec))
* clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (backport [#55903](https://github.com/frappe/erpnext/issues/55903)) ([#56169](https://github.com/frappe/erpnext/issues/56169)) ([37d2622](37d26222d7))
* disable is_debit_note while creating credit note ([e4370ab](e4370ab332))
* **err:** add missing permission check on `get_account_details` ([041a9ad](041a9adbbf))
* escape user image url on various templates (backport [#56269](https://github.com/frappe/erpnext/issues/56269)) ([#56270](https://github.com/frappe/erpnext/issues/56270)) ([42af4ce](42af4ce7b0))
* **manufacturing:** make item_code mandatory in Job Card Item ([1b4da9d](1b4da9dc96))
* **payment_entry:** recompute base amount when exchange rate changes (backport [#56136](https://github.com/frappe/erpnext/issues/56136)) ([#56397](https://github.com/frappe/erpnext/issues/56397)) ([cef608d](cef608d043))
* **pos:** remove redundant opening balance dialog onchange handler (backport [#54591](https://github.com/frappe/erpnext/issues/54591)) ([#56402](https://github.com/frappe/erpnext/issues/56402)) ([334a0b2](334a0b2137))
* preserve stock ageing on non-serial reconciliation ([1991312](19913127a7))
* **report_utils:** remove unnecessary whitelist decorator on `get_invoiced_item_gross_margin` ([0efebf5](0efebf5d8c))
* resolve backport conflict in accounting dashboard number cards ([f106513](f106513005)), closes [#55548](https://github.com/frappe/erpnext/issues/55548) [#55484](https://github.com/frappe/erpnext/issues/55484)
* set a fallback value if no fiscal year set ([da1ccc2](da1ccc2b62))
* show contextual balance label on party dashboard for net balances ([9b6adc4](9b6adc42b6))
* simplify get_round_off_applicable_accounts function signature ([42121f2](42121f2e36))
* **stock:** allow partial raw material picking/transfer from work order ([a858d77](a858d77461))
* **stock:** apply precision to the additional cost amount in stock entry ([acc1444](acc1444c03))
* **stock:** propagate renamed attribute values to variant items ([27d574d](27d574dad5))
* **stock:** update transfer status for mixed transfer flows ([3f9a88a](3f9a88a5e2))
* **stock:** update variant attributes on value rename ([c7acd88](c7acd88742))
* **stock:** update voucher valuaion rate in sle (backport [#55960](https://github.com/frappe/erpnext/issues/55960)) ([#56262](https://github.com/frappe/erpnext/issues/56262)) ([37f847e](37f847e730))
* tax.base_tax_amount as none when payment entry created using API ([37dffa7](37dffa7273))
* update reference doctype mapping and field visibility in bank guarantee ([e556cbb](e556cbbe6a))
* update round off account functions to accept document context for regional overrides ([#55758](https://github.com/frappe/erpnext/issues/55758)) ([eef075a](eef075a2ba))
* use fiscal year instead of calendar year in accounting dashboard number cards ([81ce5fb](81ce5fbee9))

### Features

* add batch-level option to allow negative stock for batch ([5c4f19e](5c4f19ebdc))
2026-06-23 21:37:17 +00:00
Diptanil Saha
47f54a4725 Merge pull request #56360 from frappe/version-15-hotfix
chore: release v15
2026-06-24 03:05:42 +05:30
23 changed files with 107 additions and 1069 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.112.0"
__version__ = "15.119.0"
def get_default_company(user=None):

View File

@@ -222,9 +222,6 @@ class Subscription(Document):
"""
Sets the status of the `Subscription`
"""
if self.status == "Cancelled":
return
if self.is_trialling():
self.status = "Trialling"
elif self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date):
@@ -561,11 +558,6 @@ class Subscription(Document):
1. `process_for_active`
2. `process_for_past_due`
"""
# Snapshot before update_subscription_period() below can roll this forward,
# so the cancel_at_period_end check further down still targets the period
# that just ended, not the next one.
current_period_end = self.current_invoice_end
if not self.is_current_invoice_generated(
self.current_invoice_start, self.current_invoice_end
) and self.can_generate_new_invoice(posting_date):
@@ -575,8 +567,8 @@ class Subscription(Document):
self.update_subscription_period()
if self.cancel_at_period_end and (
getdate(posting_date) >= getdate(current_period_end)
or (self.end_date and getdate(posting_date) >= getdate(self.end_date))
getdate(posting_date) >= getdate(self.current_invoice_end)
or getdate(posting_date) >= getdate(self.end_date)
):
self.cancel_subscription()

View File

@@ -280,59 +280,6 @@ class TestSubscription(FrappeTestCase):
settings.cancel_after_grace = default_grace_period_action
settings.save()
def test_cancelled_subscription_stays_cancelled_after_payment_and_reprocess(self):
# https://github.com/frappe/erpnext/issues/57761
subscription = create_subscription(
start_date=nowdate(), generate_invoice_at="Beginning of the current subscription period"
)
subscription.process(posting_date=nowdate()) # generate first invoice
invoice = subscription.get_current_invoice()
self.assertIsNotNone(invoice)
invoice.db_set("outstanding_amount", 0)
invoice.db_set("status", "Paid")
subscription.cancel_subscription()
self.assertEqual(subscription.status, "Cancelled")
cancelation_date = getdate(subscription.cancelation_date)
subscription.set_subscription_status()
self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(getdate(subscription.cancelation_date), cancelation_date)
subscription.cancel_at_period_end = 1
subscription.end_date = None
invoice_count = len(subscription.invoices)
subscription.process()
self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), invoice_count)
def test_subscription_cancels_at_period_end_without_end_date(self):
# https://github.com/frappe/erpnext/issues/57761 -- generate_invoice() rolls
# current_invoice_end forward to the next period before this check runs, so
# with no end_date to fall back on, cancel_at_period_end must compare
# against the period that just ended, not the (already advanced) next one.
create_plan(
plan_name="_Test plan name 11",
cost=80,
currency="INR",
billing_interval="Day",
billing_interval_count=3,
)
subscription = create_subscription(
start_date=nowdate(),
generate_invoice_at="End of the current subscription period",
plans=[{"plan": "_Test plan name 11", "qty": 1}],
)
subscription.cancel_at_period_end = 1
self.assertEqual(len(subscription.invoices), 0)
period_end = subscription.current_invoice_end
subscription.process(posting_date=period_end)
self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), 1)
def test_subscription_restart_and_process(self):
settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace

View File

@@ -117,11 +117,8 @@ frappe.query_reports["Accounts Payable"] = {
{
fieldname: "supplier_group",
label: __("Supplier Group"),
fieldtype: "MultiSelectList",
fieldtype: "Link",
options: "Supplier Group",
get_data: function (txt) {
return frappe.db.get_link_options("Supplier Group", txt);
},
hidden: 1,
},
{

View File

@@ -121,36 +121,6 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_supplier_group_filter(self):
pi = self.create_purchase_invoice()
supplier_group = frappe.db.get_value("Supplier", self.supplier, "supplier_group")
other_group = frappe.get_doc(
doctype="Supplier Group",
supplier_group_name="_Test Supplier Group AP",
parent_supplier_group="All Supplier Groups",
).insert()
filters = {
"company": self.company,
"party_type": "Supplier",
"report_date": today(),
"range": "30, 60, 90, 120",
"supplier_group": supplier_group,
}
self.assertIn(pi.name, [row.voucher_no for row in execute(filters)[1]])
filters.update({"supplier_group": [other_group.name]})
self.assertEqual(len(execute(filters)[1]), 0)
filters.update({"supplier_group": [supplier_group, other_group.name]})
self.assertIn(pi.name, [row.voucher_no for row in execute(filters)[1]])
filters.update({"supplier_group": ["All Supplier Groups"]})
self.assertIn(pi.name, [row.voucher_no for row in execute(filters)[1]])
filters.update({"supplier_group": ["_Test Supplier Group Mars"]})
self.assertRaises(frappe.ValidationError, execute, filters)
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}

View File

@@ -100,11 +100,8 @@ frappe.query_reports["Accounts Payable Summary"] = {
{
fieldname: "supplier_group",
label: __("Supplier Group"),
fieldtype: "MultiSelectList",
fieldtype: "Link",
options: "Supplier Group",
get_data: function (txt) {
return frappe.db.get_link_options("Supplier Group", txt);
},
},
{
fieldname: "based_on_payment_terms",

View File

@@ -140,11 +140,8 @@ frappe.query_reports["Accounts Receivable"] = {
{
fieldname: "territory",
label: __("Territory"),
fieldtype: "MultiSelectList",
fieldtype: "Link",
options: "Territory",
get_data: function (txt) {
return frappe.db.get_link_options("Territory", txt);
},
},
{
fieldname: "group_by_party",

View File

@@ -1013,13 +1013,7 @@ class ReceivablePayableReport:
self.qb_selection_filter.append(self.ple.party.isin(customers))
if self.filters.get("territory"):
territories = get_nested_set_children("Territory", self.filters.territory)
customers = (
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer["territory"].isin(territories))
)
self.qb_selection_filter.append(self.ple.party.isin(customers))
self.get_hierarchical_filters("Territory", "territory")
if self.filters.get("payment_terms_template"):
customer_ptt = self.ple.party.isin(
@@ -1040,10 +1034,11 @@ class ReceivablePayableReport:
def add_supplier_filters(self):
supplier = qb.DocType("Supplier")
if self.filters.get("supplier_group"):
groups = get_party_group_with_children("Supplier", self.filters.supplier_group)
self.qb_selection_filter.append(
self.ple.party.isin(
qb.from_(supplier).select(supplier.name).where(supplier.supplier_group.isin(groups))
qb.from_(supplier)
.select(supplier.name)
.where(supplier.supplier_group == self.filters.get("supplier_group"))
)
)
@@ -1095,6 +1090,16 @@ class ReceivablePayableReport:
return ptt
def get_hierarchical_filters(self, doctype, key):
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
doc = qb.DocType(doctype)
ple = self.ple
customer = self.customer
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
self.qb_selection_filter.append(ple.party.isin(customers))
def add_accounting_dimensions_filters(self):
accounting_dimensions = get_accounting_dimensions(as_list=False)
@@ -1324,23 +1329,19 @@ def get_party_group_with_children(party, party_groups):
if party not in ("Customer", "Supplier"):
return []
return get_nested_set_children(f"{party} Group", party_groups)
group_dtype = f"{party} Group"
if not isinstance(party_groups, list):
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
def get_nested_set_children(doctype, values):
if not isinstance(values, list):
values = [d.strip() for d in values.split(",") if d.strip()]
if not values:
frappe.throw(_("Please select a valid {0}").format(_(doctype)))
all_values = []
for d in values:
if frappe.db.exists(doctype, d):
lft, rgt = frappe.db.get_value(doctype, d, ["lft", "rgt"])
children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name")
all_values += children
all_party_groups = []
for d in party_groups:
if frappe.db.exists(group_dtype, d):
lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"])
children = frappe.get_all(
group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name"
)
all_party_groups += children
else:
frappe.throw(_("{0}: {1} does not exist").format(doctype, d))
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
return list(set(all_values))
return list(set(all_party_groups))

View File

@@ -771,38 +771,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
# Assert that the customer group of each row is in the list of customer groups
self.assertIn(row.customer_group, cus_groups_list)
def test_territory_filter(self):
self.create_sales_invoice()
territory = frappe.db.get_value("Customer", self.customer, "territory")
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"territory": territory,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
self.assertEqual(
[100.0, 100.0, territory], [report[0].invoiced, report[0].outstanding, report[0].territory]
)
filters.update({"territory": ["_Test Territory United States"]})
self.assertEqual(len(execute(filters)[1]), 0)
filters.update({"territory": [territory, "_Test Territory United States"]})
self.assertEqual(len(execute(filters)[1]), 1)
frappe.db.set_value("Customer", self.customer, "territory", "_Test Territory Maharashtra")
filters.update({"territory": ["_Test Territory India"]})
self.assertEqual(len(execute(filters)[1]), 1)
filters.update({"territory": ["_Test Territory Mars"]})
self.assertRaises(frappe.ValidationError, execute, filters)
filters.update({"territory": " "})
self.assertRaises(frappe.ValidationError, execute, filters)
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (

View File

@@ -106,11 +106,8 @@ frappe.query_reports["Accounts Receivable Summary"] = {
{
fieldname: "territory",
label: __("Territory"),
fieldtype: "MultiSelectList",
fieldtype: "Link",
options: "Territory",
get_data: function (txt) {
return frappe.db.get_link_options("Territory", txt);
},
},
{
fieldname: "sales_partner",

View File

@@ -93,27 +93,5 @@ frappe.query_reports["Customer Ledger Summary"] = {
fieldtype: "Data",
hidden: 1,
},
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
],
};
erpnext.utils.add_dimensions("Customer Ledger Summary", 14);

View File

@@ -4,10 +4,7 @@
import frappe
from frappe import _, qb, scrub
<<<<<<< HEAD
from frappe.query_builder import Criterion, Tuple
=======
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
from frappe.query_builder.functions import IfNull
from frappe.utils import getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
@@ -22,12 +19,6 @@ TREE_DOCTYPES = frozenset(
["Customer Group", "Territory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
)
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
class PartyLedgerSummaryReport:
def __init__(self, filters=None):
@@ -35,21 +26,6 @@ class PartyLedgerSummaryReport:
self.filters.from_date = getdate(self.filters.from_date or nowdate())
self.filters.to_date = getdate(self.filters.to_date or nowdate())
<<<<<<< HEAD
<<<<<<< HEAD
=======
if self.filters.get("cost_center"):
self.filters.cost_center = frappe.parse_json(self.filters.get("cost_center"))
if self.filters.get("project"):
self.filters.project = frappe.parse_json(self.filters.get("project"))
=======
>>>>>>> 9610a33d23 (fix: remove irrelavent conditions)
if not self.filters.get("company"):
self.filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company")
>>>>>>> 901bcd5c43 (feat: add accounting dimensions in ledger summary reports)
def run(self, args):
self.filters.party_type = args.get("party_type")
@@ -344,10 +320,6 @@ class PartyLedgerSummaryReport:
return out
def get_gl_entries(self):
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
gle = qb.DocType("GL Entry")
query = (
qb.from_(gle)
@@ -356,12 +328,7 @@ class PartyLedgerSummaryReport:
gle.party,
gle.voucher_type,
gle.voucher_no,
<<<<<<< HEAD
gle.against_voucher, # For handling returned invoices (Credit/Debit Notes)
=======
gle.against_voucher_type,
gle.against_voucher,
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
gle.debit,
gle.credit,
gle.is_opening,
@@ -372,60 +339,12 @@ class PartyLedgerSummaryReport:
& (gle.party_type == self.filters.party_type)
& (IfNull(gle.party, "") != "")
& (gle.posting_date <= self.filters.to_date)
<<<<<<< HEAD
& (gle.party.isin(self.parties))
)
=======
conditions = self.prepare_conditions()
join = join_field = ""
if self.filters.party_type == "Customer":
join_field = ", p.customer_name as party_name"
join = "left join `tabCustomer` p on gle.party = p.name"
elif self.filters.party_type == "Supplier":
join_field = ", p.supplier_name as party_name"
join = "left join `tabSupplier` p on gle.party = p.name"
self.gl_entries = frappe.db.sql(
f"""
select
gle.posting_date, gle.party, gle.voucher_type, gle.voucher_no, gle.against_voucher_type,
gle.against_voucher, gle.debit, gle.credit, gle.is_opening {join_field}
from `tabGL Entry` gle
{join}
where
gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != ''
and gle.posting_date <= %(to_date)s {conditions}
order by gle.posting_date
""",
self.filters,
as_dict=True,
>>>>>>> 901bcd5c43 (feat: add accounting dimensions in ledger summary reports)
)
query = self.prepare_conditions(query)
=======
)
.orderby(gle.posting_date)
)
if self.filters.party_type == "Customer":
customer = qb.DocType("Customer")
query = (
query.select(customer.customer_name.as_("party_name"))
.left_join(customer)
.on(customer.name == gle.party)
)
elif self.filters.party_type == "Supplier":
supplier = qb.DocType("Supplier")
query = (
query.select(supplier.supplier_name.as_("party_name"))
.left_join(supplier)
.on(supplier.name == gle.party)
)
query = self.prepare_conditions(query)
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
self.gl_entries = query.run(as_dict=True)
def prepare_conditions(self, query):
@@ -436,7 +355,6 @@ class PartyLedgerSummaryReport:
if self.filters.finance_book:
query = query.where(IfNull(gle.finance_book, "") == self.filters.finance_book)
<<<<<<< HEAD
if self.filters.cost_center:
query = query.where((gle.cost_center).isin(self.filters.cost_center))
@@ -460,107 +378,7 @@ class PartyLedgerSummaryReport:
(gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
)
<<<<<<< HEAD
return query
=======
conditions.append(
f"""party in (select name from tabCustomer
where exists(select name from `tabTerritory` where lft >= {lft} and rgt <= {rgt}
and name=tabCustomer.territory))"""
=======
if self.filters.party:
query = query.where(gle.party == self.filters.party)
if self.filters.party_type == "Customer":
customer = qb.DocType("Customer")
if self.filters.customer_group:
query = query.where(
(gle.party).isin(
qb.from_(customer)
.select(customer.name)
.where(customer.customer_group == self.filters.customer_group)
)
)
if self.filters.territory:
query = query.where(
(gle.party).isin(
qb.from_(customer)
.select(customer.name)
.where(customer.territory == self.filters.territory)
)
)
if self.filters.payment_terms_template:
query = query.where(
(gle.party).isin(
qb.from_(customer)
.select(customer.name)
.where(customer.payment_terms == self.filters.payment_terms_template)
)
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
)
if self.filters.sales_partner:
query = query.where(
(gle.party).isin(
qb.from_(customer)
.select(customer.name)
.where(customer.default_sales_partner == self.filters.sales_partner)
)
)
if self.filters.sales_person:
sales_team = qb.DocType("Sales Team")
query = query.where(
(gle.party).isin(
qb.from_(sales_team)
.select(sales_team.parent)
.where(sales_team.sales_person == self.filters.sales_person)
)
)
if self.filters.party_type == "Supplier":
if self.filters.supplier_group:
supplier = qb.DocType("Supplier")
query = query.where(
(gle.party).isin(
qb.from_(supplier)
.select(supplier.name)
.where(supplier.supplier_group == self.filters.supplier_group)
)
)
if self.filters.cost_center:
self.filters.cost_center = get_cost_centers_with_children(self.filters.cost_center)
query = query.where((gle.cost_center).isin(self.filters.cost_center))
if self.filters.project:
query = query.where((gle.project).isin(self.filters.project))
accounting_dimensions = get_accounting_dimensions(as_list=False)
if accounting_dimensions:
for dimension in accounting_dimensions:
if self.filters.get(dimension.fieldname):
if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
self.filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, self.filters.get(dimension.fieldname)
)
query = query.where(
(gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
)
else:
query = query.where(
(gle[dimension.fieldname]).isin(self.filters.get(dimension.fieldname))
)
<<<<<<< HEAD
return " and ".join(conditions)
>>>>>>> 901bcd5c43 (feat: add accounting dimensions in ledger summary reports)
=======
return query
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
def get_return_invoices(self):
doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice"
@@ -577,53 +395,13 @@ class PartyLedgerSummaryReport:
def get_party_adjustment_amounts(self):
account_type = "Expense Account" if self.filters.party_type == "Customer" else "Income Account"
<<<<<<< HEAD
=======
self.income_or_expense_accounts = frappe.db.get_all(
"Account", filters={"account_type": account_type, "company": self.filters.company}, pluck="name"
)
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit"
round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account")
<<<<<<< HEAD
current_period_vouchers = set()
adjustment_voucher_entries = {}
=======
if not self.income_or_expense_accounts:
# prevent empty 'in' condition
self.income_or_expense_accounts.append("")
else:
# escape '%' in account name
# ignoring frappe.db.escape as it replaces single quotes with double quotes
self.income_or_expense_accounts = [x.replace("%", "%%") for x in self.income_or_expense_accounts]
gl = qb.DocType("GL Entry")
accounts_query = self.get_base_accounts_query()
accounts_query_voucher_no = accounts_query.select(gl.voucher_no)
accounts_query_voucher_type = accounts_query.select(gl.voucher_type)
subquery = self.get_base_subquery()
subquery_voucher_no = subquery.select(gl.voucher_no)
subquery_voucher_type = subquery.select(gl.voucher_type)
gl_entries = (
qb.from_(gl)
.select(
gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit
)
.where(
(gl.docstatus < 2)
& (gl.is_cancelled == 0)
& (gl.voucher_no.isin(accounts_query_voucher_no))
& (gl.voucher_type.isin(accounts_query_voucher_type))
& (gl.voucher_no.isin(subquery_voucher_no))
& (gl.voucher_type.isin(subquery_voucher_type))
)
).run(as_dict=True)
>>>>>>> 7614f166d8 (refactor: convert sql queries to qb queries)
self.party_adjustment_details = {}
self.party_adjustment_accounts = set()
@@ -694,26 +472,6 @@ class PartyLedgerSummaryReport:
self.party_adjustment_details[party].setdefault(account, 0)
self.party_adjustment_details[party][account] += amount
def get_base_accounts_query(self):
gl = qb.DocType("GL Entry")
query = qb.from_(gl).where(
(gl.account.isin(self.income_or_expense_accounts))
& (gl.posting_date.gte(self.filters.from_date))
& (gl.posting_date.lte(self.filters.to_date))
)
return query
def get_base_subquery(self):
gl = qb.DocType("GL Entry")
query = qb.from_(gl).where(
(gl.docstatus < 2)
& (gl.party_type == self.filters.party_type)
& (IfNull(gl.party, "") != "")
& (gl.posting_date.between(self.filters.from_date, self.filters.to_date))
)
query = self.prepare_conditions(query)
return query
def get_children(doctype, value):
if not isinstance(value, list):

View File

@@ -74,27 +74,5 @@ frappe.query_reports["Supplier Ledger Summary"] = {
fieldtype: "Data",
hidden: 1,
},
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
],
};
erpnext.utils.add_dimensions("Supplier Ledger Summary", 11);

View File

@@ -948,8 +948,6 @@ class TestPurchaseOrder(FrappeTestCase):
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
compare_payment_schedules(self, po, pi)
@change_settings("Selling Settings", {"maintain_same_sales_rate": 1})
@change_settings("Buying Settings", {"maintain_same_rate": 1})
def test_internal_transfer_flow(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@@ -961,6 +959,9 @@ class TestPurchaseOrder(FrappeTestCase):
)
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
prepare_data_for_internal_transfer()
supplier = "_Test Internal Supplier 2"

View File

@@ -312,12 +312,13 @@ class StatusUpdater(Document):
qty_or_amount,
)
role = None
if qty_or_amount == "qty":
if args.get("overflow_type") in ("delivery", "receipt"):
role = frappe.get_single_value("Stock Settings", "role_allowed_to_over_deliver_receive")
else:
role = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
role_allowed_to_over_deliver_receive = frappe.db.get_single_value(
"Stock Settings", "role_allowed_to_over_deliver_receive"
)
role_allowed_to_over_bill = frappe.db.get_single_value(
"Accounts Settings", "role_allowed_to_over_bill"
)
role = role_allowed_to_over_deliver_receive if qty_or_amount == "qty" else role_allowed_to_over_bill
overflow_percent = (
(item[args["target_field"]] - item[args["target_ref_field"]]) / item[args["target_ref_field"]]

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext import get_company_currency
@@ -91,32 +91,6 @@ class TestBlanketOrder(FrappeTestCase):
frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10)
po.submit()
@change_settings("Selling Settings", {"blanket_order_allowance": 0})
@change_settings("Buying Settings", {"blanket_order_allowance": 0})
@change_settings(
"Stock Settings",
{"over_delivery_receipt_allowance": 10, "role_allowed_to_over_deliver_receive": "Stock Manager"},
)
def test_stock_over_delivery_role_does_not_bypass_blanket_order_allowance(self):
test_user = frappe.get_doc("User", "test@example.com")
test_user.add_roles("Accounts User", "Stock Manager")
frappe.clear_cache()
for blanket_order_type, doctype, date_field in (
("Selling", "Sales Order", "delivery_date"),
("Purchasing", "Purchase Order", "schedule_date"),
):
bo = make_blanket_order(blanket_order_type=blanket_order_type, quantity=100)
frappe.flags.args.doctype = doctype
order = make_order(bo.name)
order.currency = get_company_currency(order.company)
setattr(order, date_field, today())
order.items[0].qty = 110
with self.set_user("test@example.com"):
order.flags.ignore_permissions = True
self.assertRaises(frappe.ValidationError, order.submit)
def test_party_item_code(self):
item_doc = make_item("_Test Item 1 for Blanket Order")
item_code = item_doc.name

View File

@@ -4996,66 +4996,6 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
def test_purchase_return_valuation_for_batchwise_valuation_batch(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
item_code = make_item(
"Test Purchase Return Batchwise Valn Item",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BN-TPRBWV-.#####",
},
).name
batch_no = "BN-TPRBWV-00001"
batch = frappe.new_doc("Batch").update({"batch_id": batch_no, "item": item_code}).insert()
self.assertEqual(batch.use_batchwise_valuation, 1)
warehouse = "_Test Warehouse - _TC"
pr = make_purchase_receipt(
item_code=item_code,
qty=100,
rate=1000,
warehouse=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
)
make_purchase_receipt(
item_code=item_code,
qty=100,
rate=400,
warehouse=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
)
create_delivery_note(
item_code=item_code,
qty=100,
warehouse=warehouse,
batch_no=batch_no,
use_serial_batch_fields=1,
)
return_pr = make_return_doc("Purchase Receipt", pr.name)
return_pr.submit()
sle = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": return_pr.name, "is_cancelled": 0},
["stock_value_difference", "qty_after_transaction", "stock_value", "serial_and_batch_bundle"],
as_dict=True,
)
self.assertEqual(flt(sle.qty_after_transaction), 0.0)
self.assertEqual(flt(sle.stock_value_difference, 2), -70000.0)
self.assertEqual(flt(sle.stock_value, 2), 0.0)
rate = frappe.db.get_value(
"Serial and Batch Entry", {"parent": sle.serial_and_batch_bundle}, "incoming_rate"
)
self.assertEqual(flt(rate, 2), 700.0)
def test_negative_stock_error_for_purchase_return(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

View File

@@ -402,13 +402,6 @@ class SerialandBatchBundle(Document):
valuation_method = get_valuation_method(self.item_code)
# An outward return must go out at the batch's current average rate for a
# batchwise valuation batch. The original receipt rate is only correct while
# the batch still holds stock at that rate; once other receipts have changed
# the average, removing at the original rate strands a residue in the batch
# value (negative when returning the costlier receipt).
batchwise_avg_rates = self.get_batchwise_return_avg_rates()
stock_queue = []
non_batchwise_batches = []
if not self.has_serial_no and valuation_method == "FIFO":
@@ -442,12 +435,6 @@ class SerialandBatchBundle(Document):
batches = sorted(list(valuation_details["batches"].keys()))
valuation_rate = valuation_details["batches"].get(batches[cint(row.idx) - 1])
# a batch with an available balance goes out at its current average rate (a
# valid 0.0 included); the original receipt rate applies only when there is
# no balance to average
if not row.serial_no and row.batch_no in batchwise_avg_rates:
valuation_rate = batchwise_avg_rates[row.batch_no]
row.incoming_rate = flt(valuation_rate)
row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate)
@@ -476,41 +463,6 @@ class SerialandBatchBundle(Document):
elif self.type_of_transaction == "Inward":
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
def get_batchwise_return_avg_rates(self):
from erpnext.stock.utils import get_valuation_method
if self.type_of_transaction != "Outward" or self.has_serial_no:
return {}
batch_nos = [d.batch_no for d in self.entries if d.batch_no]
if not batch_nos:
return {}
if get_valuation_method(self.item_code) == "Moving Average" and frappe.db.get_single_value(
"Stock Settings", "do_not_use_batchwise_valuation"
):
return {}
batchwise_batches = frappe.get_all(
"Batch",
filters={"name": ("in", batch_nos), "use_batchwise_valuation": 1},
pluck="name",
)
if not batchwise_batches:
return {}
# scoped to batchwise batches only, so BatchNoValuation's non-batchwise
# machinery never runs for them
sle = self.get_sle_for_outward_transaction()
sle.batch_nos = {batch_no: sle.batch_nos[batch_no] for batch_no in batchwise_batches}
sle.batchwise_valuation_batches = batchwise_batches
sn_obj = BatchNoValuation(sle=sle, item_code=self.item_code, warehouse=self.warehouse)
return {
batch_no: abs(flt(sn_obj.batch_avg_rate.get(batch_no)))
for batch_no in batchwise_batches
if flt(sn_obj.available_qty.get(batch_no))
}
def validate_returned_serial_batch_no(self, return_against, row, original_inv_details):
if frappe.flags.through_repost_item_valuation:
return

View File

@@ -131,8 +131,7 @@
"description": "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.",
"fieldname": "over_delivery_receipt_allowance",
"fieldtype": "Float",
"label": "Over Delivery/Receipt Allowance (%)",
"non_negative": 1
"label": "Over Delivery/Receipt Allowance (%)"
},
{
"default": "Stop",
@@ -283,8 +282,7 @@
"description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.",
"fieldname": "mr_qty_allowance",
"fieldtype": "Float",
"label": "Over Transfer Allowance",
"non_negative": 1
"label": "Over Transfer Allowance"
},
{
"default": "0",
@@ -448,8 +446,7 @@
"description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.",
"fieldname": "over_picking_allowance",
"fieldtype": "Percent",
"label": "Over Picking Allowance",
"non_negative": 1
"label": "Over Picking Allowance"
},
{
"default": "1",
@@ -531,7 +528,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-08-01 23:35:02.896836",
"modified": "2026-03-27 22:39:16.812184",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

View File

@@ -101,7 +101,6 @@ class StockSettings(Document):
)
self.validate_warehouses()
self.validate_over_delivery_receipt_allowance()
self.cant_change_valuation_method()
self.validate_clean_description_html()
self.validate_pending_reposts()
@@ -111,10 +110,6 @@ class StockSettings(Document):
self.change_precision_for_purchase()
self.validate_do_not_use_batchwise_valuation()
def validate_over_delivery_receipt_allowance(self):
if not self.over_delivery_receipt_allowance:
self.role_allowed_to_over_deliver_receive = None
def validate_do_not_use_batchwise_valuation(self):
doc_before_save = self.get_doc_before_save()
if not doc_before_save:

View File

@@ -7,10 +7,8 @@ from collections import defaultdict
import frappe
from frappe import _
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, flt, get_datetime
from pypika import Order
from pypika.analytics import RowNumber
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -55,15 +53,14 @@ def execute(filters=None):
data = []
conversion_factors = []
opening_rows = opening_row if isinstance(opening_row, list) else ([opening_row] if opening_row else [])
for row in opening_rows:
data.append(row)
if opening_row:
data.append(opening_row)
conversion_factors.append(0)
actual_qty = stock_value = 0
if opening_rows:
actual_qty = opening_rows[0].get("qty_after_transaction", 0)
stock_value = opening_rows[0].get("stock_value", 0)
if opening_row:
actual_qty = opening_row.get("qty_after_transaction")
stock_value = opening_row.get("stock_value")
available_serial_nos = {}
@@ -690,120 +687,43 @@ def get_opening_balance(filters, columns, sl_entries, inv_dimension_wise_value=N
if not (filters.item_code and filters.warehouse and filters.from_date):
return
item_codes = filters.item_code
if isinstance(item_codes, str):
item_codes = [item_codes]
from erpnext.stock.stock_ledger import get_previous_sle
warehouses = get_matching_warehouses(filters.warehouse)
if not warehouses:
return
project = None
if filters.get("project") and not frappe.get_all(
"Inventory Dimension", filters={"reference_document": "Project"}
):
project = filters.get("project")
sle_doctype = frappe.qb.DocType("Stock Ledger Entry")
sr_doctype = frappe.qb.DocType("Stock Reconciliation")
opening_reco_query = (
frappe.qb.from_(sle_doctype)
.inner_join(sr_doctype)
.on(sle_doctype.voucher_no == sr_doctype.name)
.select(sle_doctype.voucher_no)
.where(sle_doctype.docstatus < 2)
.where(sle_doctype.is_cancelled == 0)
.where(sle_doctype.item_code.isin(item_codes))
.where(sle_doctype.warehouse.isin(warehouses))
.where(sle_doctype.voucher_type == "Stock Reconciliation")
.where(sle_doctype.posting_date == filters.from_date)
.where(sr_doctype.purpose == "Opening Stock")
last_entry = get_previous_sle(
{
"item_code": filters.item_code,
"warehouse_condition": get_warehouse_condition(filters.warehouse),
"posting_date": filters.from_date,
"posting_time": "00:00:00",
"project": project,
},
for_report=True,
)
opening_reco_vouchers = set(opening_reco_query.run(pluck=True))
# check if any SLEs are actually Opening Stock Reconciliation
for sle in list(sl_entries):
if (
sle.get("voucher_type") == "Stock Reconciliation"
and sle.posting_date == filters.from_date
and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock"
):
last_entry = sle
sl_entries.remove(sle)
if opening_reco_vouchers:
sl_entries[:] = [sle for sle in sl_entries if sle.get("voucher_no") not in opening_reco_vouchers]
sle_cond = (sle_doctype.posting_date < filters.from_date) | (
(sle_doctype.posting_date == filters.from_date) & (sle_doctype.posting_time == "00:00:00")
)
if opening_reco_vouchers:
sle_cond = sle_cond | (
(sle_doctype.posting_date == filters.from_date)
& (sle_doctype.voucher_no.isin(list(opening_reco_vouchers)))
)
subq = (
frappe.qb.from_(sle_doctype)
.select(
sle_doctype.qty_after_transaction,
sle_doctype.stock_value,
RowNumber()
.over(sle_doctype.item_code, sle_doctype.warehouse)
.orderby(sle_doctype.posting_datetime, sle_doctype.creation, sle_doctype.name, order=Order.desc)
.as_("rn"),
)
.where(sle_doctype.docstatus < 2)
.where(sle_doctype.is_cancelled == 0)
.where(sle_doctype.item_code.isin(item_codes))
.where(sle_doctype.warehouse.isin(warehouses))
.where(sle_cond)
)
for field in ["voucher_no", "project", "company"]:
if filters.get(field):
subq = subq.where(sle_doctype[field] == filters.get(field))
inventory_dimension_fields = get_inventory_dimension_fields()
if inventory_dimension_fields:
for fieldname in inventory_dimension_fields:
if filters.get(fieldname):
subq = subq.where(sle_doctype[fieldname].isin(filters.get(fieldname)))
query = (
frappe.qb.from_(subq)
.select(
IfNull(Sum(subq.qty_after_transaction), 0.0).as_("total_qty"),
IfNull(Sum(subq.stock_value), 0.0).as_("total_stock_value"),
)
.where(subq.rn == 1)
)
res = query.run(as_dict=True)
total_qty = flt(res[0].total_qty) if res else 0.0
total_stock_value = flt(res[0].total_stock_value) if res else 0.0
valuation_rate = flt(total_stock_value / total_qty) if total_qty else 0.0
return {
row = {
"item_code": _("'Opening'"),
"qty_after_transaction": total_qty,
"valuation_rate": valuation_rate,
"stock_value": total_stock_value,
"qty_after_transaction": last_entry.get("qty_after_transaction", 0),
"valuation_rate": last_entry.get("valuation_rate", 0),
"stock_value": last_entry.get("stock_value", 0),
}
def get_matching_warehouses(warehouses):
if not warehouses:
return []
if isinstance(warehouses, str):
warehouses = [warehouses]
warehouse_details = frappe.get_all(
"Warehouse",
filters={"name": ("in", warehouses)},
fields=["lft", "rgt"],
)
if not warehouse_details:
return warehouses
wh = frappe.qb.DocType("Warehouse")
cond = None
for d in warehouse_details:
c = (wh.lft >= d.lft) & (wh.rgt <= d.rgt)
cond = c if cond is None else (cond | c)
matching = (frappe.qb.from_(wh).select(wh.name).where(cond)).run(pluck=True)
return matching if matching else warehouses
return row
def get_warehouse_condition(warehouses):
@@ -859,15 +779,7 @@ def get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value):
if not filters.item_code or not filters.warehouse or not filters.from_date:
return
item_codes = filters.get("item_code")
if isinstance(item_codes, str):
item_codes = [item_codes]
warehouses = filters.get("warehouse")
if isinstance(warehouses, str):
warehouses = [warehouses]
if len(item_codes) > 1 or len(warehouses) > 1:
if len(filters.get("item_code")) > 1 or len(filters.get("warehouse")) > 1:
return
sl_doctype = frappe.qb.DocType("Stock Ledger Entry")
@@ -887,11 +799,17 @@ def get_opening_balance_for_inv_dimension(filters, inv_dimension_wise_value):
)
)
if item_codes:
query = query.where(sl_doctype.item_code.isin(item_codes))
if filters.get("item_code"):
if isinstance(filters.item_code, list | tuple):
query = query.where(sl_doctype.item_code.isin(filters.item_code))
else:
query = query.where(sl_doctype.item_code == filters.item_code)
if warehouses:
query = query.where(sl_doctype.warehouse.isin(warehouses))
if filters.get("warehouse"):
if isinstance(filters.warehouse, list | tuple):
query = query.where(sl_doctype.warehouse.isin(filters.warehouse))
else:
query = query.where(sl_doctype.warehouse == filters.warehouse)
for key, value in inv_dimension_wise_value.items():
if isinstance(value, list | tuple):

View File

@@ -5,335 +5,20 @@ import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.report.stock_ledger.stock_ledger import execute
WAREHOUSE = "Stores - _TC"
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
make_serial_item_with_serial,
)
class TestStockLedgerReport(FrappeTestCase):
"""Correctness tests for the Stock Ledger report.
A shared `make_movements`/`run` pair keeps each test small without persisting
any data: movements are created per test and rolled back, while the report runs
read-only. Tests reuse bootstrap items and transact in `Stores - _TC`, which
starts clean (zero balance) for these items.
"""
class TestStockLedgerReeport(FrappeTestCase):
def setUp(self) -> None:
make_serial_item_with_serial("_Test Stock Report Serial Item")
self.filters = frappe._dict(
company="_Test Company",
from_date=today(),
to_date=add_days(today(), 30),
item_code=["_Test Stock Report Serial Item"],
)
def tearDown(self) -> None:
frappe.db.rollback()
def make_movements(self, item_code, movements):
for movement in movements:
make_stock_entry(item_code=item_code, **movement)
def run_report(self, item_code, from_date=None, to_date=None):
filters = frappe._dict(
company="_Test Company",
from_date=from_date or add_days(today(), -1),
to_date=to_date or today(),
item_code=[item_code],
warehouse=WAREHOUSE,
)
return list(execute(filters)[1])
def test_in_out_quantities_and_running_balance(self):
item = "_Test Item"
self.make_movements(
item,
[
{"qty": 10, "to_warehouse": WAREHOUSE, "basic_rate": 100},
{"qty": 4, "from_warehouse": WAREHOUSE},
],
)
rows = self.run_report(item)
receipt = next(row for row in rows if row.get("in_qty"))
issue = next(row for row in rows if row.get("out_qty"))
self.assertEqual(receipt["in_qty"], 10)
self.assertEqual(receipt["qty_after_transaction"], 10)
self.assertEqual(issue["out_qty"], -4)
self.assertEqual(issue["qty_after_transaction"], 6)
def test_opening_balance_reflects_movements_before_from_date(self):
item = "_Test Item"
self.make_movements(
item,
[
{
"qty": 10,
"to_warehouse": WAREHOUSE,
"basic_rate": 100,
"posting_date": add_days(today(), -10),
},
{"qty": 4, "from_warehouse": WAREHOUSE, "posting_date": today()},
],
)
rows = self.run_report(item, from_date=add_days(today(), -5), to_date=today())
# the receipt predates the range, so it surfaces as the opening balance
self.assertEqual(rows[0]["item_code"], "'Opening'")
self.assertEqual(rows[0]["qty_after_transaction"], 10)
# the in-range issue draws down from the opening balance
issue = next(row for row in rows if row.get("out_qty"))
self.assertEqual(issue["qty_after_transaction"], 6)
def test_filters_to_requested_item_only(self):
item_a = "_Test Item"
item_b = "_Test Item 2"
self.make_movements(item_a, [{"qty": 5, "to_warehouse": WAREHOUSE, "basic_rate": 100}])
self.make_movements(item_b, [{"qty": 7, "to_warehouse": WAREHOUSE, "basic_rate": 100}])
rows = self.run_report(item_a)
item_codes = {row["item_code"] for row in rows if row.get("voucher_no")}
self.assertEqual(item_codes, {item_a})
def test_multi_item_opening_balance_with_and_without_transactions(self):
item_a = "_Test Item"
item_b = "_Test Item 2"
self.make_movements(
item_a,
[
{
"qty": 10,
"to_warehouse": WAREHOUSE,
"basic_rate": 100,
"posting_date": add_days(today(), -10),
}
],
)
self.make_movements(
item_b,
[{"qty": 5, "to_warehouse": WAREHOUSE, "basic_rate": 50, "posting_date": add_days(today(), -10)}],
)
self.make_movements(
item_a,
[{"qty": 2, "from_warehouse": WAREHOUSE, "posting_date": today()}],
)
filters = frappe._dict(
company="_Test Company",
from_date=add_days(today(), -5),
to_date=today(),
item_code=[item_a, item_b],
warehouse=WAREHOUSE,
)
columns, rows = execute(filters)
opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"]
self.assertEqual(len(opening_rows), 1)
self.assertEqual(opening_rows[0]["qty_after_transaction"], 15)
def test_multi_warehouse_opening_balance_aggregation(self):
item = "_Test Item"
warehouse_1 = "Stores - _TC"
warehouse_2 = "Finished Goods - _TC"
self.make_movements(
item,
[
{
"qty": 10,
"to_warehouse": warehouse_1,
"basic_rate": 100,
"posting_date": add_days(today(), -10),
},
{
"qty": 20,
"to_warehouse": warehouse_2,
"basic_rate": 100,
"posting_date": add_days(today(), -10),
},
],
)
filters = frappe._dict(
company="_Test Company",
from_date=add_days(today(), -5),
to_date=today(),
item_code=[item],
warehouse=[warehouse_1, warehouse_2],
)
columns, rows = execute(filters)
opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"]
self.assertEqual(len(opening_rows), 1)
self.assertEqual(opening_rows[0]["qty_after_transaction"], 30)
def test_opening_stock_reconciliation_on_from_date_non_midnight_time(self):
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
item = "_Test Item"
from_date = today()
sr = create_stock_reconciliation(
item_code=item,
warehouse=WAREHOUSE,
qty=25,
rate=100,
posting_date=from_date,
posting_time="10:30:00",
purpose="Opening Stock",
do_not_submit=False,
)
filters = frappe._dict(
company="_Test Company",
from_date=from_date,
to_date=from_date,
item_code=[item],
warehouse=WAREHOUSE,
)
columns, rows = execute(filters)
opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"]
self.assertEqual(len(opening_rows), 1)
self.assertEqual(opening_rows[0]["qty_after_transaction"], 25)
# Ensure the Opening Stock Reconciliation is not duplicated in detail transaction rows
reco_rows = [row for row in rows if row.get("voucher_no") == sr.name]
self.assertEqual(len(reco_rows), 0)
def test_backdated_sle_independent_maxima_handling(self):
item = "_Test Item"
# Entry 1: Later posting date (2026-07-20), created first
self.make_movements(
item,
[
{
"qty": 10,
"to_warehouse": WAREHOUSE,
"basic_rate": 100,
"posting_date": add_days(today(), -10),
}
],
)
# Entry 2: Backdated posting date (2026-07-15), created LATER
self.make_movements(
item,
[
{
"qty": 5,
"to_warehouse": WAREHOUSE,
"basic_rate": 100,
"posting_date": add_days(today(), -15),
}
],
)
filters = frappe._dict(
company="_Test Company",
from_date=add_days(today(), -5),
to_date=today(),
item_code=[item],
warehouse=WAREHOUSE,
)
columns, rows = execute(filters)
opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"]
self.assertEqual(len(opening_rows), 1)
# Should correctly pick the latest posting date entry (15 Qty) despite backdated creation order
self.assertEqual(opening_rows[0]["qty_after_transaction"], 15)
def test_filtered_opening_balance_does_not_pick_excluded_creation_entry(self):
item = "_Test Item"
posting_date = add_days(today(), -10)
posting_time = "09:00:00"
included_entry = make_stock_entry(
item_code=item,
qty=10,
to_warehouse=WAREHOUSE,
basic_rate=100,
posting_date=posting_date,
posting_time=posting_time,
)
make_stock_entry(
item_code=item,
qty=50,
to_warehouse=WAREHOUSE,
basic_rate=100,
posting_date=posting_date,
posting_time=posting_time,
)
filters = frappe._dict(
company="_Test Company",
from_date=add_days(today(), -5),
to_date=today(),
item_code=[item],
warehouse=WAREHOUSE,
voucher_no=included_entry.name,
)
columns, rows = execute(filters)
opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"]
self.assertEqual(len(opening_rows), 1)
self.assertEqual(opening_rows[0]["qty_after_transaction"], 10)
def test_tied_creation_terminal_sle_is_not_summed_twice(self):
item = "_Test Item"
posting_date = add_days(today(), -10)
posting_time = "09:00:00"
stock_entry_1 = make_stock_entry(
item_code=item,
qty=10,
to_warehouse=WAREHOUSE,
basic_rate=100,
posting_date=posting_date,
posting_time=posting_time,
)
stock_entry_2 = make_stock_entry(
item_code=item,
qty=5,
to_warehouse=WAREHOUSE,
basic_rate=100,
posting_date=posting_date,
posting_time=posting_time,
)
sle_rows = frappe.get_all(
"Stock Ledger Entry",
filters={
"voucher_type": "Stock Entry",
"voucher_no": ("in", [stock_entry_1.name, stock_entry_2.name]),
"item_code": item,
"warehouse": WAREHOUSE,
"is_cancelled": 0,
},
fields=["name", "qty_after_transaction"],
order_by="name desc",
)
self.assertEqual(len(sle_rows), 2)
for sle in sle_rows:
frappe.db.set_value(
"Stock Ledger Entry",
sle.name,
"creation",
"2026-01-01 00:00:00.000000",
update_modified=False,
)
filters = frappe._dict(
company="_Test Company",
from_date=add_days(today(), -5),
to_date=today(),
item_code=[item],
warehouse=WAREHOUSE,
)
columns, rows = execute(filters)
opening_rows = [row for row in rows if row.get("item_code") == "'Opening'"]
self.assertEqual(len(opening_rows), 1)
self.assertEqual(opening_rows[0]["qty_after_transaction"], sle_rows[0].qty_after_transaction)
self.assertNotEqual(
opening_rows[0]["qty_after_transaction"],
sum(sle.qty_after_transaction for sle in sle_rows),
)

View File

@@ -910,11 +910,6 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
self.batchwise_valuation_batches = []
self.non_batchwise_valuation_batches = []
if batchwise_batches := self.sle.get("batchwise_valuation_batches"):
self.batchwise_valuation_batches = list(batchwise_batches)
self.non_batchwise_valuation_batches = list(set(self.batches) - set(batchwise_batches))
return
if get_valuation_method(self.sle.item_code) == "Moving Average" and frappe.db.get_single_value(
"Stock Settings", "do_not_use_batchwise_valuation"
):