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
26 changed files with 153 additions and 1067 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

@@ -798,18 +798,6 @@ class JournalEntry(AccountsController):
)
)
if reference_type == "Purchase Invoice" and invoice.invoice_is_blocked():
msg = (
_("{0} {1} is blocked and on hold until {2}.").format(
invoice.doctype, invoice.name, invoice.release_date
)
if invoice.release_date
else _("{0} {1} is blocked.").format(
invoice.doctype, invoice.name, invoice.release_date
)
)
frappe.throw(msg)
def set_against_account(self):
accounts_debited, accounts_credited = [], []
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):

View File

@@ -6,7 +6,7 @@ import unittest
import frappe
from frappe.tests.utils import change_settings
from frappe.utils import add_days, flt, nowdate
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
@@ -602,69 +602,6 @@ class TestJournalEntry(unittest.TestCase):
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def make_jv_against_purchase_invoice(self, invoice, amount=100):
jv = make_journal_entry("Creditors - _TC", "_Test Cash - _TC", amount, save=False)
jv.accounts[0].party_type = "Supplier"
jv.accounts[0].party = invoice.supplier
jv.accounts[0].reference_type = "Purchase Invoice"
jv.accounts[0].reference_name = invoice.name
return jv
def test_jv_against_purchase_invoice_respects_hold_state(self):
"""Payment can be booked against a Purchase Invoice only while it is not on hold."""
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
release_date = add_days(nowdate(), 10)
def never_held():
return make_purchase_invoice()
def held_until_a_future_date():
invoice = make_purchase_invoice()
invoice.block_invoice(hold_comment="Waiting for the goods", release_date=release_date)
return invoice
def held_without_a_release_date():
invoice = make_purchase_invoice()
invoice.block_invoice(hold_comment="Under dispute")
return invoice
def held_until_a_date_that_has_passed():
invoice = held_until_a_future_date()
frappe.db.set_value("Purchase Invoice", invoice.name, "release_date", add_days(nowdate(), -1))
return invoice
def unblocked_again():
invoice = held_until_a_future_date()
invoice.unblock_invoice()
return invoice
for build_invoice in (held_until_a_future_date, held_without_a_release_date):
with self.subTest(build_invoice.__name__):
jv = self.make_jv_against_purchase_invoice(build_invoice())
self.assertRaisesRegex(frappe.ValidationError, "is blocked", jv.insert)
for build_invoice in (never_held, held_until_a_date_that_has_passed, unblocked_again):
with self.subTest(build_invoice.__name__):
invoice = build_invoice()
jv = self.make_jv_against_purchase_invoice(invoice)
jv.insert()
self.assertEqual(jv.reference_types[invoice.name], "Purchase Invoice")
def test_jv_against_blocked_sales_invoice_reference_is_not_checked(self):
"""A Sales Invoice has no hold state, so the check must skip it rather than fail."""
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
invoice = create_sales_invoice(rate=500)
jv = make_journal_entry("_Test Cash - _TC", "Debtors - _TC", 100, save=False)
jv.accounts[1].party_type = "Customer"
jv.accounts[1].party = "_Test Customer"
jv.accounts[1].reference_type = "Sales Invoice"
jv.accounts[1].reference_name = invoice.name
jv.insert()
self.assertEqual(jv.reference_types[invoice.name], "Sales Invoice")
def make_journal_entry(
account1,

View File

@@ -237,8 +237,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
unblock_invoice() {
const me = this;
me.frm.call("unblock_invoice", null, () => {
me.frm.reload_doc();
frappe.call({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.unblock_invoice",
args: { name: me.frm.doc.name },
callback: (r) => me.frm.reload_doc(),
});
}
@@ -289,16 +291,15 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.dialog.set_primary_action(__("Save"), function () {
const dialog_data = me.dialog.get_values();
me.frm.call(
"block_invoice",
{
frappe.call({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.block_invoice",
args: {
name: me.frm.doc.name,
hold_comment: dialog_data.hold_comment,
release_date: dialog_data.release_date,
},
() => {
me.frm.reload_doc();
}
);
callback: (r) => me.frm.reload_doc(),
});
me.dialog.hide();
});
@@ -337,9 +338,10 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
set_release_date(data) {
const me = this;
return me.frm.call("change_release_date", { release_date: data.release_date }, () => {
me.frm.reload_doc();
return frappe.call({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.change_release_date",
args: data,
callback: (r) => this.frm.reload_doc(),
});
}

View File

@@ -352,7 +352,6 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.on_hold",
"depends_on": "eval:doc.on_hold",
"fieldname": "sb_14",
"fieldtype": "Section Break",
"label": "Hold Invoice"
@@ -1663,7 +1662,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-08-05 15:40:16.519774",
"modified": "2026-07-12 23:54:21.263951",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _, qb, throw
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import DateTimeLikeObject, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
@@ -299,9 +299,6 @@ class PurchaseInvoice(BuyingController):
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
self.set_percentage_received()
if self.on_hold:
self.validate_invoice_hold()
def set_percentage_received(self):
total_billed_qty = 0.0
total_received_qty = 0.0
@@ -313,13 +310,6 @@ class PurchaseInvoice(BuyingController):
if total_billed_qty and total_received_qty:
self.per_received = total_received_qty / total_billed_qty * 100
def validate_invoice_hold(self):
if self.is_return:
frappe.throw(_("Return Purchase Invoice cannot be held."))
if self.docstatus < 1:
frappe.throw(_("Purchase Invoice can be held after submitting."))
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
frappe.throw(_("Release date must be in the future"))
@@ -1865,38 +1855,14 @@ class PurchaseInvoice(BuyingController):
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
@frappe.whitelist(methods=["POST"])
def block_invoice(self, hold_comment: str | None = None, release_date: DateTimeLikeObject | None = None):
self.check_permission("write")
self.on_hold = 1
self.release_date = release_date
self.validate_block_invoice()
self.db_set({"on_hold": 1, "hold_comment": cstr(hold_comment), "release_date": release_date})
@frappe.whitelist(methods=["POST"])
def unblock_invoice(self):
self.check_permission("write")
self.db_set({"on_hold": 0, "release_date": None})
@frappe.whitelist(methods=["POST"])
def change_release_date(self, release_date: DateTimeLikeObject | None = None):
self.check_permission("write")
if not self.on_hold:
frappe.throw(_("Invoice is not blocked. Block the invoice to change the release date."))
self.release_date = release_date
self.validate_block_invoice()
def block_invoice(self, hold_comment=None, release_date=None):
self.db_set("on_hold", 1)
self.db_set("hold_comment", cstr(hold_comment))
self.db_set("release_date", release_date)
def validate_block_invoice(self):
self.validate_invoice_hold()
if self.outstanding_amount <= 0:
frappe.throw(_("Purchase Invoice without any outstanding amount cannot be held."))
self.validate_release_date()
def unblock_invoice(self):
self.db_set("on_hold", 0)
self.db_set("release_date", None)
def set_tax_withholding(self):
self.set("advance_tax", [])
@@ -2116,6 +2082,28 @@ def make_stock_entry(source_name, target_doc=None):
return doc
@frappe.whitelist()
def change_release_date(name, release_date=None):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_doc("Purchase Invoice", name)
pi.check_permission()
pi.db_set("release_date", release_date)
@frappe.whitelist()
def unblock_invoice(name):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_doc("Purchase Invoice", name)
pi.unblock_invoice()
@frappe.whitelist()
def block_invoice(name, release_date, hold_comment=None):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_doc("Purchase Invoice", name)
pi.block_invoice(hold_comment, release_date)
@frappe.whitelist()
def make_inter_company_sales_invoice(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction

View File

@@ -287,166 +287,14 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def test_purchase_invoice_explicit_block(self):
pi = make_purchase_invoice()
release_date = add_days(nowdate(), 10)
pi.block_invoice(hold_comment="Waiting for the goods", release_date=release_date)
pi.block_invoice()
self.assertEqual(pi.on_hold, 1)
on_hold, hold_comment, saved_release_date = frappe.db.get_value(
"Purchase Invoice", pi.name, ["on_hold", "hold_comment", "release_date"]
)
self.assertEqual(on_hold, 1)
self.assertEqual(hold_comment, "Waiting for the goods")
self.assertEqual(getdate(saved_release_date), getdate(release_date))
pi.unblock_invoice()
self.assertEqual(pi.on_hold, 0)
on_hold, saved_release_date = frappe.db.get_value(
"Purchase Invoice", pi.name, ["on_hold", "release_date"]
)
self.assertEqual(on_hold, 0)
self.assertIsNone(saved_release_date)
def test_purchase_invoice_cannot_be_held_before_submission(self):
pi = make_purchase_invoice(do_not_save=True)
pi.on_hold = 1
self.assertRaises(frappe.ValidationError, pi.save)
pi.on_hold = 0
pi.save()
pi.submit()
pi.block_invoice()
self.assertEqual(frappe.db.get_value("Purchase Invoice", pi.name, "on_hold"), 1)
def test_return_purchase_invoice_cannot_be_held(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
pi = make_purchase_invoice()
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.on_hold = 1
self.assertRaisesRegex(frappe.ValidationError, "cannot be held", return_pi.save)
return_pi.on_hold = 0
return_pi.save()
return_pi.submit()
self.assertRaisesRegex(frappe.ValidationError, "cannot be held", return_pi.block_invoice)
def test_return_purchase_invoice_is_not_affected_by_hold_validations(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
pi = make_purchase_invoice()
# a return has a negative outstanding amount, which must not be mistaken
# for an invalid hold on a document that was never held
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.save()
return_pi.submit()
self.assertEqual(return_pi.docstatus, 1)
self.assertEqual(return_pi.on_hold, 0)
self.assertLess(return_pi.outstanding_amount, 0)
def test_settled_purchase_invoice_cannot_be_held(self):
pi = make_purchase_invoice()
pe = get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.save()
pe.submit()
pi.reload()
self.assertEqual(pi.outstanding_amount, 0)
self.assertRaises(frappe.ValidationError, pi.block_invoice)
self.assertEqual(frappe.db.get_value("Purchase Invoice", pi.name, "on_hold"), 0)
def test_release_date_of_held_invoice_must_be_in_future(self):
pi = make_purchase_invoice()
self.assertRaises(frappe.ValidationError, pi.block_invoice, "Hold", add_days(nowdate(), -1))
self.assertRaises(frappe.ValidationError, pi.block_invoice, "Hold", nowdate())
def test_rejected_hold_does_not_partially_update_invoice(self):
pi = make_purchase_invoice()
self.assertRaises(frappe.ValidationError, pi.block_invoice, "Hold", add_days(nowdate(), -1))
pi.reload()
self.assertEqual(pi.on_hold, 0)
self.assertIsNone(pi.release_date)
def test_change_release_date_of_held_invoice(self):
pi = make_purchase_invoice()
pi.block_invoice(hold_comment="Hold", release_date=add_days(nowdate(), 10))
new_release_date = add_days(nowdate(), 20)
pi.change_release_date(new_release_date)
self.assertEqual(
getdate(frappe.db.get_value("Purchase Invoice", pi.name, "release_date")),
getdate(new_release_date),
)
self.assertRaises(frappe.ValidationError, pi.change_release_date, add_days(nowdate(), -1))
def test_release_date_cannot_be_changed_on_an_invoice_that_is_not_held(self):
pi = make_purchase_invoice()
self.assertRaisesRegex(
frappe.ValidationError,
"Invoice is not blocked",
pi.change_release_date,
add_days(nowdate(), 10),
)
self.assertIsNone(frappe.db.get_value("Purchase Invoice", pi.name, "release_date"))
def test_hold_methods_are_whitelisted_document_methods(self):
import erpnext.accounts.doctype.purchase_invoice.purchase_invoice as purchase_invoice_module
pi = frappe.new_doc("Purchase Invoice")
for method in ("block_invoice", "unblock_invoice", "change_release_date"):
# raises if the method is not whitelisted for client side calls
pi.is_whitelisted(method)
self.assertFalse(
hasattr(purchase_invoice_module, method),
f"{method} should only be exposed as a document method",
)
def test_hold_methods_require_write_permission(self):
pi = make_purchase_invoice()
user = "test_pi_hold_permission@example.com"
if not frappe.db.exists("User", user):
frappe.get_doc(
{
"doctype": "User",
"email": user,
"first_name": "Test PI Hold",
"roles": [{"role": "Employee"}],
}
).insert(ignore_permissions=True)
frappe.set_user(user)
try:
self.assertRaises(frappe.PermissionError, pi.block_invoice)
self.assertRaises(frappe.PermissionError, pi.unblock_invoice)
self.assertRaises(frappe.PermissionError, pi.change_release_date, add_days(nowdate(), 10))
finally:
frappe.set_user("Administrator")
self.assertEqual(frappe.db.get_value("Purchase Invoice", pi.name, "on_hold"), 0)
def test_gl_entries_with_perpetual_inventory_against_pr(self):
pr = make_purchase_receipt(
company="_Test Company with perpetual inventory",

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

@@ -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"
):