Compare commits

..

31 Commits

Author SHA1 Message Date
Frappe PR Bot
12cd563fb9 chore(release): Bumped to Version 16.35.0
# [16.35.0](https://github.com/frappe/erpnext/compare/v16.34.2...v16.35.0) (2026-09-15)

### Bug Fixes

* % Amount Billed exceeds 100% on fully rejected Purchase Receipts ([#58897](https://github.com/frappe/erpnext/issues/58897)) ([79c8e4d](79c8e4db7f))
* **accounts:** add permission checks on `execute_repost_payment_ledger` (backport [#58980](https://github.com/frappe/erpnext/issues/58980)) ([#58982](https://github.com/frappe/erpnext/issues/58982)) ([9bc98ef](9bc98efe2f))
* **accounts:** add permission checks on `invoice_discounting.get_invoices` (backport [#58975](https://github.com/frappe/erpnext/issues/58975)) ([#58990](https://github.com/frappe/erpnext/issues/58990)) ([a9f3d69](a9f3d69fbe))
* **accounts:** reject disabled price lists in buying and selling ([#58891](https://github.com/frappe/erpnext/issues/58891)) ([5bd623c](5bd623c4dd))
* **accounts:** stop counting received qty on non-stock invoice returns (backport [#58924](https://github.com/frappe/erpnext/issues/58924)) ([#58929](https://github.com/frappe/erpnext/issues/58929)) ([3856638](3856638d01))
* **asset:** recompute reference doc reqd on asset type change ([6d447f6](6d447f6847))
* **assets:** build Finance Books even when Calculate Depreciation is checked before Net Purchase Amount is entered ([1652f6e](1652f6ea42))
* **assets:** discard stale set_finance_book responses ([dfde7d8](dfde7d811c))
* avoid TypeError on new Work Order when company is not set ([#58969](https://github.com/frappe/erpnext/issues/58969)) ([e20501a](e20501a15c))
* calculate holiday list totals for half days ([#58792](https://github.com/frappe/erpnext/issues/58792)) ([3d6afff](3d6afffcef))
* coerce cost_allocation_per to float in BOM cost allocation ([1918ed6](1918ed6a3c))
* keep a dunning claimable until its interest is paid too ([7723f5a](7723f5aaed))
* keep each row's own rate under maintain same rate ([855d038](855d0385da))
* **manufacturing:** handle empty bom cost allocation ([#58939](https://github.com/frappe/erpnext/issues/58939)) ([77d8d5b](77d8d5b3e2))
* **manufacturing:** handle empty raw materials in workstation ([#58927](https://github.com/frappe/erpnext/issues/58927)) ([3b787c9](3b787c9443))
* **mrp:** align monthly bucket labels with demand dates ([#58963](https://github.com/frappe/erpnext/issues/58963)) ([c5773e7](c5773e7899))
* prevent disabled items from being used in bom ([#58997](https://github.com/frappe/erpnext/issues/58997)) ([f1a7fd0](f1a7fd006d))
* read posting date confirmation setting from boot data ([1349e0b](1349e0b3ab))
* refresh posting date confirmation setting ([a404d9b](a404d9b4e8))
* reset price list fallback when changing parties ([#58893](https://github.com/frappe/erpnext/issues/58893)) ([6751264](6751264ba5))
* resolve conflicts ([2add72b](2add72ba36))
* **selling:** add email permission check on `send_emails` (backport [#58935](https://github.com/frappe/erpnext/issues/58935)) ([#58938](https://github.com/frappe/erpnext/issues/58938)) ([a79abc0](a79abc0462))
* **selling:** exclude fully billed orders from the invoice picker (backport [#58966](https://github.com/frappe/erpnext/issues/58966)) ([#58967](https://github.com/frappe/erpnext/issues/58967)) ([5d9cbd9](5d9cbd9755))
* **selling:** list billable sales orders oldest first (backport [#59010](https://github.com/frappe/erpnext/issues/59010)) ([#59011](https://github.com/frappe/erpnext/issues/59011)) ([8672a6e](8672a6e604))
* **stock:** distribute additional costs when incoming items have no value ([b603581](b6035810f0))
* **stock:** do not rebuild the Warehouse tree on an account map miss (backport [#58956](https://github.com/frappe/erpnext/issues/58956)) ([#58973](https://github.com/frappe/erpnext/issues/58973)) ([6fb76eb](6fb76eba71))
* **stock:** guard inventory dimension lookup without reference document ([#58889](https://github.com/frappe/erpnext/issues/58889)) ([b21ca36](b21ca36428))
* **stock:** ignore disabled price lists on transactions (backport [#58926](https://github.com/frappe/erpnext/issues/58926)) ([#58931](https://github.com/frappe/erpnext/issues/58931)) ([53e5339](53e53390f4))
* **stock:** remove empty delivery stops after mapping ([#58896](https://github.com/frappe/erpnext/issues/58896)) ([ca33019](ca33019fdf))
* **stock:** set item_name to fetch if empty in packing slip item ([#58925](https://github.com/frappe/erpnext/issues/58925)) ([9e5d87a](9e5d87ac4a))
* **stock:** show actual outgoing rate in ledger preview ([#58700](https://github.com/frappe/erpnext/issues/58700)) ([2932793](2932793af0))
* **stock:** use correct field when reading previous stock closing balance (backport [#55898](https://github.com/frappe/erpnext/issues/55898)) ([#58854](https://github.com/frappe/erpnext/issues/58854)) ([70f391d](70f391d5e7)), closes [#54819](https://github.com/frappe/erpnext/issues/54819)
* **stock:** value stock entry rows as of the posting date (backport [#59042](https://github.com/frappe/erpnext/issues/59042)) ([#59048](https://github.com/frappe/erpnext/issues/59048)) ([7db6303](7db6303ac2))
* **subcontracting:** cannot select Against Finished Good on customer receipt (backport [#58949](https://github.com/frappe/erpnext/issues/58949)) ([#58951](https://github.com/frappe/erpnext/issues/58951)) ([df62766](df62766157))
* **subcontracting:** validate project across the subcontracting flow ([#58965](https://github.com/frappe/erpnext/issues/58965)) ([0095560](0095560dc7))
* sync translations from crowdin (version-16-hotfix) ([#59052](https://github.com/frappe/erpnext/issues/59052)) ([442fcd3](442fcd33e9))
* validate shelf life for automatically created expiring batches ([#58911](https://github.com/frappe/erpnext/issues/58911)) ([fa504b8](fa504b87db))

### Features

* **stock:** multi select item and warehouse filters in warehouse wise item balance ([bf33bc1](bf33bc17c0))
2026-09-15 18:46:11 +00:00
Diptanil Saha
6441ecd784 Merge pull request #59073 from frappe/version-16-hotfix
chore: release v16
2026-09-16 00:14:39 +05:30
Frappe PR Bot
4048fb70e1 chore(release): Bumped to Version 16.34.2
## [16.34.2](https://github.com/frappe/erpnext/compare/v16.34.1...v16.34.2) (2026-09-08)

### Bug Fixes

* **accounts:** enforce account field allow-list on financial report filters (backport [#58790](https://github.com/frappe/erpnext/issues/58790)) ([#58849](https://github.com/frappe/erpnext/issues/58849)) ([6e39b42](6e39b42137))
* **accounts:** reject same-account internal transfers (backport [#58529](https://github.com/frappe/erpnext/issues/58529)) ([#58877](https://github.com/frappe/erpnext/issues/58877)) ([6ec3035](6ec30350d2))
* **accounts:** round journal entry totals to field precision (backport [#58629](https://github.com/frappe/erpnext/issues/58629)) ([#58875](https://github.com/frappe/erpnext/issues/58875)) ([9fff01d](9fff01db32))
* add reconciliation after submit logic for bank transactions ([#57330](https://github.com/frappe/erpnext/issues/57330)) ([0a60d68](0a60d6805f))
* **bank reconciliation:** match Payment Entries on the bank-side amount (backport [#57740](https://github.com/frappe/erpnext/issues/57740)) ([#58765](https://github.com/frappe/erpnext/issues/58765)) ([189bd1f](189bd1f39d))
* **banking:** Federal bank dark logo (backport [#58844](https://github.com/frappe/erpnext/issues/58844)) ([#58845](https://github.com/frappe/erpnext/issues/58845)) ([3cc73e4](3cc73e4282))
* **banking:** find transfers on the same day (backport [#58766](https://github.com/frappe/erpnext/issues/58766)) ([#58767](https://github.com/frappe/erpnext/issues/58767)) ([8229aea](8229aeaead))
* **banking:** UI cleanup and better statement parsing (backport [#58817](https://github.com/frappe/erpnext/issues/58817)) ([#58824](https://github.com/frappe/erpnext/issues/58824)) ([0610708](0610708d78))
* **batch:** show Expired status only after expiry date has passed ([#58736](https://github.com/frappe/erpnext/issues/58736)) ([0684599](0684599bdb))
* check material request price list permission ([#58740](https://github.com/frappe/erpnext/issues/58740)) ([e5b1ff6](e5b1ff667d))
* check write permission in whitelisted document methods (backport [#58689](https://github.com/frappe/erpnext/issues/58689)) ([#58701](https://github.com/frappe/erpnext/issues/58701)) ([074f9f0](074f9f0828))
* **crm:** check read permission on lead in add_lead_to_prospect ([2cc1a51](2cc1a51d9a))
* fetch accounts by company (backport [#58523](https://github.com/frappe/erpnext/issues/58523)) ([#58879](https://github.com/frappe/erpnext/issues/58879)) ([75cb796](75cb796477))
* **gross-profit:** include item_name in export (backport [#58631](https://github.com/frappe/erpnext/issues/58631)) ([#58864](https://github.com/frappe/erpnext/issues/58864)) ([59b80b9](59b80b903a))
* **journal-entry:** avoid full grid re-render per row in set_exchange_rate (backport [#58328](https://github.com/frappe/erpnext/issues/58328)) ([#58803](https://github.com/frappe/erpnext/issues/58803)) ([8b51005](8b51005525))
* **manufacturing:** account for process loss in Production Plan Work Orders (backport [#58799](https://github.com/frappe/erpnext/issues/58799)) ([#58838](https://github.com/frappe/erpnext/issues/58838)) ([cfdf976](cfdf97601a))
* **manufacturing:** apply MOQ once across Production Plan rows (v16) ([#58831](https://github.com/frappe/erpnext/issues/58831)) ([f6dbb31](f6dbb3131d))
* **manufacturing:** apply safety stock once across Production Plan rows (v16) ([#58832](https://github.com/frappe/erpnext/issues/58832)) ([4719ad9](4719ad9b91))
* **manufacturing:** share transfer stock across Production Plan rows (v16) ([#58834](https://github.com/frappe/erpnext/issues/58834)) ([e59fb39](e59fb396e5))
* **manufacturing:** sum consolidated sub-assembly required quantity (v16) ([#58833](https://github.com/frappe/erpnext/issues/58833)) ([a7c5ab8](a7c5ab89e8))
* minor improvements to financial report template validation ([#58724](https://github.com/frappe/erpnext/issues/58724)) ([d23b407](d23b407ec7))
* order smallest purchase UOM qty that meets min order qty (backport [#57883](https://github.com/frappe/erpnext/issues/57883)) ([#58813](https://github.com/frappe/erpnext/issues/58813)) ([28f0ac5](28f0ac5287))
* persist rendered terms on transaction save ([#58858](https://github.com/frappe/erpnext/issues/58858)) ([89872eb](89872ebb37))
* **pos:** use company-currency change amount when netting pos gl entries (backport [#58599](https://github.com/frappe/erpnext/issues/58599)) ([#58678](https://github.com/frappe/erpnext/issues/58678)) ([06057cc](06057ccfc0))
* prevent duplicate Batch messages and Project links ([#58705](https://github.com/frappe/erpnext/issues/58705)) ([2dc2a04](2dc2a04522))
* resolve code lists by URI and version (backport [#58770](https://github.com/frappe/erpnext/issues/58770)) ([#58772](https://github.com/frappe/erpnext/issues/58772)) ([31319bd](31319bd36e))
* resolve conflicts ([d4b52c2](d4b52c236a))
* restore hover tooltip on Profit and Loss dashboard chart ([bb26f8f](bb26f8f7b9))
* **selling:** fetch orders within billing allowance (backport [#58751](https://github.com/frappe/erpnext/issues/58751)) ([#58820](https://github.com/frappe/erpnext/issues/58820)) ([817926c](817926ca2e))
* **setup:** strict permissions for transaction deletion record (backport [#58687](https://github.com/frappe/erpnext/issues/58687)) ([#58723](https://github.com/frappe/erpnext/issues/58723)) ([dfb64d7](dfb64d7635))
* show cost center for unmatched payments in receivables (backport [#58453](https://github.com/frappe/erpnext/issues/58453)) ([#58881](https://github.com/frappe/erpnext/issues/58881)) ([51bf7cb](51bf7cb241))
* **stock:** allow creating stock closing balances (backport [#58590](https://github.com/frappe/erpnext/issues/58590)) ([#58685](https://github.com/frappe/erpnext/issues/58685)) ([a1c8dc8](a1c8dc878d))
* **stock:** carry accounting dimensions from landed cost voucher charges into gl entries ([0c7be31](0c7be311b8))
* **stock:** recalculate delivery note billing after return ([#58869](https://github.com/frappe/erpnext/issues/58869)) ([7bb19c8](7bb19c89b1))
* **subcontracting:** add condition to exclude tax withholding accounts in purchase receipt mapping ([15a531f](15a531f03f))
* sync translations from crowdin (version-16-hotfix) ([#58580](https://github.com/frappe/erpnext/issues/58580)) ([6971c80](6971c80743))
* **timesheet:** handle empty allowed projects (backport [#58745](https://github.com/frappe/erpnext/issues/58745)) ([#58746](https://github.com/frappe/erpnext/issues/58746)) ([f75601e](f75601e9b1))
* use account currency in Bank Reconciliation Statement print (backport [#58692](https://github.com/frappe/erpnext/issues/58692)) ([#58871](https://github.com/frappe/erpnext/issues/58871)) ([b1bf95d](b1bf95d5b3))
* user not able to set valuation rate zero in stock reco (backport [#58800](https://github.com/frappe/erpnext/issues/58800)) ([#58823](https://github.com/frappe/erpnext/issues/58823)) ([fb13222](fb132225d7))

### Reverts

* **manufacturing:** remove material coverage changes on version 16 ([#58716](https://github.com/frappe/erpnext/issues/58716)) ([82392fa](82392fa747))
2026-09-08 13:23:31 +00:00
Diptanil Saha
cc57a7cf55 Merge pull request #58868 from frappe/version-16-hotfix
chore: release v16
2026-09-08 18:50:53 +05:30
Frappe PR Bot
0b50853985 chore(release): Bumped to Version 16.34.1
## [16.34.1](https://github.com/frappe/erpnext/compare/v16.34.0...v16.34.1) (2026-09-02)

### Bug Fixes

* filter cancelled BOMs in BOM Stock Analysis (backport [#58647](https://github.com/frappe/erpnext/issues/58647)) ([#58693](https://github.com/frappe/erpnext/issues/58693)) ([d6f9dde](d6f9dde3cb))
* improve message formatting and translation for validation issues ([#58425](https://github.com/frappe/erpnext/issues/58425)) ([a43de7c](a43de7ce95))
* include payment deductions in sales/purchase register ledger bal… (backport [#58437](https://github.com/frappe/erpnext/issues/58437)) ([#58680](https://github.com/frappe/erpnext/issues/58680)) ([4137401](41374019ba))
2026-09-02 12:24:04 +00:00
Sagar Vora
c19ddf187e Merge pull request #58707 from frappe/version-16-hotfix
chore: release v16
2026-09-02 17:51:22 +05:30
Frappe PR Bot
80c2c9e0e0 chore(release): Bumped to Version 16.34.0
# [16.34.0](https://github.com/frappe/erpnext/compare/v16.33.0...v16.34.0) (2026-09-02)

### Bug Fixes

* **accounts:** add permission checks on payment entry whitelisted methods ([#58644](https://github.com/frappe/erpnext/issues/58644)) ([c745558](c745558168))
* **accounts:** added permission checks on `get_available_payment_schedules` (backport [#58588](https://github.com/frappe/erpnext/issues/58588)) ([#58595](https://github.com/frappe/erpnext/issues/58595)) ([765a773](765a7736ed))
* **accounts:** guard get_gl_dict for v16 and drop unused base_gl_composer ([abf311e](abf311e8ca))
* **accounts:** prevent child table doctypes as accounting dimensions ([6dbaa50](6dbaa5048e))
* **accounts:** resolve subscription plans for any reference doctype in Payment Request (backport [#58438](https://github.com/frappe/erpnext/issues/58438)) ([#58450](https://github.com/frappe/erpnext/issues/58450)) ([3c63fff](3c63fffcb6))
* **accounts:** set pos profile on invoices respecting user permissions (backport [#58508](https://github.com/frappe/erpnext/issues/58508)) ([#58519](https://github.com/frappe/erpnext/issues/58519)) ([23290a6](23290a60da))
* add missing perm checks on various whitelisted methods (backport [#58652](https://github.com/frappe/erpnext/issues/58652)) ([#58654](https://github.com/frappe/erpnext/issues/58654)) ([86e9480](86e9480362))
* add type hints to make_request_for_quotation ([4435f16](4435f166ea))
* clarify duplicate internal party messages ([#58469](https://github.com/frappe/erpnext/issues/58469)) ([f0e603a](f0e603aec5))
* **company:** throw if linked to demo_company field ([9f3dd7e](9f3dd7eabd))
* correct is_frozen description on customer and supplier ([#58168](https://github.com/frappe/erpnext/issues/58168)) ([9e00888](9e00888d67))
* **crm:** add missing permission validation on `get_contract_template` (backport [#58621](https://github.com/frappe/erpnext/issues/58621)) ([#58623](https://github.com/frappe/erpnext/issues/58623)) ([30f0cd9](30f0cd9c95))
* **crm:** check write permission in edit_note ([c2261f6](c2261f6129))
* **crm:** validate contact email before saving an email campaign (backport [#58667](https://github.com/frappe/erpnext/issues/58667)) ([#58672](https://github.com/frappe/erpnext/issues/58672)) ([f22f84a](f22f84a58a))
* do not map the same row twice in "Get Items From" (backport [#58617](https://github.com/frappe/erpnext/issues/58617)) ([#58619](https://github.com/frappe/erpnext/issues/58619)) ([f3379b4](f3379b4694))
* explain missing fields when loading party details or taxes (backport [#58627](https://github.com/frappe/erpnext/issues/58627)) ([#58628](https://github.com/frappe/erpnext/issues/58628)) ([8a8bafd](8a8bafd6cb))
* filter fully ordered items when creating RFQ from Material Request ([9b1d907](9b1d907b6a))
* handle BOM price list currency update (backport [#58638](https://github.com/frappe/erpnext/issues/58638)) ([#58639](https://github.com/frappe/erpnext/issues/58639)) ([fb74a03](fb74a030de))
* **manufacturing:** account for pending job card qty ([#58466](https://github.com/frappe/erpnext/issues/58466)) ([7b9cdb4](7b9cdb4926))
* **manufacturing:** classify MRP items without a BOM as Purchase (backport [#58509](https://github.com/frappe/erpnext/issues/58509)) ([#58513](https://github.com/frappe/erpnext/issues/58513)) ([ee3b224](ee3b22403d))
* **manufacturing:** handle duplicate root BOM items (backport [#58614](https://github.com/frappe/erpnext/issues/58614)) ([#58616](https://github.com/frappe/erpnext/issues/58616)) ([22755c4](22755c4ad6))
* **manufacturing:** preserve job card qty in mr transfer (backport [#58482](https://github.com/frappe/erpnext/issues/58482)) ([#58499](https://github.com/frappe/erpnext/issues/58499)) ([76d0cb3](76d0cb382d))
* **manufacturing:** use item warehouses in production plan work orders ([#58663](https://github.com/frappe/erpnext/issues/58663)) ([ec392a7](ec392a7bcf))
* **manufacturing:** use packed row delivery date ([#58570](https://github.com/frappe/erpnext/issues/58570)) ([e64fddf](e64fddfad5))
* persist redistributed additional costs during stock entry repost (backport [#58433](https://github.com/frappe/erpnext/issues/58433)) ([#58533](https://github.com/frappe/erpnext/issues/58533)) ([629bc20](629bc20e90))
* **pos:** add permission checks on `get_invoices` (backport [#58591](https://github.com/frappe/erpnext/issues/58591)) ([#58597](https://github.com/frappe/erpnext/issues/58597)) ([d95eeb0](d95eeb0f6a))
* **projects:** check read permission on source in create_duplicate_project ([bd35957](bd35957582))
* **projects:** ignore cancelled invoices in timesheet portal (backport [#58501](https://github.com/frappe/erpnext/issues/58501)) ([#58504](https://github.com/frappe/erpnext/issues/58504)) ([d732d06](d732d06e25))
* round party balances to currency precision in Trial Balance for Party ([956a6ed](956a6ed407))
* **selling:** check quotation write permission before marking lost ([#58493](https://github.com/frappe/erpnext/issues/58493)) ([4e74831](4e74831a08))
* **selling:** check sales order permission before work order creation (backport [#58492](https://github.com/frappe/erpnext/issues/58492)) ([#58497](https://github.com/frappe/erpnext/issues/58497)) ([2b8e12e](2b8e12ef44))
* **sms_settings:** add patch to pre-fill roles into SMS Settings Roles Table ([4b89ddf](4b89ddffde))
* **stock:** auto-select batch no before saving transaction records (backport [#58536](https://github.com/frappe/erpnext/issues/58536)) ([#58539](https://github.com/frappe/erpnext/issues/58539)) ([eb4c327](eb4c327a2b))
* **stock:** ignore cancelled batch entries in valuation (backport [#58659](https://github.com/frappe/erpnext/issues/58659)) ([#58660](https://github.com/frappe/erpnext/issues/58660)) ([f4ed4ca](f4ed4cae64))
* **stock:** keep pick list links when refetching stock entry items ([01e0844](01e0844d40))
* **stock:** load available serial no report (backport [#58558](https://github.com/frappe/erpnext/issues/58558)) ([#58562](https://github.com/frappe/erpnext/issues/58562)) ([06758f2](06758f2792))
* **stock:** preserve exchange gain loss journals in lcv ([#58505](https://github.com/frappe/erpnext/issues/58505)) ([1242316](124231681a))
* **stock:** validate serial inventory dimensions ([#58394](https://github.com/frappe/erpnext/issues/58394)) ([a1aabaf](a1aabaff18))
* sum item and its alternate transferred qty on work order required items ([1411835](1411835507))
* sync translations from crowdin (version-16-hotfix) ([#58455](https://github.com/frappe/erpnext/issues/58455)) ([c924de3](c924de3c55))
* **taxes:** skip tax addition for invoice created from opening invoice tool ([bfbc13d](bfbc13d147))
* **timesheet:** scoping whitelisted methods output to projects and timesheets that are acccessible to users (backport [#58267](https://github.com/frappe/erpnext/issues/58267)) ([#58577](https://github.com/frappe/erpnext/issues/58577)) ([4f409e5](4f409e5ccf))
* translate doctype in map msg (backport [#58515](https://github.com/frappe/erpnext/issues/58515)) ([#58517](https://github.com/frappe/erpnext/issues/58517)) ([f0d2d40](f0d2d40d35))
* translate label in party validation (backport [#58624](https://github.com/frappe/erpnext/issues/58624)) ([#58626](https://github.com/frappe/erpnext/issues/58626)) ([9bb8d4f](9bb8d4ff93))
* validate items against source Sales Order in Material Request ([6546646](65466464df))
* validate POS Settings invoice and search fields on the server (backport [#58611](https://github.com/frappe/erpnext/issues/58611)) ([#58620](https://github.com/frappe/erpnext/issues/58620)) ([f8c2927](f8c29274a5))
* Work Order picks wrong Delivery Date when Sales Order has the same item in multiple rows ([#58526](https://github.com/frappe/erpnext/issues/58526)) ([cb4ec72](cb4ec72370))

### Features

* **analytics:** filter sales and purchase analytics by entity ([#58402](https://github.com/frappe/erpnext/issues/58402)) ([655ed81](655ed81575))
* **manufacturing:** add multi-currency support to Blanket Orders (backport [#58472](https://github.com/frappe/erpnext/issues/58472)) ([#58630](https://github.com/frappe/erpnext/issues/58630)) ([ff656b9](ff656b9ad2))
* valuation type for BOM secondary items (backport [#58431](https://github.com/frappe/erpnext/issues/58431)) ([#58632](https://github.com/frappe/erpnext/issues/58632)) ([1b44df4](1b44df480f))
2026-09-02 07:13:39 +00:00
Diptanil Saha
600cbf36af Merge pull request #58645 from frappe/version-16-hotfix
chore: release v16
2026-09-02 12:42:03 +05:30
Frappe PR Bot
b24c9eba55 chore(release): Bumped to Version 16.33.0
# [16.33.0](https://github.com/frappe/erpnext/compare/v16.32.3...v16.33.0) (2026-08-25)

### Bug Fixes

* **accounts:** allocate drop-ship cost by invoice quantity (backport [#58226](https://github.com/frappe/erpnext/issues/58226)) ([#58263](https://github.com/frappe/erpnext/issues/58263)) ([a62a949](a62a949b2a))
* **accounts:** disallow reversing a reverse journal entry ([8a26834](8a26834704))
* **accounts:** match returns to source invoice items (backport [#58250](https://github.com/frappe/erpnext/issues/58250)) ([#58264](https://github.com/frappe/erpnext/issues/58264)) ([9b0db26](9b0db26c25))
* **accounts:** supplier group filter not applied on accounts payable report ([d23b919](d23b9197d5))
* aggregate child warehouses in Stock Qty vs Serial No Count report (backport [#58134](https://github.com/frappe/erpnext/issues/58134)) ([#58366](https://github.com/frappe/erpnext/issues/58366)) ([abe4c0a](abe4c0a6b7))
* apply Sales Person user permissions in Accounts Receivable ([956be58](956be58c9c))
* **assets:** split FIFO/LIFO rate across grouped stock item rows ([98bef1c](98bef1cac8))
* **bank_statement_import:** add missing permission check on `get_import_status` (backport [#58217](https://github.com/frappe/erpnext/issues/58217)) ([#58219](https://github.com/frappe/erpnext/issues/58219)) ([2b685ed](2b685ed982))
* **bank_statement_import:** add missing permission check on multiple whitelisted methods (backport [#58221](https://github.com/frappe/erpnext/issues/58221)) ([#58224](https://github.com/frappe/erpnext/issues/58224)) ([81461ea](81461ea56c))
* block disabled/frozen party on Opportunity and Request for Quotation (backport [#57983](https://github.com/frappe/erpnext/issues/57983)) ([#58035](https://github.com/frappe/erpnext/issues/58035)) ([9f99ba6](9f99ba6ddc))
* **buying:** allow purchase returns against a closed purchase order ([#58139](https://github.com/frappe/erpnext/issues/58139)) ([61b5499](61b549960e))
* conflicts ([cbe7931](cbe7931b3b))
* correct Item Group doctype name in item tax template dashboard  (backport [#58192](https://github.com/frappe/erpnext/issues/58192)) ([#58213](https://github.com/frappe/erpnext/issues/58213)) ([56ac872](56ac87292f))
* correct to and from date filters in timesheet billing summary re… (backport [#58317](https://github.com/frappe/erpnext/issues/58317)) ([#58318](https://github.com/frappe/erpnext/issues/58318)) ([2077e84](2077e844d4))
* **crm_settings:** create custom fields for Frappe CRM on enabling synchronization ([795cf85](795cf8544c))
* describe stale exchange rate settings ([1ec2a2c](1ec2a2cd5d))
* don't set work order status to In Process only due to skip material transfer ([#58245](https://github.com/frappe/erpnext/issues/58245)) ([dd4d108](dd4d10862d))
* drop removed Restaurant doctype from sales tax template dashboard  (backport [#58191](https://github.com/frappe/erpnext/issues/58191)) ([#58211](https://github.com/frappe/erpnext/issues/58211)) ([c3cf7f2](c3cf7f2e91))
* **email_digest:** added permission check for `get_msg_html` (backport [#58197](https://github.com/frappe/erpnext/issues/58197)) ([#58200](https://github.com/frappe/erpnext/issues/58200)) ([32ebd69](32ebd69abc))
* escape interpolated values in portal, print and desk templates (backport [#58273](https://github.com/frappe/erpnext/issues/58273)) ([#58278](https://github.com/frappe/erpnext/issues/58278)) ([6b7b479](6b7b4796b4))
* escape interpolated values in text positions across portal and desk templates (backport [#58286](https://github.com/frappe/erpnext/issues/58286)) ([#58288](https://github.com/frappe/erpnext/issues/58288)) ([d357310](d3573105c4))
* escape on status image for workstations in production status (backport [#58279](https://github.com/frappe/erpnext/issues/58279)) ([#58281](https://github.com/frappe/erpnext/issues/58281)) ([9e2173e](9e2173e4c2))
* filter available batch report by company (backport [#57995](https://github.com/frappe/erpnext/issues/57995)) ([#58077](https://github.com/frappe/erpnext/issues/58077)) ([5933476](5933476778))
* get items from sales order in sales invoice ([#58163](https://github.com/frappe/erpnext/issues/58163)) ([0255314](0255314ea7))
* hide rfq status in supplier portal ([#58368](https://github.com/frappe/erpnext/issues/58368)) ([8a2109e](8a2109e62a))
* hide supplier name in rfq portal (backport [#58373](https://github.com/frappe/erpnext/issues/58373)) ([#58376](https://github.com/frappe/erpnext/issues/58376)) ([2329ef6](2329ef6424))
* ignore historical negative batch stock in outward validation (backport [#58148](https://github.com/frappe/erpnext/issues/58148)) ([#58151](https://github.com/frappe/erpnext/issues/58151)) ([c18881b](c18881b37d))
* Ignore permission while deleting user permission (backport [#57296](https://github.com/frappe/erpnext/issues/57296)) ([#57760](https://github.com/frappe/erpnext/issues/57760)) ([98d69b2](98d69b2ae3))
* include time logs ending at midnight in timesheet billing summary (backport [#58355](https://github.com/frappe/erpnext/issues/58355)) ([#58357](https://github.com/frappe/erpnext/issues/58357)) ([0bf34c0](0bf34c054a))
* **italy:** handle none price_list_rate in e-invoice xml generation (backport [#58242](https://github.com/frappe/erpnext/issues/58242)) ([#58370](https://github.com/frappe/erpnext/issues/58370)) ([feb51a4](feb51a475a))
* keep Currency and Price List section open for foreign currency (backport [#58074](https://github.com/frappe/erpnext/issues/58074)) ([#58113](https://github.com/frappe/erpnext/issues/58113)) ([a3a6efb](a3a6efbcea))
* **manufacturing:** derive material transfers from actual coverage ([#58115](https://github.com/frappe/erpnext/issues/58115)) ([2a9e430](2a9e4304a2))
* **manufacturing:** fall back to item group defaults for work order w… ([#58237](https://github.com/frappe/erpnext/issues/58237)) ([861fb26](861fb26b2c))
* **manufacturing:** keep MRP chart dates distinct ([1b37fd2](1b37fd2edc))
* **Material Requirements Planning Report:** detailed-view chart timescale ([de06cb7](de06cb7f45)), closes [#52632](https://github.com/frappe/erpnext/issues/52632)
* new docs should refetch incoming rates (backport [#58097](https://github.com/frappe/erpnext/issues/58097)) ([#58295](https://github.com/frappe/erpnext/issues/58295)) ([edb9b33](edb9b33264))
* patch to delete the `crm_deal` custom fields ([d66dc14](d66dc143e3))
* prevent duplicate supplier quotations from portal ([39e15c7](39e15c7b2d))
* prevent TimestampMismatchError resolving Dunning with multiple overdue installments ([5764d1d](5764d1d173))
* **projects:** respect permissions in timesheet billing summary (backport [#58320](https://github.com/frappe/erpnext/issues/58320)) ([#58322](https://github.com/frappe/erpnext/issues/58322)) ([647c933](647c93321b))
* Qty and UOM not fetched when adding Item in Material Request ([#58118](https://github.com/frappe/erpnext/issues/58118)) ([aecc551](aecc551b5e))
* renaming the overdue billing checkbox (backport [#58165](https://github.com/frappe/erpnext/issues/58165)) ([#58166](https://github.com/frappe/erpnext/issues/58166)) ([1ea1d5d](1ea1d5d6a5))
* render missing terms before printing ([#58358](https://github.com/frappe/erpnext/issues/58358)) ([f56cdac](f56cdac45f))
* resolve backport conflicts ([3fd3f94](3fd3f9485b))
* respect zero currency precision ([#58395](https://github.com/frappe/erpnext/issues/58395)) ([6b61146](6b61146d2f))
* rewriting the description of the overdue billing checkbox (backport [#58172](https://github.com/frappe/erpnext/issues/58172)) ([#58174](https://github.com/frappe/erpnext/issues/58174)) ([4bb7799](4bb7799919))
* run set_missing_values before creating Purchase Order from MRP report ([72114fa](72114fa738))
* **selling:** read overdue amount from payment ledger, not gl tags  (backport [#57786](https://github.com/frappe/erpnext/issues/57786)) ([#58026](https://github.com/frappe/erpnext/issues/58026)) ([29f6a5b](29f6a5b9cf))
* send auto reorder email to all managers in single company setup ([ac1c692](ac1c6921da))
* skip covered rows when ordering from the mrp report ([#58249](https://github.com/frappe/erpnext/issues/58249)) ([#58312](https://github.com/frappe/erpnext/issues/58312)) ([dfc2301](dfc2301f08))
* **stock:** confirm before changing item qty from the batch selector (backport [#58123](https://github.com/frappe/erpnext/issues/58123)) ([#58125](https://github.com/frappe/erpnext/issues/58125)) ([4f3ffd3](4f3ffd3820))
* **stock:** fetch item stock UOM in stock reconciliation (backport [#58284](https://github.com/frappe/erpnext/issues/58284)) ([#58291](https://github.com/frappe/erpnext/issues/58291)) ([1f21c0b](1f21c0b0af))
* **stock:** honour pick serial / batch based on in the batch selector (backport [#58176](https://github.com/frappe/erpnext/issues/58176)) ([#58182](https://github.com/frappe/erpnext/issues/58182)) ([d3a4d47](d3a4d476eb))
* **stock:** preserve item UOM conversion factor (backport [#58299](https://github.com/frappe/erpnext/issues/58299)) ([#58304](https://github.com/frappe/erpnext/issues/58304)) ([53b61e5](53b61e5ac6))
* **stock:** reset bin when no stock ledger entries remain ([#58362](https://github.com/frappe/erpnext/issues/58362)) ([37a1fd1](37a1fd11e9))
* stop doubling totals in timesheet billing summary (backport [#58311](https://github.com/frappe/erpnext/issues/58311)) ([#58315](https://github.com/frappe/erpnext/issues/58315)) ([c3313f5](c3313f5b34))
* use user data fields hook (backport [#58274](https://github.com/frappe/erpnext/issues/58274)) ([#58283](https://github.com/frappe/erpnext/issues/58283)) ([94b6622](94b6622d9d))
* validation for task end date check ([7b0df4b](7b0df4b28b))
* work order finish dialog with process loss qty from job card (backport [#58256](https://github.com/frappe/erpnext/issues/58256)) ([#58262](https://github.com/frappe/erpnext/issues/58262)) ([27a04d8](27a04d8e08))

### Features

* **accounts:** opt-in 'Consider Accounting Dimension' filter on General Ledger Report (backport [#58156](https://github.com/frappe/erpnext/issues/58156)) ([#58158](https://github.com/frappe/erpnext/issues/58158)) ([f8c3270](f8c3270049))
* add status filter to Supplier Quotation Comparison report ([2b84ed7](2b84ed78e8))
* Belgian Charts of Accounts (commercial + non-profit, FR + NL) (backport [#54679](https://github.com/frappe/erpnext/issues/54679)) ([#58186](https://github.com/frappe/erpnext/issues/58186)) ([d805f4d](d805f4d3fc))
* taxable-base resolver hook for custom charge types ([#56175](https://github.com/frappe/erpnext/issues/56175)) ([ea238a9](ea238a90ac))
* validate purchase receipt exchange rate parity on purchase invoice (backport [#58177](https://github.com/frappe/erpnext/issues/58177)) ([#58190](https://github.com/frappe/erpnext/issues/58190)) ([900064e](900064e395))
2026-08-25 17:04:28 +00:00
Diptanil Saha
1a745cd433 Merge pull request #58397 from frappe/version-16-hotfix
chore: release v16
2026-08-25 22:21:37 +05:30
Frappe PR Bot
11e0ba0a1c chore(release): Bumped to Version 16.32.3
## [16.32.3](https://github.com/frappe/erpnext/compare/v16.32.2...v16.32.3) (2026-08-18)

### Bug Fixes

* Qty and UOM not fetched when adding Item in Material Request (backport [#58118](https://github.com/frappe/erpnext/issues/58118)) ([#58266](https://github.com/frappe/erpnext/issues/58266)) ([f01f741](f01f7415e1))
2026-08-18 09:57:04 +00:00
mergify[bot]
f01f7415e1 fix: Qty and UOM not fetched when adding Item in Material Request (backport #58118) (#58266) 2026-08-18 15:25:21 +05:30
Frappe PR Bot
5412ea0b8c chore(release): Bumped to Version 16.32.2
## [16.32.2](https://github.com/frappe/erpnext/compare/v16.32.1...v16.32.2) (2026-08-18)

### Bug Fixes

* get items from sales order in sales invoice (backport [#58163](https://github.com/frappe/erpnext/issues/58163)) ([#58258](https://github.com/frappe/erpnext/issues/58258)) ([204f979](204f979f33))
2026-08-18 07:29:42 +00:00
mergify[bot]
204f979f33 fix: get items from sales order in sales invoice (backport #58163) (#58258)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-18 07:27:23 +00:00
Frappe PR Bot
21d1873020 chore(release): Bumped to Version 16.32.1
## [16.32.1](https://github.com/frappe/erpnext/compare/v16.32.0...v16.32.1) (2026-08-14)

### Bug Fixes

* ignore historical negative batch stock in outward validation (backport [#58148](https://github.com/frappe/erpnext/issues/58148)) (backport [#58151](https://github.com/frappe/erpnext/issues/58151)) ([#58153](https://github.com/frappe/erpnext/issues/58153)) ([e79b4ab](e79b4abe9d))
2026-08-14 06:20:44 +00:00
mergify[bot]
e79b4abe9d fix: ignore historical negative batch stock in outward validation (backport #58148) (backport #58151) (#58153)
fix: ignore historical negative batch stock in outward validation (backport #58148) (#58151)

fix: ignore historical negative batch stock in outward validation (#58148)

(cherry picked from commit 9239d1c2a3)


(cherry picked from commit c18881b37d)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-08-14 11:48:55 +05:30
Frappe PR Bot
81a6f97566 chore(release): Bumped to Version 16.32.0
# [16.32.0](https://github.com/frappe/erpnext/compare/v16.31.1...v16.32.0) (2026-08-11)

### Bug Fixes

* add raw material to its operation even when another operation uses the item ([35f9bec](35f9bec988))
* add type hints to conversion factor API ([dbfe7e1](dbfe7e199e))
* allow non-admin roles to import chart of accounts (backport [#57454](https://github.com/frappe/erpnext/issues/57454)) ([#58060](https://github.com/frappe/erpnext/issues/58060)) ([a707c82](a707c82c0c))
* allow selecting a warehouse for new items in the update items dialog ([#57876](https://github.com/frappe/erpnext/issues/57876)) ([c9977be](c9977be5d4))
* cap a manufacture entry at the job card's pending production ([79ad410](79ad410cc1))
* clear deferred revenue/expense fields on uncheck (backport [#57140](https://github.com/frappe/erpnext/issues/57140)) ([7e72e70](7e72e70cc1))
* **coa_importer:** added server-side validations for importing chart of accounts (backport [#58065](https://github.com/frappe/erpnext/issues/58065)) ([#58066](https://github.com/frappe/erpnext/issues/58066)) ([db22fcb](db22fcb157))
* condition check with empty object for falsy case ([838fb8e](838fb8e8df))
* **consolidated cash flow:** correct totals and labels in section foo… (backport [#57336](https://github.com/frappe/erpnext/issues/57336)) ([#58056](https://github.com/frappe/erpnext/issues/58056)) ([f11d0d5](f11d0d5cec))
* **controllers:** correct negative rate settings link ([51aecec](51aecec598))
* convert hours to minutes in workstation complete_job ([822e6d8](822e6d8924))
* declare precision 9 on all conversion_factor fields ([db49b03](db49b03913))
* derive operation FG items before material expansion, keep the final one the BOM's item ([3e0d0b2](3e0d0b2d68))
* don't demand raw material transfer for semi FG job cards on submit ([99d9d84](99d9d845bd))
* escape `customer_details` on lead creation from appointment (backport [#57947](https://github.com/frappe/erpnext/issues/57947)) ([#57949](https://github.com/frappe/erpnext/issues/57949)) ([dff4ec6](dff4ec6a74))
* field validation and perm checks on `get_stock_reservation_entries_for_voucher` (backport [#57968](https://github.com/frappe/erpnext/issues/57968)) ([#57970](https://github.com/frappe/erpnext/issues/57970)) ([0a9b632](0a9b632925))
* generate the next manufacture entry net of booked process loss ([2548751](2548751673))
* guard reconciliation table deletes when tables are missing ([21fbfa5](21fbfa541d))
* handling negative grand total ([e657a7f](e657a7f19f))
* incorrect batch-wise valuation rate for entries with same posting datetime (backport [#57794](https://github.com/frappe/erpnext/issues/57794)) ([#57797](https://github.com/frappe/erpnext/issues/57797)) ([adfa676](adfa6768c9))
* incorrect entry detection in Stock Ledger Invariant Check ([#57886](https://github.com/frappe/erpnext/issues/57886)) ([f7bae88](f7bae888cf))
* item property updates in POS and transactions and add styling (backport [#57189](https://github.com/frappe/erpnext/issues/57189)) ([#58058](https://github.com/frappe/erpnext/issues/58058)) ([5f43c89](5f43c89c45))
* **job_card:** apply the completion dialog's qty to manufacture ([#57685](https://github.com/frappe/erpnext/issues/57685)) ([d97cf13](d97cf131a1))
* **job_card:** leave the pending qty out of the job card's own output ([#57686](https://github.com/frappe/erpnext/issues/57686)) ([0c79194](0c7919429e))
* **job_card:** reject a completion split that cannot add up ([#57687](https://github.com/frappe/erpnext/issues/57687)) ([3907d93](3907d93f9f))
* **job_card:** require the previous operation to be manufactured ([#57684](https://github.com/frappe/erpnext/issues/57684)) ([a22a7fd](a22a7fddba))
* **journal_entry:** validate blocked purchase invoices ([04718e1](04718e15c9))
* **journal_entry:** validation message for blocked purchase invoice ([#57896](https://github.com/frappe/erpnext/issues/57896)) ([f0adbd2](f0adbd2bd3))
* keep asset repair downtime in sync with entered dates ([ca5418e](ca5418e2c9))
* keep job card actions visible for corrective job cards ([4ee276d](4ee276d3db))
* keep source rate on re-fetch when maintain same rate is enabled (backport [#57479](https://github.com/frappe/erpnext/issues/57479)) ([#57792](https://github.com/frappe/erpnext/issues/57792)) ([ee5316e](ee5316ecd0))
* keep Target Warehouse optional for work orders tracking semi finished goods ([c598cf9](c598cf9010))
* keep the transfer qty check for legacy semi FG cards without an FG item ([f810d78](f810d780c0))
* **manufacturing:** adapt version 16 compatibility ([a8060d5](a8060d5e99))
* **manufacturing:** align quantity split rounding ([317dd18](317dd18ce5))
* **manufacturing:** avoid child BOM cache lookups ([fb87751](fb877515ca))
* **manufacturing:** correct nested BOM quantities ([b4929e2](b4929e2737))
* **manufacturing:** keep item code searchable when a barcode matches the same text ([87c4009](87c4009572))
* **manufacturing:** type whitelisted BOM arguments ([87b456f](87b456faa2))
* mirror rounding adjustment on distributed_discount_amount (backport [#58047](https://github.com/frappe/erpnext/issues/58047)) ([#58055](https://github.com/frappe/erpnext/issues/58055)) ([d680115](d680115fcb))
* optimize product bundle item search ([2934971](29349711e4))
* preserve custom title on new JV (backport [#57987](https://github.com/frappe/erpnext/issues/57987)) ([#57989](https://github.com/frappe/erpnext/issues/57989)) ([4a2163e](4a2163ebf8))
* preserve original operation idx in manually created Job Cards ([fee003a](fee003a82d))
* purchase return of batchwise valuation batch valued at original receipt rate instead of batch avg rate (version-16-hotfix) ([#57836](https://github.com/frappe/erpnext/issues/57836)) ([aa70d9b](aa70d9bbc3))
* re-check future sle before queuing repost on submit ([#57664](https://github.com/frappe/erpnext/issues/57664)) ([#57961](https://github.com/frappe/erpnext/issues/57961)) ([2b020c2](2b020c2fcf))
* reflect in-invoice receivable settlements in Sales Register ledger view ([a90907f](a90907fda6))
* **regional:** rename Italy's duplicate Customer name fields ([f56867d](f56867d843)), closes [#46281](https://github.com/frappe/erpnext/issues/46281) [#50915](https://github.com/frappe/erpnext/issues/50915) [#57215](https://github.com/frappe/erpnext/issues/57215) [#50921](https://github.com/frappe/erpnext/issues/50921) [#53409](https://github.com/frappe/erpnext/issues/53409)
* repost read stale sibling SLE rate for moving average returns ([ffa65b0](ffa65b0c48))
* require FG / Semi FG Item on operations when tracking semi finished goods ([03eeb83](03eeb839fb))
* require material transfer before job card start and completion ([a7f75de](a7f75de1aa))
* require WIP warehouse for work orders tracking semi finished goods ([24cd5f2](24cd5f22b5))
* roll up process loss to the work order for semi finished goods ([27130d8](27130d8e49))
* round computed conversion factors to field precision ([206ed28](206ed28924))
* round production plan mr_items quantity to field precision ([2d056ae](2d056aee3d))
* **sales-invoice:** respect Customize Form hidden setting on Update Stock ([#57819](https://github.com/frappe/erpnext/issues/57819)) ([0e26f9b](0e26f9b1db))
* scale generated raw materials to the manufacture entry's production share ([acba994](acba9945b0))
* scope manufacture entry process loss to its own job card ([a0b370b](a0b370b2e9)), closes [frappe/erpnext#57892](https://github.com/frappe/erpnext/issues/57892)
* **selling:** bill re-delivered sales order quantities ([6fb09b1](6fb09b1e21))
* **selling:** preserve explicit UOM during item selection ([218397e](218397e78d))
* **selling:** read overdue amount from payment ledger, not gl tags (backport [#57786](https://github.com/frappe/erpnext/issues/57786)) ([#58057](https://github.com/frappe/erpnext/issues/58057)) ([b89229a](b89229a93d))
* **selling:** reset stale item details on item change ([b5d727f](b5d727fb81))
* set `restrict_globals=True` in `frappe.render_template` (backport [#57899](https://github.com/frappe/erpnext/issues/57899)) ([#57902](https://github.com/frappe/erpnext/issues/57902)) ([6b45002](6b45002abc))
* **setup:** fetch driver address by supplier link ([62a851b](62a851bebb))
* skip incoming rate calc when serial no qty is zero (backport [#57427](https://github.com/frappe/erpnext/issues/57427)) ([#57957](https://github.com/frappe/erpnext/issues/57957)) ([a4d9411](a4d9411345))
* skip PO items with invoice-created receipts in billing repair patch ([9139994](9139994686))
* skip the pending production check on update-after-submit saves ([491f9fa](491f9fa3fe))
* **stock:** drop call to confirm_if_drafts_exist missing on v16 ([#57833](https://github.com/frappe/erpnext/issues/57833)) ([626e351](626e35135f))
* **stock:** handle multi-item opening balance in Stock Ledger report (backport [#57591](https://github.com/frappe/erpnext/issues/57591)) ([#57796](https://github.com/frappe/erpnext/issues/57796)) ([af3184c](af3184c8b4))
* **stock:** repair duplicated purchase receipt billing ([fa733e6](fa733e691b))
* **stock:** validate new warehouse inventory account after naming ([fce0eb1](fce0eb1577))
* **stock:** validate warehouse accounts when used ([0dfa54f](0dfa54f812))
* stop asking for a manufacturing entry when process loss explains the shortfall ([104c8df](104c8df765))
* **subscription:** don't reactivate a cancelled subscription (backport [#57774](https://github.com/frappe/erpnext/issues/57774))  ([eeb3cd2](eeb3cd238e)), closes [#57761](https://github.com/frappe/erpnext/issues/57761) [#56796](https://github.com/frappe/erpnext/issues/56796) [#56796](https://github.com/frappe/erpnext/issues/56796) [#57780](https://github.com/frappe/erpnext/issues/57780)
* sync open reference forms after Quality Inspection updates them ([33eb619](33eb6199d9))
* sync translations from crowdin ([#57841](https://github.com/frappe/erpnext/issues/57841)) ([a9d139a](a9d139ae6b))
* sync translations from crowdin ([#58069](https://github.com/frappe/erpnext/issues/58069)) ([f697b02](f697b024db))
* tolerate floating-point drift in sales team allocated percentage ([39f15bb](39f15bb3e9))
* use stock settings for warehouse defaults ([d8bbe86](d8bbe865a8))
* validate webform for project ([8d98fe8](8d98fe8187))
* zero-rate repost fallback could still read sibling SLE ([81f81ff](81f81fff32))

### Features

* **job_card:** print quantities with their stock uom ([#57689](https://github.com/frappe/erpnext/issues/57689)) ([eb90186](eb90186d3f))
* stock validations in Period Closing Voucher and snapshot-seeded batch valuation (backport [#57811](https://github.com/frappe/erpnext/issues/57811)) ([#57816](https://github.com/frappe/erpnext/issues/57816)) ([243266f](243266f5ef))
* sync serial no status from stock ledger in Stock Qty vs Serial No Count report (version-16-hotfix) ([#57864](https://github.com/frappe/erpnext/issues/57864)) ([ea5cbb1](ea5cbb116c))
2026-08-11 23:39:40 +00:00
Diptanil Saha
b27bc42d51 Merge pull request #58024 from frappe/version-16-hotfix
chore: release v16
2026-08-12 05:08:00 +05:30
Frappe PR Bot
22247ab7c5 chore(release): Bumped to Version 16.31.1
## [16.31.1](https://github.com/frappe/erpnext/compare/v16.31.0...v16.31.1) (2026-08-06)

### Bug Fixes

* **stock:** drop call to confirm_if_drafts_exist missing on v16 ([#57833](https://github.com/frappe/erpnext/issues/57833)) ([e834910](e834910bfd))
2026-08-06 08:12:42 +00:00
Mihir Kandoi
f8b19f7376 Merge pull request #57834 from frappe/mergify/bp/version-16/pr-57833
fix(stock): drop call to confirm_if_drafts_exist missing on v16 (backport #57833)
2026-08-06 13:40:55 +05:30
Mihir Kandoi
e834910bfd fix(stock): drop call to confirm_if_drafts_exist missing on v16 (#57833)
(cherry picked from commit 626e35135f)
2026-08-06 08:09:41 +00:00
Frappe PR Bot
68ea583a1f chore(release): Bumped to Version 16.31.0
# [16.31.0](https://github.com/frappe/erpnext/compare/v16.30.0...v16.31.0) (2026-08-04)

### Bug Fixes

* **accounts receivable:** made territory field multi select (backport [#57322](https://github.com/frappe/erpnext/issues/57322)) ([#57346](https://github.com/frappe/erpnext/issues/57346)) ([9b452f1](9b452f12b7))
* **accounts:** fetch deferred invoice docs on non-empty `sales_docs` or `purchase_docs` in repost accounting ledger ([#57753](https://github.com/frappe/erpnext/issues/57753)) ([6153202](6153202231))
* **accounts:** skip party dashboard without invoice permission ([ee6955d](ee6955d56c))
* **accounts:** update AU standard chart of accounts (backport [#57145](https://github.com/frappe/erpnext/issues/57145)) ([#57608](https://github.com/frappe/erpnext/issues/57608)) ([1ea1ce1](1ea1ce15d0))
* allow custom remark on reversal journal entry ([b4dfca9](b4dfca9ef1))
* apply the supplier selection to this branch's own mapper module ([e5f8d0c](e5f8d0c84b))
* **assets:** add permission checks on whitelisted methods on `asset_capitalization` ([99f249b](99f249b1a0))
* **banking:** fetch company list from DB instead of boot (backport [#57731](https://github.com/frappe/erpnext/issues/57731)) ([#57739](https://github.com/frappe/erpnext/issues/57739)) ([ca60653](ca6065398c))
* do not fetch a random inventory account when multiple inventory accounts exist (backport [#57626](https://github.com/frappe/erpnext/issues/57626)) ([#57632](https://github.com/frappe/erpnext/issues/57632)) ([34cbd3c](34cbd3c8d5))
* drop row prefix in reserve stock message when row is unknown ([684ae4d](684ae4d762))
* escape data in multiple templates (backport [#57742](https://github.com/frappe/erpnext/issues/57742)) ([#57770](https://github.com/frappe/erpnext/issues/57770)) ([824ae57](824ae57e44))
* escape item code and UOM in the supplier dialog errors ([06a753f](06a753faf3))
* exclude transferred and consumed qty from dashboard reserved stock ([46317b0](46317b063a))
* filter Accounts Receivable by invoice sales partner (backport [#57628](https://github.com/frappe/erpnext/issues/57628)) ([#57648](https://github.com/frappe/erpnext/issues/57648)) ([0e46937](0e46937f60))
* guard against None row in get_stock_balance_for (backport [#57567](https://github.com/frappe/erpnext/issues/57567)) ([283ee6e](283ee6e07b))
* guard scio row lookup in stock entry items_add ([fb6c87d](fb6c87dd0c))
* **italy:** skip e-invoicing for opening invoices ([5e584d1](5e584d1cfb))
* **item_variant:** added permission checks on `enqueue_multiple_variant_creation` ([c7cf9d8](c7cf9d868b))
* keep the bulk supplier field to half the supplier selection dialog ([2c9db13](2c9db13041))
* label the items table in the supplier selection dialog ([187840b](187840b559))
* let Purchase Receipt cancel defer to Frappe's linked-document check (backport [#57592](https://github.com/frappe/erpnext/issues/57592)) ([#57597](https://github.com/frappe/erpnext/issues/57597)) ([16be0f0](16be0f0944))
* link the item and spell out the unit in the supplier dialog errors ([810b9ae](810b9ae28f))
* list the Purchase Orders created per supplier instead of opening one ([d2fe4b6](d2fe4b623c))
* **manufacturing:** reach the whole configurator from tree toolbar actions ([281e92f](281e92fb6e)), closes [#57773](https://github.com/frappe/erpnext/issues/57773)
* open the Purchase Order when the supplier selection creates only one ([e71cef0](e71cef02b3))
* **opportunity:** add validation for positive item quantities ([a9f969e](a9f969e942))
* **payment reconciliation:** correct supplier gain/loss posting ([dc907ad](dc907add40))
* **payment_request:** added permission checks on `resend_payment_email` ([fcbbb25](fcbbb251cf))
* **plant_floor:** add missing perm check on `get_stock_summary` (backport [#57667](https://github.com/frappe/erpnext/issues/57667)) ([#57670](https://github.com/frappe/erpnext/issues/57670)) ([7bc0475](7bc04752fc))
* **pos:** don't double-escape Item Group names in get_item_groups ([#57673](https://github.com/frappe/erpnext/issues/57673)) ([a5544d0](a5544d0bfb))
* preserve UOM conversion factor precision in transactions ([e5999b2](e5999b22c7))
* prevent duplicate shipping charges without cost center ([666b616](666b6167a1))
* **projects:** add project filter ([3ba8313](3ba83134af))
* **projects:** include on hold status in project filters and reports ([d104d8e](d104d8e723))
* **purchase:** reject purchase returns where every item has zero quantity ([032b922](032b922f0c))
* **quotation:** carry forward communications from opportunity at after_insert (backport [#57639](https://github.com/frappe/erpnext/issues/57639)) ([#57643](https://github.com/frappe/erpnext/issues/57643)) ([624a236](624a236f88))
* reject the same Material Request item twice in one supplier selection ([ea770f6](ea770f6a8e))
* resolve version 16 backport conflicts ([e98471d](e98471d9c9))
* resolve version-16 backport conflicts ([697f68d](697f68d1d2))
* respect child warehouse account override in Stock and Account Value Comparison ([#57552](https://github.com/frappe/erpnext/issues/57552)) ([ca657d2](ca657d2629))
* respect quantity precision in material transfer validation ([eb969a5](eb969a5866))
* **sales:** reject sales returns where every item has zero quantity ([aa71cd6](aa71cd695b))
* seed standard Item Groups under the existing tree root ([4ffa950](4ffa950aa3)), closes [#57581](https://github.com/frappe/erpnext/issues/57581)
* **selling:** don't require cancel and delete perms to remove items via Update Items (backport [#57419](https://github.com/frappe/erpnext/issues/57419)) ([#57601](https://github.com/frappe/erpnext/issues/57601)) ([04e1ca8](04e1ca8226))
* set Required By on Purchase Orders created per supplier ([f99d66d](f99d66d578))
* set reservation voucher_qty to voucher demand not reserved qty ([ecccedf](ecccedf0ed))
* source manually created asset value from valuation rate ([b556b01](b556b012f0))
* **stock:** accept every number a reading can be written as ([fccf122](fccf1220f6))
* **stock:** correct secondary item valuation across stock entry purposes ([4ed0374](4ed03748fe)), closes [#57732](https://github.com/frappe/erpnext/issues/57732) [#57735](https://github.com/frappe/erpnext/issues/57735) [#57736](https://github.com/frappe/erpnext/issues/57736) [#57737](https://github.com/frappe/erpnext/issues/57737) [#57738](https://github.com/frappe/erpnext/issues/57738)
* **stock:** keep manufactured item rate at zero when inputs are free  (backport [#57334](https://github.com/frappe/erpnext/issues/57334)) ([#57513](https://github.com/frappe/erpnext/issues/57513)) ([49924dd](49924ddd1d))
* **stock:** read quality inspection readings in the user's number format ([3b7fb68](3b7fb6851a))
* **stock:** resolve backport conflict in quality inspection imports ([656db1c](656db1c2fe))
* **stock:** scope over deliver/receive role check to delivery and receipt overflow ([4713ddd](4713ddd55b))
* **stock:** validate only the variant attributes that changed ([0013908](00139081f6))
* **stock:** validate over delivery/receipt allowance in stock settings ([3f3292c](3f3292ca4a))
* **stock:** value batched packed-item returns from the original bundle  (backport [#57327](https://github.com/frappe/erpnext/issues/57327)) ([#57511](https://github.com/frappe/erpnext/issues/57511)) ([c0ac8aa](c0ac8aaf86))
* sync translations from crowdin ([#57741](https://github.com/frappe/erpnext/issues/57741)) ([285aec3](285aec3164))
* update stock variance account logic which defaults to default expense ([#57656](https://github.com/frappe/erpnext/issues/57656)) ([d09c04a](d09c04a623))
* use payment entry posting date for received amount exchange rate (backport [#57660](https://github.com/frappe/erpnext/issues/57660)) ([#57663](https://github.com/frappe/erpnext/issues/57663)) ([5595d1e](5595d1ed2f))
* validate account frozen date ([17aeb0b](17aeb0b55b))
* warn about existing draft orders before the supplier selection creates more ([93331a1](93331a1cf0))

### Features

* adjust the ordered quantity in the supplier selection dialog ([2c8c375](2c8c375ca6))
* alert when Required By falls back to today ([1adeb66](1adeb66bdc))
* auto-fill subscription accounting dimensions from plan with item fallback (backport [#57615](https://github.com/frappe/erpnext/issues/57615)) ([#57622](https://github.com/frappe/erpnext/issues/57622)) ([ebba4e9](ebba4e9958))
* order only the items ticked in the supplier selection dialog ([d6ee543](d6ee5436b8))
* select a supplier per item when creating Purchase Orders from Material Request ([95f7810](95f7810948))
* set one supplier across every item in the supplier selection dialog ([9b647be](9b647bed5c))
* show the UOM alongside the quantity in the supplier selection dialog ([5aaefec](5aaefec747))
* status based bar colors in Work Order gantt view (backport [#57634](https://github.com/frappe/erpnext/issues/57634)) ([#57636](https://github.com/frappe/erpnext/issues/57636)) ([2d03d80](2d03d80269))
2026-08-04 20:30:34 +00:00
Diptanil Saha
2769a8c69e Merge pull request #57781 from frappe/version-16-hotfix
chore: release v16
2026-08-05 01:58:06 +05:30
Frappe PR Bot
8378b6e203 chore(release): Bumped to Version 16.30.0
# [16.30.0](https://github.com/frappe/erpnext/compare/v16.29.0...v16.30.0) (2026-07-28)

### Bug Fixes

* add permission check for `get_item_details` (backport [#57515](https://github.com/frappe/erpnext/issues/57515)) ([#57551](https://github.com/frappe/erpnext/issues/57551)) ([7a606ab](7a606ab91c))
* bump item doctype modified timestamp so description change syncs on migrate ([2471c5c](2471c5ccbb))
* **crm:** align Opportunity status checks with Quotation statuses (backport [#57489](https://github.com/frappe/erpnext/issues/57489)) ([#57491](https://github.com/frappe/erpnext/issues/57491)) ([ad9870a](ad9870acc1))
* **crm:** clarify the reason why an opportunity cannot be declared as lost (backport [#57495](https://github.com/frappe/erpnext/issues/57495)) ([#57498](https://github.com/frappe/erpnext/issues/57498)) ([3ea19d8](3ea19d8eb1))
* detect the currency column by fieldtype in trends total row ([a30aac8](a30aac87bf))
* enable the 'Include Zero Stock Items' filter by default to show zero-stock items in the Stock Balance report (backport [#57458](https://github.com/frappe/erpnext/issues/57458)) ([#57460](https://github.com/frappe/erpnext/issues/57460)) ([a0af717](a0af717234))
* enhance growth view filtering by validating period keys ([3f87836](3f87836536))
* force-delete repost data file during cleanup (backport [#57245](https://github.com/frappe/erpnext/issues/57245)) ([32baf6a](32baf6a47c))
* get reserved batch qty precision from settings ([95de237](95de2374ed))
* guard against missing is_your_company_address custom field on address ([2e6b4b5](2e6b4b5838))
* 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)) ([#57397](https://github.com/frappe/erpnext/issues/57397)) ([84813d7](84813d7f46))
* **item:** correct description on deferred revenue/expense ([ee12f8d](ee12f8d2d4))
* **manufacturing:** exclude corrective job cards from semi-FG aggregate ([ebc8482](ebc8482310))
* **manufacturing:** fall back to UOM Conversion Factor in Production Plan (backport [#57553](https://github.com/frappe/erpnext/issues/57553)) ([#57554](https://github.com/frappe/erpnext/issues/57554)) ([8f36753](8f36753705))
* **manufacturing:** scope BOM Creator tree children to the parent row ([339bb0b](339bb0b4ea)), closes [#57311](https://github.com/frappe/erpnext/issues/57311)
* **manufacturing:** sum semi-FG qty across split job cards ([c411b8e](c411b8e471))
* **manufacturing:** update cost of BOMs created via BOM Creator ([f1a0a5e](f1a0a5e1bf))
* map MT940 per-transaction reference from :61: customer_reference ([b9c4f79](b9c4f790bd))
* map pick list customer to delivery note when no sales order ([3f6501b](3f6501b4ff)), closes [#57412](https://github.com/frappe/erpnext/issues/57412)
* migrate stored AR/AP ageing filter to renamed field ([2e5fc38](2e5fc38179))
* pool batch slot values on every run, not only when negative ([5763378](5763378ee1))
* **quotation:** carry forward communications from opportunity (backport [#57507](https://github.com/frappe/erpnext/issues/57507)) ([#57509](https://github.com/frappe/erpnext/issues/57509)) ([f9b3e42](f9b3e42dcd))
* rebalance batch slot values at the pooled rate when driven negative ([8a50572](8a50572786))
* recalculate operating cost on hour rate change in routing ([c182b40](c182b4085b))
* recover failed POS closings (backport [#57203](https://github.com/frappe/erpnext/issues/57203)) ([#57572](https://github.com/frappe/erpnext/issues/57572)) ([9c946eb](9c946eb168))
* rename misleading filter labels in AR/AP reports ([6d856fa](6d856fa632))
* resolve backport conflicts in item doctype ([0ce5040](0ce50407f9))
* respect selected BOM when creating work order for variant item ([#57359](https://github.com/frappe/erpnext/issues/57359)) ([1132eb1](1132eb1a0f))
* respect user permissions in party dashboard company list ([c79f2e4](c79f2e45a8)), closes [frappe/erpnext#57428](https://github.com/frappe/erpnext/issues/57428)
* **setup:** scope manufacturing warehouse filters to company ([f05e8ed](f05e8ed0ce))
* skip stock expense GL entries for non-stock items ([#57518](https://github.com/frappe/erpnext/issues/57518)) ([861c50e](861c50e727))
* **stock:** narrow legacy serial ledger lookup by item (backport [#57499](https://github.com/frappe/erpnext/issues/57499)) ([#57506](https://github.com/frappe/erpnext/issues/57506)) ([59efe72](59efe7299a))
* stop storing raw title template on subcontracting orders ([0863c1e](0863c1e05c))
* **subcontracting:** release raw-material reservation when closing a subcontracting order ([ee75fac](ee75fac9d7))
* **test:** flaky test in exchange rate revaluation ([484ff8e](484ff8e349))
* typeerror in get_batches_by_oldest for mixed batch expiry ([bbe7580](bbe7580c9d))
* update formatting of growth view for FS report ([da3844c](da3844c4df))
* update operating cost when propagating workstation hour rate to routing ([#57504](https://github.com/frappe/erpnext/issues/57504)) ([39d5fd8](39d5fd84db))
* use company currency instead of global default in report (backport [#56561](https://github.com/frappe/erpnext/issues/56561)) ([9e5f77b](9e5f77b57c))

### Features

* block sales invoice submit when customer overdue exceeds threshold (backport [#57230](https://github.com/frappe/erpnext/issues/57230), [#57298](https://github.com/frappe/erpnext/issues/57298)) ([#57438](https://github.com/frappe/erpnext/issues/57438)) ([d3c5e86](d3c5e866f0)), closes [#52960](https://github.com/frappe/erpnext/issues/52960)
* book Expenses Added To Stock GL entries (backport [#57190](https://github.com/frappe/erpnext/issues/57190) + [#57475](https://github.com/frappe/erpnext/issues/57475)) ([#57503](https://github.com/frappe/erpnext/issues/57503)) ([68caa60](68caa60dfa))
* make Shipping Rule Cost Center optional with company default fallback (backport [#57355](https://github.com/frappe/erpnext/issues/57355)) ([#57385](https://github.com/frappe/erpnext/issues/57385)) ([bbd942c](bbd942c600))
2026-07-28 23:31:26 +00:00
Diptanil Saha
eaf95e5c36 Merge pull request #57548 from frappe/version-16-hotfix
chore: release v16
2026-07-29 04:59:39 +05:30
Frappe PR Bot
a5de60c357 chore(release): Bumped to Version 16.29.0
# [16.29.0](https://github.com/frappe/erpnext/compare/v16.28.0...v16.29.0) (2026-07-22)

### Bug Fixes

* add fetch from in production plan material request child table ([2852671](2852671cd5))
* added missing validations for `Dunning Type` (backport [#57224](https://github.com/frappe/erpnext/issues/57224)) ([#57227](https://github.com/frappe/erpnext/issues/57227)) ([3948951](3948951904))
* allow delivery when a batch is reserved across multiple sales orders (backport [#57169](https://github.com/frappe/erpnext/issues/57169)) ([56bbca0](56bbca0203))
* block changing Stock account type when stock ledger entries exist (backport [#57283](https://github.com/frappe/erpnext/issues/57283)) ([#57285](https://github.com/frappe/erpnext/issues/57285)) ([a26296c](a26296ca50))
* consider min order qty in the purchase/transfer flow of production plan (backport [#57204](https://github.com/frappe/erpnext/issues/57204)) ([cbd4f93](cbd4f93f3d))
* correct typo in allow_negative_stock parameter ([e2be05e](e2be05e48b))
* dont overwrite rate with 0 if not found ([aa08f75](aa08f753b4))
* exclude fully-delivered reservations from the conflict message ([555c607](555c607f2f))
* exclude transferred_qty from work order item to pick list item mapping ([c466b49](c466b49d09)), closes [#57253](https://github.com/frappe/erpnext/issues/57253) [#57236](https://github.com/frappe/erpnext/issues/57236) [#56596](https://github.com/frappe/erpnext/issues/56596)
* hide job card field in purchase order item ([20f6ac8](20f6ac81b9))
* keep Standard Cost stock value in step with the standard rate ([fe65882](fe65882e59))
* name every conflicting voucher in the reserved batch error ([839fd5e](839fd5e3b6))
* parallel reposting stalls between scheduler ticks (backport [#57220](https://github.com/frappe/erpnext/issues/57220)) ([#57248](https://github.com/frappe/erpnext/issues/57248)) ([a3bfded](a3bfdede06))
* pass ctx keys get_price_list_rate_for reads, skip rate update on insert ([c3aea9c](c3aea9ca9c))
* **payments:** ensure `payments` app installed on the site in `payment_app_import_guard` (backport [#57342](https://github.com/frappe/erpnext/issues/57342)) ([#57344](https://github.com/frappe/erpnext/issues/57344)) ([2c8c076](2c8c076f6e))
* permission issue (backport [#57112](https://github.com/frappe/erpnext/issues/57112)) (backport [#57142](https://github.com/frappe/erpnext/issues/57142)) ([#57184](https://github.com/frappe/erpnext/issues/57184)) ([0817d10](0817d1064c))
* project % complete field allowing modification when manual method (backport [#57274](https://github.com/frappe/erpnext/issues/57274)) ([#57276](https://github.com/frappe/erpnext/issues/57276)) ([0d53f1a](0d53f1adb7))
* **project:** improved access control for project users (backport [#56675](https://github.com/frappe/erpnext/issues/56675)) ([#57181](https://github.com/frappe/erpnext/issues/57181)) ([b1adec7](b1adec7e9e))
* **report:** handle nonetype error in timesheet billing summary grouping logic ([b7cf3bf](b7cf3bf641))
* rescale batch FIFO slot values on stock reconciliation ([6b3b03f](6b3b03fcd8))
* rescale stock ageing FIFO slot values on stock reconciliation ([c9394c0](c9394c030f))
* resolve float precision before streaming stock ledger entries ([0f25254](0f252542c3))
* restrict jinja globals in process statement of accounts templates (backport [#56458](https://github.com/frappe/erpnext/issues/56458)) ([#57232](https://github.com/frappe/erpnext/issues/57232)) ([5f6952b](5f6952b15c))
* revalue batch reco slots only when the entry covers the full batch ([a4bf506](a4bf50656a))
* round outstanding qty guard consistently with the conflict gate ([f3e1b3f](f3e1b3fca7))
* set correct currency in supplier quotation net rate field ([e24ef84](e24ef847dc))
* show transaction currency symbol in Payment Request schedule dialog and reference table (backport [#57050](https://github.com/frappe/erpnext/issues/57050)) ([#57312](https://github.com/frappe/erpnext/issues/57312)) ([429b58b](429b58b833))
* **stock:** correct stock ageing value for moving average and lifo items ([#56693](https://github.com/frappe/erpnext/issues/56693)) ([02549ef](02549ef8c4))
* strip account number when building account name in COA importer ([e1ebfa7](e1ebfa7163))
* sync process loss percentage when fg qty changes ([beeffee](beeffee8f9))
* sync translations from crowdin ([#57187](https://github.com/frappe/erpnext/issues/57187)) ([827831a](827831a247))
* use system float precision for batch qty comparison ([92217b2](92217b2c45))
* validate buying price list on material request and update item rates on change ([025b2f2](025b2f2922))

### Features

* add on hold status to project ([57a49ce](57a49ce168))
* recalculate valuation rate and stock value from Bin ([a38cbfc](a38cbfc88e))
* **stock:** automatically link portal users to their associated contact profiles for customers and suppliers ([134d63d](134d63de78))
* **stock:** expose all Bin qty fields in Stock Summary and Stock Projected Qty ([0bdf258](0bdf258888))
2026-07-22 00:17:03 +00:00
Diptanil Saha
264bfa188b Merge pull request #57324 from frappe/version-16-hotfix
chore: release v16
2026-07-22 05:45:21 +05:30
Frappe PR Bot
de591661b9 chore(release): Bumped to Version 16.28.0
# [16.28.0](https://github.com/frappe/erpnext/compare/v16.27.0...v16.28.0) (2026-07-15)

### Bug Fixes

* **accounts:** added permission checks on `get_account_balances_coa` (backport [#57107](https://github.com/frappe/erpnext/issues/57107)) ([#57122](https://github.com/frappe/erpnext/issues/57122)) ([e8a5325](e8a532587d))
* allow barcode scan to add and increment items in pick list ([00a646a](00a646ac25))
* apply precision fix inline for v16-hotfix, drop develop-only asset_service refactor ([d3a8e91](d3a8e91cda))
* **asset:** allow asset repair creation for fully depreciated assets ([8658039](8658039e9a))
* **budget-variance:** correct month shift in comparison chart ([703e9a7](703e9a728c))
* guard company logo lookup in default letterheads ([7b54314](7b543142a2))
* **journal-entry:** fetch outstanding on foreign currency ([ec782ee](ec782ee20d))
* **manufacturing:** preserve job card transferred quantity ([d7e9321](d7e9321746))
* match depreciation schedule rows at currency precision to avoid duplicate JEs ([a1fae95](a1fae959ed))
* prevent duplicate material request items in purchase order ([4d951c1](4d951c1cf8))
* set explicit table and logo widths in grey letterhead ([0d80fef](0d80fef3bf))
* **stock:** fix sqlparse token limit in get_bundle_wise_serial_nos ([5a99dd6](5a99dd6016))
* **stock:** set stock_uom on transferred Stock Reservation Entries ([5991ecf](5991ecfa3d))
* **stock:** show qty (company) and qty (warehouse) in sales transactions ([0d9ace7](0d9ace7ab8))
* **tnc:** `get_terms_and_conditions` render_template with `safe_exec` (backport [#56944](https://github.com/frappe/erpnext/issues/56944)) ([#56977](https://github.com/frappe/erpnext/issues/56977)) ([40c85a0](40c85a0087))

### Features

* **balance-sheet:** implement execute_synced_report with full parity to normal report ([636bcbe](636bcbedc0))
* **general-ledger:** implement execute_synced_report with full parity to normal report ([19ec095](19ec095ff8))
* **profit-and-loss:** implement execute_synced_report with full parity to normal report ([21ddb00](21ddb00e20))
* **trial-balance:** implement execute_duckdb with full parity to normal report ([228418b](228418b05f))
* weekly auto-repost of incorrect stock valuation entries (backport [#56637](https://github.com/frappe/erpnext/issues/56637)) ([#56700](https://github.com/frappe/erpnext/issues/56700)) ([bf1b7f2](bf1b7f2bea))
2026-07-15 02:24:51 +00:00
ruthra kumar
9a7e796fd2 Merge pull request #57161 from frappe/version-16-hotfix
chore: release v16
2026-07-15 06:17:14 +05:30
Frappe PR Bot
9d5c7605b8 chore(release): Bumped to Version 16.27.0
# [16.27.0](https://github.com/frappe/erpnext/compare/v16.26.2...v16.27.0) (2026-07-13)

### Bug Fixes

* **`frappe_crm_api`:** handle failure for `after_app_install` and `after_app_uninstall` ([eac3afc](eac3afcd88))
* **accounts:** clear clearance date when amending reconciled voucher ([6f28135](6f281356a6)), closes [#54909](https://github.com/frappe/erpnext/issues/54909)
* **accounts:** retain invoice table on opening invoice creation error (backport [#56353](https://github.com/frappe/erpnext/issues/56353)) ([#57058](https://github.com/frappe/erpnext/issues/57058)) ([4136ca5](4136ca55a7))
* add page numbers to print format footer ([0371e8e](0371e8eaf0))
* added permission checks on various whitelisted functions (backport [#56745](https://github.com/frappe/erpnext/issues/56745)) ([#56946](https://github.com/frappe/erpnext/issues/56946)) ([7ce1289](7ce1289c10))
* address review comments ([056195c](056195ce07))
* auto fetch serial no from previous operation output (backport to v16) ([#56861](https://github.com/frappe/erpnext/issues/56861)) ([17f2de4](17f2de42f3))
* **banking:** allow negative balance in bank statement import (backport [#56959](https://github.com/frappe/erpnext/issues/56959)) ([#57054](https://github.com/frappe/erpnext/issues/57054)) ([835e32c](835e32cec7))
* **banking:** handle blank password protected PDFs and negative amounts in CR/DR columns (backport [#56690](https://github.com/frappe/erpnext/issues/56690)) ([#56694](https://github.com/frappe/erpnext/issues/56694)) ([19c318d](19c318df68))
* **banking:** use custom renderer for translated strings and parser for rules (backport [#56643](https://github.com/frappe/erpnext/issues/56643)) ([#56647](https://github.com/frappe/erpnext/issues/56647)) ([2cb577b](2cb577b912))
* block serialized to non-serialized item change when SABB exists (backport [#56773](https://github.com/frappe/erpnext/issues/56773)) ([#56775](https://github.com/frappe/erpnext/issues/56775)) ([19d03fe](19d03fee46))
* clear stray permission message when item dashboard has no warehouse access ([a39753e](a39753ee08))
* **company:** ignore user permissions for link fields having link to `Account` and `Cost Center` (backport [#56748](https://github.com/frappe/erpnext/issues/56748)) ([#56795](https://github.com/frappe/erpnext/issues/56795)) ([003b655](003b6554c4))
* correct filter handling in Sales Person-wise Transaction Summary + tests (backport [#56783](https://github.com/frappe/erpnext/issues/56783)) ([#56908](https://github.com/frappe/erpnext/issues/56908)) ([24f0989](24f0989ac1))
* **crm_settings:** skip allowed users check when frappe crm is installed locally ([dfe4d5a](dfe4d5ab73))
* display outstanding amount using company default currency (backport [#56785](https://github.com/frappe/erpnext/issues/56785)) ([#57009](https://github.com/frappe/erpnext/issues/57009)) ([bdba4c8](bdba4c8091))
* don't attempt to create SABB for non-serialized / non-batch items (backport [#56627](https://github.com/frappe/erpnext/issues/56627)) ([#56803](https://github.com/frappe/erpnext/issues/56803)) ([8c56a5a](8c56a5ac0c))
* don't treat batch slot at FIFO queue head as qty slot ([20f6dd0](20f6dd0224))
* enforce read permission on ledger preview endpoints ([#57039](https://github.com/frappe/erpnext/issues/57039)) ([5642359](5642359c10))
* fetch payment entry reference amounts from invoice (backport [#56928](https://github.com/frappe/erpnext/issues/56928)) ([#57042](https://github.com/frappe/erpnext/issues/57042)) ([67c85ef](67c85ef0af))
* FIFO queue checks and incorrect entries filter in stock ledger reports ([3571512](3571512101)), closes [#43619](https://github.com/frappe/erpnext/issues/43619)
* **financial_statement:** render columnar financial statements instea… (backport [#56921](https://github.com/frappe/erpnext/issues/56921)) ([#57053](https://github.com/frappe/erpnext/issues/57053)) ([e6a6c13](e6a6c13355))
* for purchases do voucher based reposting (backport [#56601](https://github.com/frappe/erpnext/issues/56601)) ([#56608](https://github.com/frappe/erpnext/issues/56608)) ([e1e6176](e1e6176ddc))
* guard against missing DocType in onboarding steps patch ([#56804](https://github.com/frappe/erpnext/issues/56804)) ([f72289e](f72289e27c))
* **item-attribute:** clear attribute values when marking numeric ([374b340](374b340e73))
* **item:** error on uncommitted input and escape values in variant dialog ([04c834d](04c834d6a9))
* **item:** rework multiple variant dialog for large numeric ranges ([025d0cd](025d0cd7f3))
* make represents company field in purchase invoice ignore user permissions ([4aed77d](4aed77df0c))
* make trend report based-on and group-by column labels translatable ([b8199d8](b8199d88b6))
* map stock_qty in apply_price_list_on_item (backport [#56869](https://github.com/frappe/erpnext/issues/56869)) ([#57052](https://github.com/frappe/erpnext/issues/57052)) ([240fb2c](240fb2c4b8))
* partial delivery note against pick list (backport [#56985](https://github.com/frappe/erpnext/issues/56985)) ([#57006](https://github.com/frappe/erpnext/issues/57006)) ([20255a8](20255a8a7f))
* **patch:** moved create_company_custom_fields from `pre_model_sync` to `post_model_sync` (backport [#56962](https://github.com/frappe/erpnext/issues/56962)) ([#56965](https://github.com/frappe/erpnext/issues/56965)) ([14efd14](14efd14384))
* **payment reconciliation:** honour user permissions on accounting di… ([#56560](https://github.com/frappe/erpnext/issues/56560)) ([8eb92b8](8eb92b8b18))
* precision issue causing reconciliation error ([#54043](https://github.com/frappe/erpnext/issues/54043)) ([20df15b](20df15b3ac))
* prevent repeatable read related concurrency errors ([019b02a](019b02adcd))
* race condition and repeatable read in process pcv ([6d9f5fa](6d9f5fac76))
* recompute transferred qty before deciding work order status ([bc952f4](bc952f40d9))
* remove company default on cost center in stock entry detail ([a1fe6ce](a1fe6cee5d))
* remove incorrect Payable account_type from Customer Deposits in Philippines CoA (backport [#57018](https://github.com/frappe/erpnext/issues/57018)) ([#57061](https://github.com/frappe/erpnext/issues/57061)) ([edfbc71](edfbc71135))
* rename variant item_code/item_name when attribute abbreviation changes ([97cd1e7](97cd1e7144))
* render letter head footer in print formats ([fa4d32d](fa4d32dcdb))
* replay immutable SLE qty for serial/batch bundle valuation (backport [#56814](https://github.com/frappe/erpnext/issues/56814)) ([#56835](https://github.com/frappe/erpnext/issues/56835)) ([2264e25](2264e25cc6))
* resolve conflicts ([3aad7fe](3aad7fee24))
* restore Save button on reverse journal entry (backport [#56770](https://github.com/frappe/erpnext/issues/56770)) ([#56772](https://github.com/frappe/erpnext/issues/56772)) ([4573cd1](4573cd15a9))
* **selling:** update sales order per billed on credit note submission ([224cf19](224cf19f33))
* set conversion_rate on quotation created from customer ([26905bc](26905bc142))
* show only template items in Variant Of filter ([bebe011](bebe011636))
* skip item prices tab render for users without item price read access ([c964811](c964811293))
* skip stock reservation for opted-out production plans ([17598e2](17598e2626)), closes [#56798](https://github.com/frappe/erpnext/issues/56798)
* **stock:** backfill transferred qty for existing pick lists ([903d78c](903d78cc43))
* **stock:** link job card in stock entry created from pick list (backport [#57031](https://github.com/frappe/erpnext/issues/57031)) ([755b9cc](755b9ccbc3))
* **stock:** pick list serial batch posting date ([#57015](https://github.com/frappe/erpnext/issues/57015)) ([7e46be2](7e46be2a33)), closes [#56951](https://github.com/frappe/erpnext/issues/56951)
* **stock:** propagate project from job card to stock entry ([51d2077](51d2077eed))
* **stock:** support quality inspection for stock entry by purpose (backport [#56446](https://github.com/frappe/erpnext/issues/56446)) ([40ca3b5](40ca3b5e5d))
* update BOM operations when routing is changed ([b2e6a39](b2e6a39743))
* update events order by date asc (backport [#56963](https://github.com/frappe/erpnext/issues/56963)) ([#57056](https://github.com/frappe/erpnext/issues/57056)) ([e2fd061](e2fd061b3d))
* update modified timestamp in item.json ([f602ee0](f602ee0e73))
* use live source warehouse valuation for internal transfer purchase receipts (backport [#56431](https://github.com/frappe/erpnext/issues/56431)) ([#56677](https://github.com/frappe/erpnext/issues/56677)) ([1da28f2](1da28f2278))
* validate planned end date is not before planned start date in work order ([745baad](745baad0d1))
* validate reverse GL entries on current date under immutable ledger (backport [#56709](https://github.com/frappe/erpnext/issues/56709)) ([#56752](https://github.com/frappe/erpnext/issues/56752)) ([eea4756](eea4756ce9))
* validate template and its variant in the same Pricing Rule ([159a253](159a2538da))

### Features

* capture user persona during setup (backport [#56705](https://github.com/frappe/erpnext/issues/56705)) ([#56707](https://github.com/frappe/erpnext/issues/56707)) ([d9aa449](d9aa4491f1))
* **crm_settings:** auto-update crm sync settings on frappe crm install and uninstall ([cb2a930](cb2a930a32))
* explain FIFO allocation of fixed Discount Amount on Sales Order ([#56436](https://github.com/frappe/erpnext/issues/56436)) ([62fed1d](62fed1d562))
* **manufacturing:** create material request for raw materials from work order ([#56980](https://github.com/frappe/erpnext/issues/56980)) ([144f92d](144f92d58e))
* **sla:** filter service level agreement link by document type (backport [#56954](https://github.com/frappe/erpnext/issues/56954)) ([#56956](https://github.com/frappe/erpnext/issues/56956)) ([393b5d1](393b5d1f74))
* **stock:** support partial transfer from pick list ([af495ed](af495ed253))

### Performance Improvements

* avoid per-row Warehouse doc fetches in auto reorder job ([39b5e12](39b5e12305))
* batch bin lookups in delivery note stock update ([697e2c0](697e2c0b66))
2026-07-13 07:41:31 +00:00
Diptanil Saha
f94eee3197 Merge pull request #56931 from frappe/version-16-hotfix
chore: release v16
2026-07-13 12:09:52 +05:30
269 changed files with 65626 additions and 80506 deletions

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.26.2"
__version__ = "16.35.0"
def get_default_company(user=None):

View File

@@ -55,9 +55,6 @@ class ERPNextAddress(Address):
@frappe.whitelist()
def get_shipping_address(company, address=None):
# `select`, not `read`: Delivery, Maintenance, Purchase and Stock Manager fill in forms that ask
# for this while holding no Company `read` row. doc= so User Permissions apply to the named company.
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],

View File

@@ -24,7 +24,7 @@ def get(
heatmap_year=None,
):
if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name, check_permission="read")
chart = frappe.get_doc("Dashboard Chart", chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan
@@ -53,9 +53,6 @@ def get(
)
)
# authorise the account itself, as get_balance_on() does; doc= brings User Permissions with it
frappe.has_permission("Account", doc=account, throw=True)
if not to_date:
to_date = nowdate()
if not from_date:

View File

@@ -203,7 +203,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-04-14 18:14:42.202065",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -264,46 +264,6 @@
{
"role": "HR Manager",
"select": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -503,19 +503,12 @@ class Account(NestedSet):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.get_list(
"Account",
filters=[
["is_group", "=", 1],
["docstatus", "!=", 2],
["company", "=", filters["company"]],
[searchfield, "like", f"%{txt}%"],
],
fields=["name"],
order_by="name",
limit_start=start,
limit_page_length=page_len,
as_list=True,
return frappe.db.sql(
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = {}
and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, page_len, start),
as_list=1,
)

View File

@@ -102,8 +102,6 @@ def identify_is_group(child):
def get_chart(chart_template, existing_company=None):
chart = {}
if existing_company:
frappe.has_permission("Company", doc=existing_company, throw=True)
return get_account_tree_from_existing_company(existing_company)
elif chart_template == "Standard":

View File

@@ -235,11 +235,8 @@ def delete_accounting_dimension(doc):
frappe.clear_cache(doctype=doctype)
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def disable_dimension(doc):
# toggle_disabling rewrites a Custom Field site-wide, so demand the write that configures dimensions
frappe.has_permission("Accounting Dimension", "write", throw=True)
if frappe.in_test:
toggle_disabling(doc=doc)
else:

View File

@@ -101,7 +101,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:06:36.896195",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank",
@@ -118,14 +118,6 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
}
],
"quick_entry": 1,

View File

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

View File

@@ -60,9 +60,6 @@ def get_voucher_details(bank_guarantee_type: str, reference_name: str):
if not isinstance(reference_name, str):
raise TypeError("reference_name must be a string")
# the form is the boundary, not the referenced order: an order guard would break one of the two roles
frappe.has_permission("Bank Guarantee", throw=True)
fields_to_fetch = ["grand_total"]
if bank_guarantee_type == "Receiving":

View File

@@ -68,7 +68,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frappe.msgprint(__("Please select Bank Account"));
return;
}
frm.events.validate_dates(frm);
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
args: {
@@ -83,7 +82,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
});
frm.add_custom_button(__("Get Unreconciled Entries"), function () {
return frm.trigger("make_reconciliation_tool");
frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type(__("Get Unreconciled Entries"), null, "primary");
@@ -107,24 +106,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.trigger("get_account_opening_balance");
},
validate_dates(frm) {
const from_date = frm.doc.filter_by_reference_date
? frm.doc.from_reference_date
: frm.doc.bank_statement_from_date;
const to_date = frm.doc.filter_by_reference_date
? frm.doc.to_reference_date
: frm.doc.bank_statement_to_date;
if (from_date && to_date && from_date > to_date) {
frappe.throw(
frm.doc.filter_by_reference_date
? __("From Reference Date cannot be greater than To Reference Date")
: __("From Date cannot be greater than To Date")
);
}
},
make_reconciliation_tool(frm) {
frm.events.validate_dates(frm);
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
frm.trigger("get_cleared_balance").then(() => {

View File

@@ -10,7 +10,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import cint, create_batch, flt, getdate
from frappe.utils import cint, create_batch, flt
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
@@ -55,8 +55,6 @@ def get_bank_transactions(
all_transactions: bool = False,
):
# returns bank transactions for a bank account
validate_date_range(from_date, to_date)
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
@@ -796,7 +794,6 @@ def create_bulk_payment_entry_and_reconcile(
"deposit",
"withdrawal",
"bank_account",
"company",
"currency",
"unallocated_amount",
"date",
@@ -831,7 +828,11 @@ def create_bulk_payment_entry_and_reconcile(
"paid_from": paid_from,
"paid_to": paid_to,
"paid_amount": bank_transaction.unallocated_amount,
"base_paid_amount": bank_transaction.unallocated_amount,
"received_amount": bank_transaction.unallocated_amount,
"base_received_amount": bank_transaction.unallocated_amount,
"target_exchange_rate": 1,
"source_exchange_rate": 1,
"reference_date": bank_transaction.date,
"posting_date": bank_transaction.date,
"reference_no": (bank_transaction.reference_number or bank_transaction.description or "")[
@@ -840,8 +841,6 @@ def create_bulk_payment_entry_and_reconcile(
}
)
set_multi_currency_amounts(payment_entry_doc)
payment_entry_doc.insert()
payment_entry_doc.submit()
@@ -880,7 +879,6 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
"doctype": "Payment Entry",
}
)
set_multi_currency_amounts(payment_entry)
payment_entry.insert()
payment_entry.submit()
transaction = reconcile_vouchers(
@@ -903,33 +901,6 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
}
def set_multi_currency_amounts(pe):
"""Set real exchange rates when the bank and party accounts differ in currency."""
company_currency = frappe.get_cached_value("Company", pe.company, "default_currency")
pe.paid_from_account_currency = frappe.get_cached_value("Account", pe.paid_from, "account_currency")
pe.paid_to_account_currency = frappe.get_cached_value("Account", pe.paid_to, "account_currency")
pe.source_exchange_rate = (
1.0
if pe.paid_from_account_currency == company_currency
else get_exchange_rate(pe.paid_from_account_currency, company_currency, pe.posting_date)
)
pe.target_exchange_rate = (
1.0
if pe.paid_to_account_currency == company_currency
else get_exchange_rate(pe.paid_to_account_currency, company_currency, pe.posting_date)
)
# derive the party-side amount from the authoritative bank-side amount; Payment Entry books any
# rounding residual to Exchange Gain/Loss during validation (set_exchange_gain_loss)
if pe.payment_type == "Receive" and pe.source_exchange_rate:
base_amount = flt(pe.received_amount) * pe.target_exchange_rate
pe.paid_amount = flt(base_amount / pe.source_exchange_rate, pe.precision("paid_amount"))
elif pe.payment_type == "Pay" and pe.target_exchange_rate:
base_amount = flt(pe.paid_amount) * pe.source_exchange_rate
pe.received_amount = flt(base_amount / pe.target_exchange_rate, pe.precision("received_amount"))
@frappe.whitelist(methods=["GET"])
def search_for_transfer_transaction(transaction_id: str | int):
"""
@@ -998,7 +969,6 @@ def auto_reconcile_vouchers(
from_reference_date: str | date | None = None,
to_reference_date: str | date | None = None,
):
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
bank_transactions = get_bank_transactions(bank_account)
if len(bank_transactions) > 10:
@@ -1117,7 +1087,6 @@ def get_linked_payments(
to_reference_date: str | date | None = None,
):
# get all matching payments for a bank transaction
validate_date_range(from_date, to_date, filter_by_reference_date, from_reference_date, to_reference_date)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
@@ -1137,23 +1106,6 @@ def get_linked_payments(
return subtract_allocations(gl_account, matching)
def validate_date_range(
from_date,
to_date,
filter_by_reference_date=False,
from_reference_date=None,
to_reference_date=None,
):
if cint(filter_by_reference_date):
from_date, to_date = from_reference_date, to_reference_date
message = _("From Reference Date cannot be greater than To Reference Date")
else:
message = _("From Date cannot be greater than To Date")
if from_date and to_date and getdate(from_date) > getdate(to_date):
frappe.throw(message)
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []

View File

@@ -2,16 +2,12 @@
# See license.txt
from unittest.mock import patch
import frappe
from frappe import qb
from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
auto_reconcile_vouchers,
create_bulk_payment_entry_and_reconcile,
create_payment_entry_and_reconcile,
get_auto_reconcile_message,
get_bank_transactions,
get_linked_payments,
@@ -20,8 +16,6 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.tests.utils import ERPNextTestSuite
RATE_METHOD = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_exchange_rate"
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
@@ -137,33 +131,6 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
names = [t.name for t in get_bank_transactions(self.bank_account, to_date=add_days(today(), -1))]
self.assertEqual(names, [])
def test_rejects_reversed_date_ranges(self):
from_date, to_date = today(), add_days(today(), -1)
with self.assertRaisesRegex(frappe.ValidationError, "From Date cannot be greater than To Date"):
get_bank_transactions(self.bank_account, from_date, to_date)
with self.assertRaisesRegex(
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
):
auto_reconcile_vouchers(
self.bank_account,
filter_by_reference_date=True,
from_reference_date=from_date,
to_reference_date=to_date,
)
transaction = self.make_bank_transaction(date=today())
with self.assertRaisesRegex(
frappe.ValidationError, "From Reference Date cannot be greater than To Reference Date"
):
get_linked_payments(
transaction.name,
["payment_entry"],
filter_by_reference_date=True,
from_reference_date=from_date,
to_reference_date=to_date,
)
def test_deposit_matches_amount_received_in_bank_account(self):
# money leaves another bank account and lands here minus a charge, so the two sides differ
payment = frappe.get_doc(
@@ -232,117 +199,3 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
self.assertIn("1 Transaction Partially Reconciled", singular)
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
self.assertIn("2 Transactions Partially Reconciled", plural)
def test_multi_currency_pay_converts_and_balances(self):
# withdrawal from an INR bank paying a USD supplier; rate 3.0 makes 100/3 non-exact
self.enable_multi_currency_setup()
pe = self.reconcile_new_payment(
self.make_multi_currency_txn(withdrawal=100),
payment_type="Pay",
party_type="Supplier",
party=self.supplier,
party_account=self.creditors_usd,
paid_from=self.bank,
paid_to=self.creditors_usd,
rate=3.0,
)
self.assertEqual(pe.docstatus, 1) # submits despite the rounding residual
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (1.0, 3.0))
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33)) # bank side kept, 100/3
self.assertEqual(pe.difference_amount, 0)
# Payment Entry auto-books the rounding residual to Exchange Gain/Loss
self.assertTrue(pe.deductions[0].is_exchange_gain_loss)
self.assertEqual(pe.deductions[0].amount, 0.01) # 100 - 33.33 * 3
def test_multi_currency_receive_converts_and_balances(self):
# deposit into an INR bank from a USD customer; the party side must convert
self.enable_multi_currency_setup()
pe = self.reconcile_new_payment(
self.make_multi_currency_txn(deposit=100),
payment_type="Receive",
party_type="Customer",
party=self.customer,
party_account=self.debtors_usd,
paid_from=self.debtors_usd,
paid_to=self.bank,
rate=3.0,
)
self.assertEqual(pe.docstatus, 1)
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (3.0, 1.0))
self.assertEqual((pe.received_amount, pe.paid_amount), (100, 33.33)) # bank side kept, 100/3
self.assertEqual(pe.difference_amount, 0)
def test_multi_currency_bulk_pay_converts_and_balances(self):
# the bulk path builds the Payment Entry itself, so it must convert too
self.enable_multi_currency_setup()
txn = self.make_multi_currency_txn(withdrawal=100)
with patch(RATE_METHOD, return_value=3.0):
result = create_bulk_payment_entry_and_reconcile(
[txn.name], "Supplier", self.supplier, self.creditors_usd
)
pe = frappe.get_doc("Payment Entry", result[0]["payment_entry"].name)
self.assertEqual(pe.docstatus, 1)
self.assertEqual(pe.target_exchange_rate, 3.0)
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33))
self.assertEqual(pe.difference_amount, 0)
def enable_multi_currency_setup(self):
# USD party/accounts + a company gain/loss account to absorb rounding residuals
self.company_abbr = "_TC"
self.create_supplier(supplier_name="_Test Supplier USD", currency="USD")
self.create_customer(customer_name="_Test Customer USD", currency="USD")
self.create_usd_payable_account()
self.create_usd_receivable_account()
self.set_party_account("Supplier", self.supplier, self.creditors_usd)
if not frappe.db.get_value("Company", self.company, "exchange_gain_loss_account"):
frappe.db.set_value(
"Company", self.company, "exchange_gain_loss_account", "Exchange Gain/Loss - _TC"
)
def set_party_account(self, party_type, party, account):
doc = frappe.get_doc(party_type, party)
if not any(row.company == self.company for row in doc.accounts):
doc.append("accounts", {"company": self.company, "account": account})
doc.save()
def make_multi_currency_txn(self, withdrawal=0, deposit=0):
return (
frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": today(),
"withdrawal": withdrawal,
"deposit": deposit,
"bank_account": self.bank_account,
"currency": "INR",
"reference_number": "TEST-FX-REF",
}
)
.save()
.submit()
)
def reconcile_new_payment(
self, txn, *, payment_type, party_type, party, party_account, paid_from, paid_to, rate
):
# mimics the /banking frontend, which sends a hardcoded 1:1 rate
payment_entry_doc = {
"payment_type": payment_type,
"company": self.company,
"party_type": party_type,
"party": party,
"party_account": party_account,
"paid_from": paid_from,
"paid_to": paid_to,
"paid_amount": txn.unallocated_amount,
"received_amount": txn.unallocated_amount,
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"posting_date": today(),
"reference_no": f"TEST-FX-{payment_type}",
"reference_date": today(),
}
with patch(RATE_METHOD, return_value=rate):
result = create_payment_entry_and_reconcile(txn.name, payment_entry_doc)
return frappe.get_doc("Payment Entry", result["payment_entry"].name)

View File

@@ -436,9 +436,6 @@ def get_import_logs(docname: str):
@frappe.whitelist()
def upload_bank_statement(**args):
# The only caller routes into a new Bank Statement Import form, so `create` is the right to require.
frappe.has_permission("Bank Statement Import", "create", throw=True)
args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import")

View File

@@ -11,10 +11,6 @@ from frappe.utils.dateutils import parse_date
@frappe.whitelist()
def upload_bank_statement():
# Parsing a statement is the first step of creating Bank Transactions from it. Both functions here
# are reached only over HTTP, so there is no in-process caller to break.
frappe.has_permission("Bank Transaction", "create", throw=True)
if getattr(frappe, "uploaded_file", None):
with open(frappe.uploaded_file, "rb") as upfile:
fcontent = upfile.read()
@@ -38,13 +34,8 @@ def upload_bank_statement():
return {"columns": columns, "data": data}
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def create_bank_entries(columns, data, bank_account):
# insert()/submit() enforce this per document, but only after the loop has read each Bank Account
# and written an Error Log per rejected row. Check once up front.
frappe.has_permission("Bank Transaction", "create", throw=True)
frappe.has_permission("Bank Account", doc=bank_account, throw=True)
header_map = get_header_mapping(columns, bank_account)
success = 0

View File

@@ -8,7 +8,6 @@ from functools import reduce
import frappe
from frappe import _
from frappe.core.doctype.file.utils import find_file_by_url
from frappe.desk.form.linked_with import get_linked_fields
from frappe.model.document import Document
from frappe.utils import cint, cstr
@@ -59,8 +58,6 @@ def validate_columns(data):
@frappe.whitelist()
def validate_company(company):
frappe.has_permission("Chart of Accounts Importer", throw=True)
parent_company, allow_account_creation_against_child_company = frappe.get_cached_value(
"Company", company, ["parent_company", "allow_account_creation_against_child_company"]
)
@@ -113,10 +110,7 @@ def import_coa(file_name, company):
def get_file(file_name):
file_doc = find_file_by_url(file_name)
if not file_doc:
raise frappe.PermissionError
file_doc = frappe.get_doc("File", {"file_url": file_name})
parts = file_doc.get_extension()
extension = parts[1]
extension = extension.lstrip(".")
@@ -183,8 +177,6 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
"""called by tree view (to fetch node's children)"""
frappe.has_permission("Chart of Accounts Importer", throw=True)
file_doc, extension = get_file(file_name)
parent = None if parent == _("All Accounts") else parent
@@ -332,8 +324,6 @@ def build_response_as_excel(writer):
@frappe.whitelist()
def download_template(file_type, template_type, company):
frappe.has_permission("Chart of Accounts Importer", throw=True)
writer = get_template(template_type, company)
if file_type == "CSV":
@@ -386,6 +376,7 @@ def get_sample_template(writer, company):
return writer
@frappe.whitelist()
def validate_accounts(file_doc, extension):
if extension == "csv":
accounts = generate_data_from_csv(file_doc, as_dict=True)

View File

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

View File

@@ -5,10 +5,7 @@ frappe.ui.form.on("Coupon Code", {
setup: function (frm) {
frm.set_query("pricing_rule", function () {
return {
filters: {
coupon_code_based: 1,
disable: 0,
},
filters: [["Pricing Rule", "coupon_code_based", "=", "1"]],
};
});
},

View File

@@ -125,7 +125,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-11-19 16:35:11.836441",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Coupon Code",
@@ -179,14 +179,6 @@
"role": "Website Manager",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
}
],
"sort_field": "creation",

View File

@@ -42,23 +42,7 @@ class CouponCode(Document):
self.coupon_code = frappe.generate_hash()[:10].upper()
def validate(self):
self.validate_from_to_dates("valid_from", "valid_upto")
self.validate_pricing_rule()
if self.coupon_type == "Gift Card":
self.maximum_use = 1
if not self.customer:
frappe.throw(_("Please select the customer."))
def validate_pricing_rule(self):
if not self.pricing_rule or self.from_external_ecomm_platform:
return
# Allow existing coupons to be updated after their pricing rule is disabled.
if not (
self.has_value_changed("pricing_rule") or self.has_value_changed("from_external_ecomm_platform")
):
return
if frappe.db.get_value("Pricing Rule", self.pricing_rule, "disable"):
frappe.throw(_("Pricing Rule {0} is disabled").format(frappe.bold(self.pricing_rule)))

View File

@@ -113,43 +113,6 @@ class TestCouponCode(ERPNextTestSuite):
def setUp(self):
test_create_test_data()
def test_disabled_pricing_rule_validation(self):
coupon = frappe.get_doc("Coupon Code", "SAVE30")
rule = frappe.get_doc("Pricing Rule", coupon.pricing_rule)
rule.disable = 1
rule.save()
with self.subTest("new coupon cannot select a disabled rule"):
new_coupon = frappe.copy_doc(coupon)
new_coupon.coupon_name = "Festival Savings"
new_coupon.coupon_code = "FESTSAVE"
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
new_coupon.insert()
with self.subTest("existing coupon can retain a disabled rule"):
coupon.description = "Offer paused"
coupon.save()
coupon.reload()
self.assertEqual(coupon.description, "Offer paused")
self.assertEqual(coupon.pricing_rule, rule.name)
with self.subTest("existing coupon cannot switch to a disabled rule"):
disabled_rule = frappe.copy_doc(rule)
disabled_rule.insert()
coupon.reload()
coupon.pricing_rule = disabled_rule.name
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
coupon.save()
coupon.reload()
self.assertEqual(coupon.pricing_rule, rule.name)
def test_cannot_save_coupon_with_reversed_validity_dates(self):
coupon = frappe.get_doc("Coupon Code", "SAVE30")
coupon.valid_from = "2026-09-17"
coupon.valid_upto = "2026-09-02"
with self.assertRaises(frappe.exceptions.InvalidDates):
coupon.save()
def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)

View File

@@ -20,7 +20,7 @@
],
"icon": "fa fa-book",
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:09:44.514241",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Finance Book",
@@ -55,18 +55,6 @@
"report": 1,
"role": "Auditor",
"share": 1
},
{
"role": "HR Manager",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -82,7 +82,7 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-05-27 17:29:55.560840",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year",
@@ -131,10 +131,6 @@
{
"read": 1,
"role": "Auditor"
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"show_name_in_global_search": 1,

View File

@@ -57,7 +57,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:09:55.573483",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -95,54 +95,6 @@
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Item Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
},
{
"role": "Stock User",
"select": 1
}
],
"show_name_in_global_search": 1,

View File

@@ -1356,10 +1356,6 @@ def get_default_bank_cash_account(
):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
# `select`, not `read`: this also runs server-side from get_payment_entry, and Auditor/HR User/Desk
# User hold only the select row on Company. doc= brings User Permissions to bear.
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
if mode_of_payment:
account = get_bank_cash_account(mode_of_payment, company).get("account")
@@ -1388,10 +1384,6 @@ def get_default_bank_cash_account(
account = account_list[0].name
if account:
# `fetch_balance` is caller supplied, so authorise the account here rather than relying on
# get_balance_on(), which only checks on the branch that reads a balance.
frappe.has_permission("Account", doc=account, throw=True)
account_details = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"], as_dict=1
)
@@ -1576,39 +1568,30 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
if not frappe.db.has_column("Journal Entry", searchfield):
return []
account = filters.get("account")
JournalEntry = frappe.qb.DocType("Journal Entry")
JournalEntryAccount = frappe.qb.DocType("Journal Entry Account")
query = (
frappe.qb.from_(JournalEntry)
.join(JournalEntryAccount)
.on(JournalEntryAccount.parent == JournalEntry.name)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.remark)
.where(JournalEntryAccount.account == filters.get("account"))
.where(JournalEntryAccount.reference_type.isnull() | (JournalEntryAccount.reference_type == ""))
.where(JournalEntry.docstatus == 1)
.where(JournalEntry[searchfield].like(f"%{txt}%"))
.orderby(JournalEntry.name, order=frappe.qb.desc)
.limit(page_len)
.offset(start)
)
party = filters.get("party")
if party:
query = query.where(JournalEntryAccount.party == party)
else:
query = query.where(JournalEntryAccount.party.isnull() | (JournalEntryAccount.party == ""))
# each names one value: a list would be read as a filter operator and widen the search.
for value in (account, party):
if value and not isinstance(value, str):
frappe.throw(_("Invalid filter"), frappe.PermissionError)
# get_list applies the permission query conditions; the child-table filter resolves the check to `read`
je_filters = [
["docstatus", "=", 1],
[searchfield, "like", f"%{txt}%"],
["Journal Entry Account", "account", "=", account],
["Journal Entry Account", "reference_type", "is", "not set"],
]
je_filters.append(
["Journal Entry Account", "party", "=", party]
if party
else ["Journal Entry Account", "party", "is", "not set"]
)
return frappe.get_list(
"Journal Entry",
filters=je_filters,
fields=["name", "posting_date", "remark"],
order_by="name desc",
limit_start=start,
limit_page_length=page_len,
as_list=True,
# one row per entry, not per matching account row. group_by rather than distinct: frappe
# drops ORDER BY from a distinct query on postgres, which would lose the ordering above.
group_by="name",
)
return query.run()
@frappe.whitelist()
@@ -1827,10 +1810,6 @@ def make_reverse_journal_entry(source_name, target_doc=None):
def post_process(source, target):
target.reversal_of = source.name
target.naming_series = source.naming_series
if source.voucher_type == "Bank Entry":
target.cheque_no = source.cheque_no
target.cheque_date = source.cheque_date
doclist = get_mapped_doc(
"Journal Entry",

View File

@@ -154,7 +154,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:10:03.361383",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Loyalty Program",
@@ -171,14 +171,6 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -96,8 +96,6 @@ def get_loyalty_program_details_with_points(
include_expired_entry=False,
current_transaction_amount=0,
):
frappe.has_permission("Customer", doc=customer, throw=True)
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
loyalty_details = get_loyalty_details(
@@ -128,10 +126,6 @@ def get_loyalty_program_details(
silent=False,
include_expired_entry=False,
):
# The customer is what the caller is entitled to, not the programme: a check on Loyalty Program
# would be read-only to System Manager and deny every role that fills in the calling forms.
frappe.has_permission("Customer", doc=customer, throw=True)
lp_details = frappe._dict()
if not loyalty_program:
@@ -153,13 +147,6 @@ def get_loyalty_program_details(
@frappe.whitelist()
def get_redeemption_factor(loyalty_program=None, customer=None):
# both call sites send only `loyalty_program`, so the calling form is the boundary; the customer branch stays guarded
if not (frappe.has_permission("Sales Invoice") or frappe.has_permission("POS Invoice")):
frappe.throw(_("Not permitted"), frappe.PermissionError)
if customer:
frappe.has_permission("Customer", doc=customer, throw=True)
customer_loyalty_program = None
if not loyalty_program:
customer_loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program")

View File

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

View File

@@ -46,7 +46,7 @@
"icon": "fa fa-bar-chart",
"idx": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:10:05.873547",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Monthly Distribution",
@@ -69,10 +69,6 @@
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"sort_field": "creation",

View File

@@ -57,56 +57,26 @@ class PaymentOrder(Document):
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
def _readable_payment_order(filters):
"""Authorise the parent before reading its rows.
A child table carries no permissions of its own, so a read of it has to be authorised on the
Payment Order the rows belong to.
"""
parent = filters.get("parent")
if not parent or not frappe.db.exists("Payment Order", parent):
return None
ptype = "select" if frappe.only_has_select_perm("Payment Order") else "read"
frappe.has_permission("Payment Order", ptype, doc=parent, throw=True)
return parent
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
if not _readable_payment_order(filters):
return []
return frappe.get_all(
"Payment Order Reference",
filters={"parent": filters.get("parent"), "mode_of_payment": ["like", f"%{txt}%"]},
fields=["mode_of_payment"],
limit_start=start,
limit_page_length=page_len,
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
as_list=True,
return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
if not _readable_payment_order(filters):
return []
return frappe.get_all(
"Payment Order Reference",
filters={
"parent": filters.get("parent"),
"supplier": ["like", f"%{txt}%"],
"payment_reference": ["is", "not set"],
},
fields=["supplier"],
limit_start=start,
limit_page_length=page_len,
order_by="", # match the original query (no ORDER BY); avoid get_all's default sort
as_list=True,
return frappe.db.sql(
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
)

View File

@@ -116,7 +116,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:10:11.511137",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Term",
@@ -157,30 +157,6 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -254,13 +254,6 @@ class POSClosingEntry(StatusUpdater):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
pos_profile = filters.get("parent")
if not pos_profile or not frappe.db.exists("POS Profile", pos_profile):
return []
ptype = "select" if frappe.only_has_select_perm("POS Profile") else "read"
frappe.has_permission("POS Profile", ptype, doc=pos_profile, throw=True)
cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1)
return [c for c in cashiers_list]

View File

@@ -1637,7 +1637,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-08-12 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1680,14 +1680,6 @@
"permlevel": 1,
"read": 1,
"role": "All"
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -899,19 +899,6 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
# select-or-read on POS Profile: a bare check defaults to `read`, which Sales Manager lacks.
# The profile is the only boundary that fits -- Item and Bin read both exclude Accounts Manager.
ptype = "select" if frappe.only_has_select_perm("POS Profile") else "read"
frappe.has_permission("POS Profile", ptype, throw=True)
# the caller picks the warehouse when allow_warehouse_change is set; costs nobody who has no
# Warehouse User Permission
from frappe.permissions import get_allowed_docs_for_doctype, get_user_permissions
if warehouse_permissions := get_user_permissions(frappe.session.user).get("Warehouse"):
allowed_warehouses = get_allowed_docs_for_doctype(warehouse_permissions, "POS Invoice")
if allowed_warehouses and warehouse not in allowed_warehouses:
frappe.throw(_("Not permitted for {0}").format(warehouse), frappe.PermissionError)
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)

View File

@@ -9,7 +9,6 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.query_builder import DocType
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue, is_job_enqueued
from frappe.utils.scheduler import is_scheduler_inactive
@@ -17,6 +16,7 @@ from frappe.utils.scheduler import is_scheduler_inactive
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
class POSInvoiceMergeLog(Document):
@@ -214,8 +214,6 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
reversed_rows = get_reversed_rows([doc.return_against for doc in data if doc.is_return])
for doc in data:
old_new_item_map = frappe._dict()
old_new_tax_map = frappe._dict()
@@ -240,13 +238,9 @@ class POSInvoiceMergeLog(Document):
si_item.pos_invoice = doc.name
si_item.pos_invoice_item = item.name
if doc.is_return:
reversed_row = reversed_rows.get(item.pos_invoice_item) or frappe._dict()
si_item.sales_invoice_item = reversed_row.get("name")
# quote the rate of the row being reversed: rounding an invoice-level discount
# can leave a return's net rate a minor unit above the sale's, and
# validate_returned_items refuses a return priced above its original
if si_item.sales_invoice_item:
si_item.rate = reversed_row.rate
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
doc.return_against, item.pos_invoice_item
)
if item.serial_and_batch_bundle:
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
items.append(si_item)
@@ -438,28 +432,6 @@ class POSInvoiceMergeLog(Document):
si.cancel()
def get_reversed_rows(return_against):
"""Rows of the consolidated sales these returns reverse, keyed by the POS invoice row."""
if not return_against:
return {}
sales_invoice = DocType("Sales Invoice")
sales_invoice_item = DocType("Sales Invoice Item")
rows = (
frappe.qb.from_(sales_invoice)
.from_(sales_invoice_item)
.select(sales_invoice_item.name, sales_invoice_item.rate, sales_invoice_item.pos_invoice_item)
.where(
(sales_invoice.name == sales_invoice_item.parent)
& (sales_invoice.is_return == 0)
& (sales_invoice_item.pos_invoice.isin(return_against))
)
).run(as_dict=True)
return {row.pos_invoice_item: row for row in rows}
def get_all_unconsolidated_invoices():
filters = {
"consolidated_invoice": ["in", ["", None]],

View File

@@ -1,10 +1,8 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
from contextlib import contextmanager
import frappe
from frappe.utils import flt
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
set_default_account_for_mode_of_payment,
@@ -23,67 +21,6 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestSuite
@contextmanager
def rounding_method(method):
"""System Settings is also cached on frappe.local, so that copy has to go as well."""
previous = frappe.db.get_single_value("System Settings", "rounding_method")
try:
frappe.db.set_single_value("System Settings", "rounding_method", method)
frappe.local.system_settings = None
yield
finally:
frappe.db.set_single_value("System Settings", "rounding_method", previous)
frappe.local.system_settings = None
def sell_over_the_counter(lines, discount_percentage=0):
item_code, qty, rate = lines[0]
sale = create_pos_invoice(item_code=item_code, qty=qty, rate=rate, do_not_save=True)
for item_code, qty, rate in lines[1:]:
sale.append(
"items",
{
"item_code": item_code,
"qty": qty,
"rate": rate,
"price_list_rate": rate,
"warehouse": "_Test Warehouse - _TC",
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC",
},
)
if discount_percentage:
sale.apply_discount_on = "Net Total"
sale.additional_discount_percentage = discount_percentage
sale.run_method("calculate_taxes_and_totals")
payable = sale.rounded_total or sale.grand_total
sale.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": payable})
sale.paid_amount = sale.base_paid_amount = payable
sale.insert()
sale.submit()
return sale
def refund_over_the_counter(sale, qty=None):
"""Hand back every line of `sale`, `qty` of each when fewer units come back."""
note = make_sales_return(sale.name)
if qty is not None:
for item in note.items:
item.qty = qty
note.run_method("calculate_taxes_and_totals")
refundable = note.rounded_total or note.grand_total
note.payments[0].amount = refundable
for spare in note.payments[1:]:
spare.amount = 0
note.paid_amount = note.base_paid_amount = refundable
note.insert()
note.submit()
return note
class TestPOSInvoiceMergeLog(ERPNextTestSuite):
def setUp(self):
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
@@ -542,81 +479,3 @@ class TestPOSInvoiceMergeLog(ERPNextTestSuite):
"POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}, "company"
)
self.assertEqual(pos_merge_log_company, closing_entry.company)
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_consolidating_returns_priced_off_a_rounded_invoice_discount(self):
"""A return works out its own share of an invoice-level discount, so rounding can leave
it a minor unit above the sale's, and validate_returned_items then refuses it.
Every shape that reaches a consolidated credit note goes through one closing entry:
a split landing on a half minor unit, the same item on two rows so the rows can only
be paired through sales_invoice_item, fewer units coming back than went out, and — as
a control — a sale with no invoice-level discount to split at all.
"""
for item_code in ("_Test Item", "_Test Item 2"):
make_stock_entry(to_warehouse="_Test Warehouse - _TC", item_code=item_code, rate=100, qty=40)
with rounding_method("Banker's Rounding (legacy)"):
tied = sell_over_the_counter(
[("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)], discount_percentage=25
)
repeated = sell_over_the_counter(
[("_Test Item", 1, 42.86), ("_Test Item", 1, 57.14)], discount_percentage=25
)
oversold = sell_over_the_counter(
[("_Test Item", 3, 42.86), ("_Test Item 2", 3, 57.14)], discount_percentage=25
)
undiscounted = sell_over_the_counter([("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)])
# the sale and the return really do round the split apart
self.assertEqual(
{item.item_code: item.net_rate for item in tied.items},
{"_Test Item": 32.15, "_Test Item 2": 42.85},
)
returns = [
refund_over_the_counter(tied),
refund_over_the_counter(repeated),
refund_over_the_counter(oversold, qty=-1),
refund_over_the_counter(undiscounted),
]
self.assertEqual(
{item.item_code: item.net_rate for item in returns[0].items},
{"_Test Item": 32.14, "_Test Item 2": 42.86},
)
self.make_closing_entry()
for pos_invoice in [tied, repeated, oversold, undiscounted, *returns]:
pos_invoice.load_from_db()
self.assertTrue(
frappe.db.exists("Sales Invoice", pos_invoice.consolidated_invoice),
f"{pos_invoice.name} was not consolidated",
)
self.assertEqual(
frappe.db.get_value("Sales Invoice", pos_invoice.consolidated_invoice, "outstanding_amount"),
0,
)
for note in returns:
# no returned row may be priced above the row it reverses
for row in frappe.get_all(
"Sales Invoice Item",
filters={"parent": note.consolidated_invoice},
fields=["item_code", "rate", "sales_invoice_item"],
):
self.assertTrue(row.sales_invoice_item, f"{row.item_code} lost its link to the sale")
sold_rate = frappe.db.get_value("Sales Invoice Item", row.sales_invoice_item, "rate")
self.assertLessEqual(row.rate, sold_rate)
# returns for one customer land on a single credit note, which still adds up to
# everything handed back over the counter
refunded = {}
for note in returns:
refunded[note.consolidated_invoice] = refunded.get(note.consolidated_invoice, 0) + flt(
note.grand_total
)
for consolidated_name, handed_back in refunded.items():
self.assertEqual(
flt(frappe.db.get_value("Sales Invoice", consolidated_name, "grand_total"), 2),
flt(handed_back, 2),
)

View File

@@ -570,7 +570,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-05-26 12:07:48.597412",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@@ -594,10 +594,6 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"role": "Sales Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -108,7 +108,7 @@ frappe.ui.form.on("Pricing Rule", {
</td></tr>
</table>`;
frm.get_field("pricing_rule_help").html(help_content);
frm.set_df_property("pricing_rule_help", "options", help_content);
frm.events.set_options_for_applicable_for(frm);
frm.trigger("toggle_reqd_apply_on");
},

View File

@@ -12,29 +12,6 @@ from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import cint, flt
# the transactions the pricing engine is called for, from transaction.js and the POS
PRICING_TRANSACTION_DOCTYPES = frozenset(
{
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
"Material Request",
# these three also extend a controller that calls the pricing engine: BOM and BOM Creator
# through TransactionController, Request for Quotation through BuyingController
"BOM",
"BOM Creator",
"Request for Quotation",
# no client sends this one, but set_transaction_type below still branches on it
"Opportunity",
}
)
apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
other_fields = ["other_item_code", "other_item_group", "other_brand"]
@@ -387,18 +364,6 @@ def apply_pricing_rule(args, doc=None):
args = frappe._dict(args)
# The transaction being priced decides who may price it; doc= where the caller named one.
# An allow-list, not a type check: any readable doctype would otherwise satisfy has_permission.
transaction_doctype = args.get("doctype")
if transaction_doctype not in PRICING_TRANSACTION_DOCTYPES:
frappe.throw(_("Invalid doctype"), frappe.PermissionError)
transaction_name = args.get("name")
if not isinstance(transaction_name, str) or not frappe.db.exists(transaction_doctype, transaction_name):
transaction_name = None
frappe.has_permission(transaction_doctype, doc=transaction_name, throw=True)
set_transaction_type(args)
# list of dictionaries
@@ -757,18 +722,14 @@ def set_transaction_type(pricing_ctx: frappe._dict) -> None:
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
if filters.get("apply_on") == "Item Code":
item_filters = [["name", "=", filters.get("value")]]
else:
item_filters = [[frappe.scrub(filters.get("apply_on")), "=", filters.get("value")]]
items = frappe.get_list("Item", filters=item_filters, pluck="name")
if not items:
return []
items = [filters.get("value")]
if filters.get("apply_on") != "Item Code":
field = frappe.scrub(filters.get("apply_on"))
items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get("value")})]
return frappe.get_all(
"UOM Conversion Detail",
filters={"parent": ("in", items), "parenttype": "Item", "uom": ("like", f"{txt}%")},
filters={"parent": ("in", items), "uom": ("like", f"{txt}%")},
fields=["uom"],
as_list=1,
distinct=True,

View File

@@ -133,16 +133,14 @@ def initialize_parallel_threads(docname: str):
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Completed")
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def start_pcv_processing(docname: str):
# checked before the status is read: otherwise an unentitled caller learns it from whether this throws.
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.has_permission("Process Period Closing Voucher", "write", doc=docname, throw=True)
initialize_parallel_threads(docname)
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def pause_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
@@ -159,7 +157,7 @@ def pause_pcv_processing(docname: str):
qb.update(ppcvd).set(ppcvd.status, "Paused").where(ppcvd.name.isin(queued_dates)).run()
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def cancel_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="cancel", doc=docname, throw=True)
@@ -175,7 +173,7 @@ def cancel_pcv_processing(docname: str):
qb.update(ppcvd).set(ppcvd.status, "Cancelled").where(ppcvd.name.isin(queued_dates)).run()
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def resume_pcv_processing(docname: str):
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
@@ -260,11 +258,8 @@ def get_gle_for_closing_account(pcv, dimension_balance, dimensions):
return gl_entry
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def schedule_next_date(docname: str):
# marks a row Running and enqueues a long job, so it needs the same write check as the sibling controls
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
timeout = frappe.db.get_single_value("Accounts Settings", "pcv_job_timeout") or 3600
ppcvd = qb.DocType("Process Period Closing Voucher Detail")

View File

@@ -319,13 +319,6 @@ def get_html(doc, filters, entry, col, res, ageing):
from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0)
# render letter head content as a template so its Jinja resolves against the doc
if letter_head.get("content"):
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
letter_head["content"] = frappe.render_template(letter_head["content"], {"doc": doc})
if letter_head.get("footer"):
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
letter_head["footer"] = frappe.render_template(letter_head["footer"], {"doc": doc})
html = frappe.render_template(
template_path,
{

View File

@@ -1704,7 +1704,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-08-12 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1757,18 +1757,6 @@
"read": 1,
"role": "Accounts Manager",
"write": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -77,7 +77,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:10:26.945131",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
@@ -104,22 +104,6 @@
{
"read": 1,
"role": "Purchase User"
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "Accounts User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"show_title_field_in_link": 1,

View File

@@ -517,10 +517,6 @@ def validate_docs_for_voucher_types(doc_voucher_types):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
# Reached only from this System-Manager-only doctype's own form, so the form is the boundary.
# `filters` is passed straight to db.get_all, which is why reaching it has to be gated.
frappe.has_permission("Repost Accounting Ledger", throw=True)
if txt:
filters.update({"document_type": ("like", f"%{txt}%")})

View File

@@ -501,7 +501,6 @@ class SalesInvoice(SellingController):
self.validate_standalone_serial_nos_customer()
self.update_stock_reservation_entries()
self.update_stock_ledger()
self.validate_produced_serial_nos_against_reservation()
self.split_asset_based_on_sale_qty()
@@ -2861,28 +2860,9 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
@frappe.whitelist()
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
# The only two targets and reference fields the callers use. Stating them rejects a caller-supplied
# doctype that would otherwise be filtered on a column it does not have.
reference_fields = {
"Purchase Invoice": ("inter_company_invoice_reference", "Sales Invoice", "sales_invoice_item"),
"Purchase Order": ("inter_company_order_reference", "Sales Order", "sales_order_item"),
}
if doctype not in reference_fields:
frappe.throw(_("Invalid doctype {0}").format(doctype), frappe.PermissionError)
reference_field, source_doctype, expected_fieldname = reference_fields[doctype]
# The targets belong to the counterpart company and the caller may legitimately not read them, so
# the source document decides access. doc= brings User Permissions in.
frappe.has_permission(source_doctype, doc=reference_name, throw=True)
# `reference_fieldname` becomes a selected column and the result key, so it has to be this target's
# own reference field: any other item-table column would be returned from unauthorised rows.
if reference_fieldname != expected_fieldname:
frappe.throw(
_("{0} is not a valid reference field for {1}").format(reference_fieldname, doctype),
frappe.ValidationError,
)
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
filters = {
reference_field: reference_name,

View File

@@ -46,7 +46,7 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.stock.get_item_details import get_item_tax_map
from erpnext.stock.utils import _get_incoming_rate, get_stock_balance
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
from erpnext.tests.utils import ERPNextTestSuite
@@ -3157,7 +3157,7 @@ class TestSalesInvoice(ERPNextTestSuite):
rate = 0.0
for d in si.get("items"):
rate = _get_incoming_rate(
rate = get_incoming_rate(
{
"item_code": d.item_code,
"warehouse": d.warehouse,
@@ -5296,34 +5296,6 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
@ERPNextTestSuite.change_settings("Stock Settings", {"enable_stock_reservation": 1})
def test_update_stock_restricted_to_reserved_produced_serial_nos(self):
from erpnext.selling.doctype.sales_order.sales_order import (
make_sales_invoice as make_si_from_so,
)
from erpnext.stock.doctype.delivery_note.test_delivery_note import (
make_so_with_reserved_produced_serial_no,
)
so, reserved, unreserved = make_so_with_reserved_produced_serial_no()
def make_si(serial_no):
si = make_si_from_so(so.name)
si.update_stock = 1
si.items[0].warehouse = so.items[0].warehouse
si.items[0].use_serial_batch_fields = 1
si.items[0].serial_no = serial_no
return si.save()
frappe.db.savepoint("unreserved_serial_no")
si = make_si(unreserved[0])
self.assertRaises(frappe.ValidationError, si.submit)
frappe.db.rollback(save_point="unreserved_serial_no")
si = make_si(reserved[0])
si.submit()
self.assertEqual(get_serial_nos_from_bundle(si.items[0].serial_and_batch_bundle), reserved)
def make_item_for_si(item_code, properties=None):
from erpnext.stock.doctype.item.test_item import make_item

View File

@@ -79,7 +79,7 @@
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:10:38.343481",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
@@ -113,26 +113,6 @@
"role": "Sales Master Manager",
"share": 1,
"write": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
}
],
"show_title_field_in_link": 1,

View File

@@ -9,15 +9,10 @@ frappe.ui.form.on("Shipping Rule", {
},
company: function (frm) {
if (frm.previous_company !== frm.doc.company) {
frm.previous_company = frm.doc.company;
frm.set_value("account", "");
}
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
refresh: function (frm) {
frm.previous_company = frm.doc.company;
frm.set_query("account", function () {
return {
filters: {

View File

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

View File

@@ -52,23 +52,10 @@ class ShippingRule(Document):
# end: auto-generated types
def validate(self):
self.validate_account_company()
self.validate_from_to_values()
self.sort_shipping_rule_conditions()
self.validate_overlapping_shipping_rule_conditions()
def validate_account_company(self):
if not self.company or not self.account:
return
if frappe.get_cached_value("Account", self.account, "company") != self.company:
throw(
_("Shipping Account {0} does not belong to Company {1}").format(
frappe.bold(self.account), frappe.bold(self.company)
),
title=_("Invalid Shipping Account"),
)
def validate_from_to_values(self):
if self.calculate_based_on == "Fixed":
if self.conditions:

View File

@@ -16,39 +16,6 @@ class TestShippingRule(ERPNextTestSuite):
def setUp(self):
self.load_test_records("Shipping Rule")
def test_account_company_on_insert(self):
for rule_type in ("Selling", "Buying"):
with self.subTest(shipping_rule_type=rule_type):
shipping_rule = frappe.copy_doc(self.globalTestRecords["Shipping Rule"][0])
shipping_rule.label = f"{rule_type} Delivery"
shipping_rule.shipping_rule_type = rule_type
shipping_rule.company = "_Test Company 1"
shipping_rule.cost_center = None
with self.assertRaisesRegex(frappe.ValidationError, "does not belong to Company"):
shipping_rule.insert()
def test_account_company_on_update(self):
shipping_rule = frappe.copy_doc(self.globalTestRecords["Shipping Rule"][0])
shipping_rule.label = "Standard Delivery"
shipping_rule.insert()
shipping_rule.company = "_Test Company 1"
shipping_rule.cost_center = None
with self.assertRaisesRegex(frappe.ValidationError, "does not belong to Company"):
shipping_rule.save()
shipping_rule.reload()
shipping_rule.company = "_Test Company 1"
shipping_rule.account = "_Test Account Shipping Charges - _TC1"
shipping_rule.cost_center = None
shipping_rule.save()
shipping_rule.reload()
self.assertEqual(shipping_rule.company, "_Test Company 1")
self.assertEqual(shipping_rule.account, "_Test Account Shipping Charges - _TC1")
shipping_rule.account = "_Test Account Shipping Charges - _TC"
with self.assertRaisesRegex(frappe.ValidationError, "does not belong to Company"):
shipping_rule.save()
def test_from_greater_than_to(self):
shipping_rule = frappe.copy_doc(self.globalTestRecords["Shipping Rule"][0])
shipping_rule.name = self.globalTestRecords["Shipping Rule"][0].get("name")

View File

@@ -29,7 +29,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:10:51.976600",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",
@@ -68,62 +68,6 @@
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Item Manager",
"select": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase Master Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Sales Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
},
{
"role": "Stock User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -145,12 +145,6 @@ def get_party_details(party, party_type, args=None):
out = {}
billing_address, shipping_address = None, None
if args:
# each names a single Address: a dict is read as a filter and get_doc would resolve it to
# whichever Address matched.
for fieldname in ("billing_address", "shipping_address"):
if args.get(fieldname) and not isinstance(args.get(fieldname), str):
frappe.throw(_("Invalid address"), frappe.PermissionError)
if args.get("billing_address"):
billing_address = frappe.get_doc("Address", args.get("billing_address"))
if args.get("shipping_address"):

View File

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

View File

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

View File

@@ -106,8 +106,6 @@ def get_linked_payments_for_doc(
company: str | None = None, doctype: str | None = None, docname: str | None = None
) -> list:
if company and doctype and docname:
frappe.has_permission(doctype, doc=docname, throw=True)
_dt = doctype
_dn = docname
ple = qb.DocType("Payment Ledger Entry")

View File

@@ -5,9 +5,8 @@
import frappe
from frappe import _, msgprint, qb, scrub
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.permissions import get_allowed_docs_for_doctype
from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
@@ -159,7 +158,7 @@ def _get_party_details(
)
set_contact_details(party_details, party, party_type)
set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile, doctype)
set_price_list(party_details, party, party_type, price_list, pos_profile)
tax_template = set_taxes(
party.name,
@@ -385,33 +384,13 @@ def get_default_price_list(party):
return price_list
def get_permitted_price_lists(doctype=None):
permissions = sorted(
get_user_permissions().get("Price List", []), key=lambda p: p.get("is_default"), reverse=True
)
# a permission applicable for another doctype doesn't restrict this transaction
return get_allowed_docs_for_doctype(permissions, doctype)
def get_usable_price_list(price_lists, party_doctype):
transaction_side = "selling" if party_doctype == "Customer" else "buying"
for price_list in price_lists:
details = frappe.get_cached_value(
"Price List", price_list, ["enabled", transaction_side], as_dict=True
)
if details.enabled and details[transaction_side]:
return price_list
def set_price_list(party_details, party, party_type, given_price_list, pos=None, doctype=None):
def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list
permitted_price_lists = get_permitted_price_lists(doctype)
price_list = get_permitted_documents("Price List")
# if there is only one permitted document based on user permissions, set it
if len(permitted_price_lists) == 1:
price_list = get_usable_price_list(permitted_price_lists, party.doctype)
if price_list and len(price_list) == 1:
price_list = price_list[0]
elif pos and party_type == "Customer":
customer_price_list = frappe.get_value("Customer", party.name, "default_price_list")
@@ -423,10 +402,6 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None,
else:
price_list = get_default_price_list(party) or given_price_list
# don't set a price list the user has no permission for, the transaction can't be saved with it
if price_list and permitted_price_lists and price_list not in permitted_price_lists:
price_list = get_usable_price_list(permitted_price_lists, party.doctype)
if price_list and not is_price_list_enabled(price_list):
price_list = None

View File

@@ -156,7 +156,7 @@ frappe.query_reports["Accounts Payable"] = {
},
{
fieldname: "for_revaluation_journals",
label: __("Include Revaluation Journals"),
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{

View File

@@ -113,7 +113,7 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
{
fieldname: "for_revaluation_journals",
label: __("Include Revaluation Journals"),
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{

View File

@@ -183,7 +183,7 @@ frappe.query_reports["Accounts Receivable"] = {
},
{
fieldname: "for_revaluation_journals",
label: __("Include Revaluation Journals"),
label: __("Revaluation Journals"),
fieldtype: "Check",
},
{

View File

@@ -141,7 +141,7 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
{
fieldname: "for_revaluation_journals",
label: __("Include Revaluation Journals"),
label: __("Revaluation Journals"),
fieldtype: "Check",
},
],

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
)
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
from erpnext.stock.utils import _get_incoming_rate
from erpnext.stock.utils import get_incoming_rate
def execute(filters=None):
@@ -732,32 +732,23 @@ class GrossProfitGenerator:
)
def get_returned_invoice_items(self):
si = frappe.qb.DocType("Sales Invoice")
si_item = frappe.qb.DocType("Sales Invoice Item")
query = (
frappe.qb.from_(si)
.inner_join(si_item)
.on(si.name == si_item.parent)
.select(
si.name,
si_item.item_code,
si_item.sales_invoice_item,
si_item.stock_qty.as_("qty"),
si_item.base_net_amount.as_("base_amount"),
si.return_against,
)
.where(
(si.docstatus == 1)
& (si.is_return == 1)
& si.posting_date.between(self.filters.from_date, self.filters.to_date)
)
returned_invoices = frappe.db.sql(
"""
select
si.name, si_item.item_code, si_item.sales_invoice_item, si_item.stock_qty as qty,
si_item.base_net_amount as base_amount, si.return_against
from
`tabSales Invoice` si, `tabSales Invoice Item` si_item
where
si.name = si_item.parent
and si.docstatus = 1
and si.is_return = 1
and si.posting_date between %(from_date)s and %(to_date)s
""",
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
as_dict=1,
)
if self.filters.company:
query = query.where(si.company == self.filters.company)
returned_invoices = query.run(as_dict=1)
self.returned_invoices = frappe._dict()
self.legacy_returned_invoices = frappe._dict()
for inv in returned_invoices:
@@ -973,7 +964,7 @@ class GrossProfitGenerator:
if row.serial_and_batch_bundle:
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
average_buying_rate = _get_incoming_rate(args)
average_buying_rate = get_incoming_rate(args)
self.average_buying_rate[key] = flt(average_buying_rate)
return self.average_buying_rate[key]
@@ -1356,4 +1347,7 @@ class GrossProfitGenerator:
).setdefault(d.parent_item, []).append(d)
def load_non_stock_items(self):
self.non_stock_items = frappe.get_all("Item", filters={"is_stock_item": 0}, pluck="name")
self.non_stock_items = frappe.db.sql_list(
"""select name from tabItem
where is_stock_item=0"""
)

View File

@@ -1,6 +1,5 @@
import frappe
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.party import get_default_price_list, set_price_list
from erpnext.tests.utils import ERPNextTestSuite
@@ -35,159 +34,19 @@ class PartyTestCase(ERPNextTestSuite):
self.assertIsNone(party_details.selling_price_list)
def test_fallback_should_not_pick_an_unpermitted_price_list(self):
permitted_default = self.create_price_list(enabled=1)
permitted_other = self.create_price_list(enabled=1)
user = self.create_user_with_price_list_permissions([permitted_default, permitted_other])
customer = self.create_customer()
party_details = frappe._dict()
with self.set_user(user):
set_price_list(
party_details, customer, "Customer", self.create_price_list(enabled=1), doctype="Sales Order"
)
self.assertEqual(party_details.selling_price_list, permitted_default)
def test_permitted_given_price_list_should_be_kept(self):
permitted_default = self.create_price_list(enabled=1)
permitted_other = self.create_price_list(enabled=1)
user = self.create_user_with_price_list_permissions([permitted_default, permitted_other])
customer = self.create_customer()
party_details = frappe._dict()
with self.set_user(user):
set_price_list(party_details, customer, "Customer", permitted_other, doctype="Sales Order")
self.assertEqual(party_details.selling_price_list, permitted_other)
def test_permission_for_another_doctype_should_not_apply(self):
permitted = [self.create_price_list(enabled=1), self.create_price_list(enabled=1)]
user = self.create_user_with_price_list_permissions(permitted, applicable_for="Quotation")
customer = self.create_customer()
given_price_list = self.create_price_list(enabled=1)
party_details = frappe._dict()
with self.set_user(user):
set_price_list(party_details, customer, "Customer", given_price_list, doctype="Sales Order")
self.assertEqual(party_details.selling_price_list, given_price_list)
def test_a_single_permitted_price_list_should_fit_the_transaction(self):
buying_price_list = self.create_price_list(enabled=1, selling=0, buying=1)
user = self.create_user_with_price_list_permissions([buying_price_list])
customer = self.create_customer()
given_price_list = self.create_price_list(enabled=1)
party_details = frappe._dict()
with self.set_user(user):
set_price_list(party_details, customer, "Customer", given_price_list, doctype="Sales Order")
self.assertIsNone(party_details.selling_price_list)
def test_buying_transaction_should_not_take_a_selling_price_list(self):
permitted = [self.create_price_list(enabled=1) for _ in range(2)]
user = self.create_user_with_price_list_permissions(permitted)
supplier_price_list = self.create_price_list(enabled=1, selling=0, buying=1)
supplier = self.create_supplier(default_price_list=supplier_price_list)
party_details = frappe._dict()
with self.set_user(user):
set_price_list(party_details, supplier, "Supplier", None, doctype="Purchase Order")
self.assertIsNone(party_details.buying_price_list)
def test_permission_for_another_doctype_should_not_apply_without_a_doctype(self):
permitted = [self.create_price_list(enabled=1), self.create_price_list(enabled=1)]
user = self.create_user_with_price_list_permissions(permitted, applicable_for="Quotation")
customer = self.create_customer()
given_price_list = self.create_price_list(enabled=1)
party_details = frappe._dict()
with self.set_user(user):
set_price_list(party_details, customer, "Customer", given_price_list)
self.assertEqual(party_details.selling_price_list, given_price_list)
def test_pos_price_list_should_be_kept(self):
permitted = [self.create_price_list(enabled=1), self.create_price_list(enabled=1)]
user = self.create_user_with_price_list_permissions(permitted)
pos_price_list = self.create_price_list(enabled=1)
pos_profile = make_pos_profile(selling_price_list=pos_price_list)
customer = self.create_customer()
party_details = frappe._dict()
with self.set_user(user):
set_price_list(
party_details, customer, "Customer", None, pos=pos_profile.name, doctype="POS Invoice"
)
self.assertEqual(party_details.selling_price_list, pos_price_list)
def test_disabled_permitted_price_lists_should_clear_the_price_list(self):
permitted = [self.create_price_list(enabled=0), self.create_price_list(enabled=0)]
user = self.create_user_with_price_list_permissions(permitted)
customer = self.create_customer()
given_price_list = self.create_price_list(enabled=1)
party_details = frappe._dict()
with self.set_user(user):
set_price_list(party_details, customer, "Customer", given_price_list, doctype="Sales Order")
self.assertIsNone(party_details.selling_price_list)
def create_user_with_price_list_permissions(self, price_lists, applicable_for=None):
user = frappe.get_doc(
{
"doctype": "User",
"email": f"{frappe.generate_hash(length=10)}@example.com",
"first_name": "Price List Test",
"send_welcome_email": 0,
"roles": [{"role": "Sales User"}],
}
).insert(ignore_permissions=True)
for idx, price_list in enumerate(price_lists):
frappe.get_doc(
{
"doctype": "User Permission",
"user": user.name,
"allow": "Price List",
"for_value": price_list,
"is_default": int(idx == 0),
"apply_to_all_doctypes": int(not applicable_for),
"applicable_for": applicable_for,
}
).insert(ignore_permissions=True)
frappe.clear_cache(user=user.name)
self.addCleanup(frappe.clear_cache, user=user.name)
return user.name
def create_price_list(self, enabled, selling=1, buying=0):
def create_price_list(self, enabled):
price_list = frappe.get_doc(
{
"doctype": "Price List",
"price_list_name": frappe.generate_hash(length=10),
"currency": "INR",
"selling": selling,
"buying": buying,
"selling": 1,
"enabled": enabled,
}
).insert(ignore_permissions=True)
return price_list.name
def create_supplier(self, **values):
return frappe.get_doc(
{
"doctype": "Supplier",
"supplier_name": frappe.generate_hash(length=10),
**values,
}
).insert(ignore_permissions=True, ignore_mandatory=True)
def create_customer(self, **values):
customer = frappe.get_doc(
{

View File

@@ -627,7 +627,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2026-09-22 12:00:00.000000",
"modified": "2026-03-12 16:07:39.543227",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -663,38 +663,6 @@
"share": 1,
"submit": 1,
"write": 1
},
{
"role": "Manufacturing Manager",
"select": 1
},
{
"role": "Manufacturing User",
"select": 1
},
{
"role": "Purchase Manager",
"select": 1
},
{
"role": "Purchase User",
"select": 1
},
{
"role": "Stock Manager",
"select": 1
},
{
"role": "Stock User",
"select": 1
},
{
"role": "Accounts Manager",
"select": 1
},
{
"role": "System Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -736,7 +736,6 @@ class Asset(AccountsController):
frappe.throw(_("Asset cannot be cancelled, as it is already {0}").format(self.status))
def cancel_movement_entries(self):
# filter the parent Asset Movement's docstatus (as the original SQL did), not the child row's
movements = frappe.db.sql(
"""SELECT asm.name, asm.docstatus
FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
@@ -1317,25 +1316,7 @@ def is_cwip_accounting_enabled(asset_category):
@frappe.whitelist()
def get_asset_value_after_depreciation(asset_name, finance_book=None):
"""Whitelisted entry point: authorise the caller, then return the value."""
# one of the three calling forms is the boundary; Asset itself excludes the roles holding Asset Value Adjustment write
if not any(
frappe.has_permission(dt, "write")
for dt in ("Asset Value Adjustment", "Asset Capitalization", "Asset Repair")
):
frappe.throw(_("Not permitted"), frappe.PermissionError)
# select-or-read: these roles hold `select` on Asset, which does not satisfy a `read` check.
# Guard only here -- the in-process callers use _get_asset_value_after_depreciation() below.
ptype = "select" if frappe.only_has_select_perm("Asset") else "read"
frappe.has_permission("Asset", ptype, doc=asset_name, throw=True)
return _get_asset_value_after_depreciation(asset_name, finance_book)
def _get_asset_value_after_depreciation(asset_name, finance_book=None):
asset = frappe.get_doc("Asset", asset_name)
if not asset.calculate_depreciation:
return flt(asset.value_after_depreciation)
@@ -1344,8 +1325,6 @@ def _get_asset_value_after_depreciation(asset_name, finance_book=None):
@frappe.whitelist()
def has_active_capitalization(asset):
frappe.has_permission("Asset", doc=asset, throw=True)
active_capitalizations = frappe.db.count(
"Asset Capitalization", filters={"target_asset": asset, "docstatus": 1}
)
@@ -1354,19 +1333,7 @@ def has_active_capitalization(asset):
@frappe.whitelist()
def get_values_from_purchase_doc(purchase_doc_name: str, item_code: str, doctype: str):
# `doctype` is caller-supplied and reaches frappe.get_doc(), so without this list any document with
# an `items` table could be read for its valuation rates.
if doctype not in ("Purchase Receipt", "Purchase Invoice"):
frappe.throw(_("Invalid document type"), frappe.PermissionError)
# The Asset form is the boundary: Quality Manager writes Assets but reads neither Purchase Receipt
# nor Purchase Invoice, so the purchase document cannot be it.
frappe.has_permission("Asset", "write", throw=True)
# and the purchase document itself, whose company, posting date and amount are returned below.
# Asset write is not authority to read a Purchase Receipt or Invoice.
purchase_doc = frappe.get_doc(doctype, purchase_doc_name, check_permission="read")
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]
if not matching_items:

View File

@@ -10,7 +10,7 @@ from frappe import _
from frappe.utils import cint, flt, get_link_to_form
import erpnext
from erpnext.assets.doctype.asset.asset import _get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
@@ -32,7 +32,7 @@ from erpnext.stock.get_item_details import (
get_item_warehouse_,
)
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.utils import _get_incoming_rate
from erpnext.stock.utils import get_incoming_rate
force_fields = [
"target_item_name",
@@ -197,7 +197,7 @@ class AssetCapitalization(StockController):
cumulative_qty += flt(d.stock_qty)
args = self.get_args_for_incoming_rate(d)
args["qty"] = -1 * cumulative_qty
cumulative_rate = flt(_get_incoming_rate(args, raise_error_if_no_rate=False))
cumulative_rate = flt(get_incoming_rate(args, raise_error_if_no_rate=False))
cumulative_value = cumulative_rate * cumulative_qty
row_value = cumulative_value - prev_cumulative_value
@@ -332,8 +332,6 @@ class AssetCapitalization(StockController):
@frappe.whitelist()
def set_warehouse_details(self):
self.check_permission("write")
for d in self.get("stock_items"):
if d.item_code and d.warehouse:
args = self.get_args_for_incoming_rate(d)
@@ -344,13 +342,11 @@ class AssetCapitalization(StockController):
@frappe.whitelist()
def set_asset_values(self):
self.check_permission("write")
for d in self.get("asset_items"):
if d.asset:
finance_book = d.get("finance_book") or self.get("finance_book")
d.current_asset_value = flt(
_get_asset_value_after_depreciation(d.asset, finance_book=finance_book)
get_asset_value_after_depreciation(d.asset, finance_book=finance_book)
)
d.asset_value = get_value_after_depreciation_on_disposal_date(
d.asset, self.posting_date, finance_book=finance_book
@@ -644,18 +640,8 @@ class AssetCapitalization(StockController):
)
def check_capitalization_access(company=None):
"""Every lookup in this file feeds the Asset Capitalization form, so that form is the boundary.
`company` is accepted so the call sites read the same as on develop, where it also narrows the
caller to their permitted companies. There is no Company Restriction on this branch.
"""
frappe.has_permission("Asset Capitalization", throw=True)
@frappe.whitelist()
def get_target_item_details(item_code: str | None = None, company: str | None = None) -> frappe._dict:
check_capitalization_access(company)
out = frappe._dict()
# Get Item Details
@@ -682,7 +668,6 @@ def get_target_item_details(item_code: str | None = None, company: str | None =
@frappe.whitelist()
def get_target_asset_details(asset: str | None = None, company: str | None = None) -> frappe._dict:
check_capitalization_access(company)
out = frappe._dict()
# Get Asset Details
@@ -774,7 +759,7 @@ def get_warehouse_details(args):
frappe.has_permission("Stock Ledger Entry", throw=True)
out = {
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
"valuation_rate": _get_incoming_rate(args, raise_error_if_no_rate=False),
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
}
return out
@@ -782,14 +767,6 @@ def get_warehouse_details(args):
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_consumed_asset_details(ctx):
check_capitalization_access(ctx.get("company"))
# and the Asset the caller named: its depreciation values are returned through the unguarded
# _get_asset_value_after_depreciation. select-or-read, as in the asset.py wrapper.
if ctx.get("asset"):
ptype = "select" if frappe.only_has_select_perm("Asset") else "read"
frappe.has_permission("Asset", ptype, doc=ctx.get("asset"), throw=True)
out = frappe._dict()
asset_details = frappe._dict()
@@ -806,7 +783,7 @@ def get_consumed_asset_details(ctx):
if ctx.asset:
out.current_asset_value = flt(
_get_asset_value_after_depreciation(ctx.asset, finance_book=ctx.finance_book)
get_asset_value_after_depreciation(ctx.asset, finance_book=ctx.finance_book)
)
out.asset_value = get_value_after_depreciation_on_disposal_date(
ctx.asset, ctx.posting_date, finance_book=ctx.finance_book
@@ -836,8 +813,6 @@ def get_consumed_asset_details(ctx):
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_service_item_details(ctx):
check_capitalization_access(ctx.get("company"))
out = frappe._dict()
item = frappe._dict()
@@ -863,8 +838,6 @@ def get_items_tagged_to_wip_composite_asset(params):
if isinstance(params, str):
params = json.loads(params)
check_capitalization_access(params.get("company") if isinstance(params, dict | frappe._dict) else None)
fields = [
"item_code",
"item_name",

View File

@@ -73,7 +73,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2025-05-13 15:33:03.791814",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",
@@ -116,10 +116,6 @@
"role": "Quality Manager",
"share": 1,
"write": 1
},
{
"role": "Item Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -39,7 +39,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"link_filters": "[[\"Asset\",\"docstatus\",\"<\",\"2\"]]",
"link_filters": "[[\"Asset\",\"docstatus\",\"<\",\"2\"],[\"Asset\",\"company\",\"=\",\"eval:doc.company\"]]",
"options": "Asset",
"reqd": 1
},

View File

@@ -232,17 +232,9 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
last_depr_date = add_months(
self.fb_row.depreciation_start_date, -1 * self.fb_row.frequency_of_depreciation
)
if self.has_complete_opening_periods() and self.should_get_last_day:
last_depr_date = get_last_day(last_depr_date)
return last_depr_date
def get_booked_depr_for_months_count(self, last_depr_date):
if last_depr_date and self.has_complete_opening_periods():
first_period_end = self.get_next_schedule_date(
-cint(self.asset_doc.opening_number_of_booked_depreciations)
)
return month_diff(last_depr_date, first_period_end) - 1 + self.fb_row.frequency_of_depreciation
depr_booked_for_months = 0
if last_depr_date:
asset_used_for_months = self.fb_row.frequency_of_depreciation * (
@@ -258,20 +250,6 @@ class DepreciationScheduleController(StraightLineMethod, WDVMethod):
)
return depr_booked_for_months
def has_complete_opening_periods(self):
if (
self.fb_row.depreciation_method not in ("Straight Line", "Manual")
or self.fb_row.daily_prorata_based
or self.fb_row.shift_based
or not self.asset_doc.opening_number_of_booked_depreciations
):
return False
first_period_start = add_days(
self.get_next_schedule_date(-cint(self.asset_doc.opening_number_of_booked_depreciations) - 1), 1
)
return first_period_start == getdate(self.asset_doc.available_for_use_date)
def get_total_pending_days_or_years(self):
if cint(frappe.get_single_value("Accounts Settings", "calculate_depr_using_total_days")):
last_depr_date = self.get_last_booked_depreciation_date()

View File

@@ -2,7 +2,7 @@
# See license.txt
import frappe
from frappe.utils import add_months, cstr, flt, get_last_day, getdate
from frappe.utils import cstr, flt, getdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.assets.doctype.asset.depreciation import (
@@ -21,30 +21,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAssetDepreciationSchedule(ERPNextTestSuite):
def test_complete_opening_periods(self):
for available in ("2026-08-01", "2024-02-01"):
for frequency in (1, 3, 6, 12):
for opening in (1, 4):
with self.subTest(available=available, frequency=frequency, opening=opening):
start = get_last_day(add_months(available, (opening + 1) * frequency - 1))
asset = create_asset(
calculate_depreciation=1,
available_for_use_date=available,
depreciation_start_date=start,
frequency_of_depreciation=frequency,
total_number_of_depreciations=12,
net_purchase_amount=13200,
expected_value_after_useful_life=1200,
opening_number_of_booked_depreciations=opening,
opening_accumulated_depreciation=1000 * opening,
)
schedule = get_depr_schedule(asset.name, "Draft")
self.assertEqual(
[row.depreciation_amount for row in schedule], [1000] * (12 - opening)
)
self.assertEqual(schedule[-1].accumulated_depreciation_amount, 12000)
self.assertEqual(getdate(schedule[0].schedule_date), start)
def test_throw_error_if_another_asset_depr_schedule_exist(self):
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)

View File

@@ -61,7 +61,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:06:34.976117",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Maintenance Team",
@@ -78,10 +78,6 @@
"role": "Manufacturing User",
"share": 1,
"write": 1
},
{
"role": "Quality Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -458,15 +458,6 @@ class AssetRepair(AccountsController):
add_asset_activity(self.asset, subject)
def check_asset_repair_access(company=None):
"""Both pickers below sit on the Asset Repair form, so that form is the boundary, not Purchase Invoice.
`company` is accepted so the call sites read the same as on develop, where it also narrows the
caller to their permitted companies. There is no Company Restriction on this branch.
"""
frappe.has_permission("Asset Repair", throw=True)
@frappe.whitelist()
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
@@ -480,8 +471,6 @@ def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
Get Purchase Invoices that have expense accounts for non-stock items.
Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account.
"""
check_asset_repair_access(filters.get("company") if isinstance(filters, dict) else None)
pi = DocType("Purchase Invoice")
pi_item = DocType("Purchase Invoice Item")
item = DocType("Item")
@@ -517,8 +506,6 @@ def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
Get expense accounts for non-stock (service) items from the purchase invoice.
Used as a query function for link fields.
"""
check_asset_repair_access()
purchase_invoice = filters.get("purchase_invoice")
if not purchase_invoice:
return []

View File

@@ -36,7 +36,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2024-03-27 13:06:35.869900",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Shift Factor",
@@ -66,10 +66,6 @@
"role": "Accounts User",
"share": 1,
"write": 1
},
{
"role": "Quality Manager",
"select": 1
}
],
"sort_field": "creation",

View File

@@ -10,7 +10,7 @@ from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.assets.doctype.asset.asset import _get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
@@ -61,7 +61,7 @@ class AssetValueAdjustment(Document):
def set_current_asset_value(self):
if not self.current_asset_value and self.asset:
self.current_asset_value = _get_asset_value_after_depreciation(self.asset, self.finance_book)
self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
def on_submit(self):
self.make_asset_revaluation_entry()

View File

@@ -143,7 +143,7 @@
],
"is_tree": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2025-04-29 13:53:13.488906",
"modified_by": "Administrator",
"module": "Assets",
"name": "Location",
@@ -198,10 +198,6 @@
"role": "Stock Manager",
"share": 1,
"write": 1
},
{
"role": "Quality Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -15,7 +15,7 @@ from erpnext.accounts.report.financial_statements import (
validate_fiscal_year,
)
from erpnext.accounts.utils import get_fiscal_year
from erpnext.assets.doctype.asset.asset import _get_asset_value_after_depreciation
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
def execute(filters=None):

View File

@@ -608,7 +608,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
var item_length = me.frm.doc.items.length;
while (i < item_length) {
var qty = me.frm.doc.items[i].qty;
(r.message || []).forEach(function (d) {
(r.message[0] || []).forEach(function (d) {
if (
d.qty > 0 &&
qty > 0 &&

View File

@@ -768,18 +768,14 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=
return item_last_purchase_rate
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def close_or_unclose_purchase_orders(names, status):
frappe.has_permission("Purchase Order", "write", throw=True)
if not frappe.has_permission("Purchase Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError)
names = json.loads(names)
for name in names:
if not isinstance(name, str):
frappe.throw(_("Invalid name"), frappe.PermissionError)
# the check above is doctype level, so on its own it lets a caller restricted to one company close
# another company's orders. Matches what update_status() already does.
po = frappe.get_lazy_doc("Purchase Order", name, check_permission="submit")
po = frappe.get_lazy_doc("Purchase Order", name)
if po.docstatus == 1:
if status == "Closed":
if po.status not in ("Cancelled", "Closed") and (
@@ -997,7 +993,7 @@ def get_list_context(context=None):
return list_context
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def update_status(status, name):
po = frappe.get_lazy_doc("Purchase Order", name, check_permission="submit")
po.update_status(status)

View File

@@ -796,25 +796,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
po = create_purchase_order(qty=3.4, do_not_save=True)
self.assertRaises(UOMMustBeIntegerError, po.insert)
def test_uom_integer_check_tolerates_conversion_dust(self):
from erpnext.utilities.transaction_base import UOMMustBeIntegerError
item_doc = make_item(properties={"stock_uom": "Nos"})
item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.6})
item_doc.save()
item = item_doc.name
precision = frappe.get_precision("Purchase Order Item", "stock_qty")
po = create_purchase_order(item_code=item, qty=flt(2000 / 0.6, precision), do_not_save=1)
po.items[0].uom = "Kg"
po.items[0].conversion_factor = 0.6
po.insert()
fractional = create_purchase_order(item_code=item, qty=3333.9, do_not_save=1)
fractional.items[0].uom = "Kg"
fractional.items[0].conversion_factor = 0.6
self.assertRaises(UOMMustBeIntegerError, fractional.insert)
def test_ordered_qty_for_closing_po(self):
bin = frappe.get_all(
"Bin",

View File

@@ -11,6 +11,7 @@ from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import Order
from frappe.utils import get_url
from frappe.utils.print_format import download_pdf
from frappe.utils.user import get_user_fullname
@@ -686,34 +687,32 @@ def get_supplier_tag():
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
rfq_filters = [
["docstatus", "=", 1],
["company", "=", filters.get("company")],
]
rfq = frappe.qb.DocType("Request for Quotation")
rfq_supplier = frappe.qb.DocType("Request for Quotation Supplier")
if frappe.has_permission("Request for Quotation", "read"):
rfq_filters.append(["Request for Quotation Supplier", "supplier", "=", filters.get("supplier")])
else:
parents = frappe.get_all(
"Request for Quotation Supplier",
filters={"supplier": filters.get("supplier"), "parenttype": "Request for Quotation"},
pluck="parent",
distinct=True,
query = (
frappe.qb.from_(rfq)
.from_(rfq_supplier)
.select(rfq.name)
.distinct()
.select(rfq.transaction_date, rfq.company)
.where(
(rfq.name == rfq_supplier.parent)
& (rfq_supplier.supplier == filters.get("supplier"))
& (rfq.docstatus == 1)
& (rfq.company == filters.get("company"))
)
rfq_filters.append(["name", "in", parents or [""]])
.orderby(rfq.transaction_date, order=Order.asc)
.limit(page_len)
.offset(start)
)
if txt:
rfq_filters.append(["name", "like", f"%{txt}%"])
query = query.where(rfq.name.like(f"%%{txt}%%"))
if filters.get("transaction_date"):
rfq_filters.append(["transaction_date", "=", filters.get("transaction_date")])
query = query.where(rfq.transaction_date == filters.get("transaction_date"))
return frappe.get_list(
"Request for Quotation",
filters=rfq_filters,
fields=["name", "transaction_date", "company"],
group_by="name",
order_by="transaction_date asc",
limit_start=start,
limit_page_length=page_len,
)
rfq_data = query.run(as_dict=1)
return rfq_data

View File

@@ -562,7 +562,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-08-14 16:10:58.600553",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -618,30 +618,6 @@
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"role": "Delivery Manager",
"select": 1
},
{
"role": "Delivery User",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
},
{
"role": "Quality Manager",
"select": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Website Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -237,15 +237,6 @@ class Supplier(TransactionBase):
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
type = filters.get("type")
# `type` is caller-supplied and reaches qb.DocType(), so any doctype could be joined to Dynamic
# Link. The two pickers that call this send only these two values.
if type not in ("Contact", "Address"):
frappe.throw(_("Invalid type"), frappe.PermissionError)
# authorise the party, not Contact/Address: the `if_owner` row on Address would empty the picker rather than error
frappe.has_permission("Supplier", doc=supplier, throw=True)
type_doctype = frappe.qb.DocType(type)
dynamic_link = frappe.qb.DocType("Dynamic Link")

View File

@@ -948,7 +948,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-08-12 12:00:00.000000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -1007,14 +1007,6 @@
"read": 1,
"role": "Purchase Manager",
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -1,139 +0,0 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import frappe
import frappe.permissions
from erpnext.buying.utils import get_linked_material_requests
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.material_request.test_material_request import make_material_request
from erpnext.tests.utils import ERPNextTestSuite
def create_user_with_roles(email, *roles):
if frappe.db.exists("User", email):
user = frappe.get_doc("User", email)
else:
user = frappe.new_doc("User")
user.email = email
user.first_name = email.split("@", 1)[0]
user.insert(ignore_permissions=True)
user.set("roles", [])
for role in roles:
user.append("roles", {"role": role})
user.save(ignore_permissions=True)
# a user left without roles is downgraded to a Website User on save
frappe.db.set_value("User", email, "user_type", "System User")
return user
class TestGetLinkedMaterialRequests(ERPNextTestSuite):
def setUp(self):
self.material_request = make_material_request(item_code="_Test Item")
def test_permitted_role_can_fetch_linked_material_requests(self):
create_user_with_roles("test_buying_purchase_user@example.com", "Purchase User")
with self.set_user("test_buying_purchase_user@example.com"):
rows = get_linked_material_requests(["_Test Item"])
self.assertIn(self.material_request.name, {row.mr_name for row in rows})
def test_populated_result_is_a_flat_list_of_rows(self):
"""Both callers iterate the response directly, so it has to stay a flat list of rows
rather than a list of lists."""
create_user_with_roles("test_buying_purchase_user@example.com", "Purchase User")
with self.set_user("test_buying_purchase_user@example.com"):
rows = get_linked_material_requests(["_Test Item"])
self.assertIsInstance(rows, list)
self.assertTrue(rows)
for row in rows:
self.assertNotIsInstance(row, list | tuple)
self.assertIsInstance(row, dict)
for fieldname in ("mr_name", "mr_item", "item_code", "qty"):
self.assertIn(fieldname, row)
def test_empty_result_is_a_flat_empty_list(self):
item_without_request = make_item("_Test Item Without Material Request").name
create_user_with_roles("test_buying_purchase_user@example.com", "Purchase User")
with self.set_user("test_buying_purchase_user@example.com"):
rows = get_linked_material_requests([item_without_request])
self.assertEqual(rows, [])
def test_a_single_item_code_is_treated_as_one_code(self):
"""A lone code must be read as one item code, not iterated character by character."""
create_user_with_roles("test_buying_purchase_user@example.com", "Purchase User")
with self.set_user("test_buying_purchase_user@example.com"):
rows = get_linked_material_requests(json.dumps("_Test Item"))
self.assertIn(self.material_request.name, {row.mr_name for row in rows})
def test_items_that_are_not_item_codes_are_rejected(self):
"""Anything that is not a `str` or a `list` is already refused by the type annotation,
so these are the malformed inputs that reach the method."""
create_user_with_roles("test_buying_purchase_user@example.com", "Purchase User")
bad_inputs = (
"not json at all",
[{"item_code": "_Test Item"}],
[["_Test Item"]],
[None],
)
with self.set_user("test_buying_purchase_user@example.com"):
for bad_items in bad_inputs:
with self.subTest(items=bad_items):
self.assertRaises(frappe.ValidationError, get_linked_material_requests, bad_items)
def test_manufacturing_manager_can_fetch_linked_material_requests(self):
"""Manufacturing Manager holds write on Supplier Quotation and Request for Quotation,
both of which call this method, so it must hold Material Request read as well."""
create_user_with_roles("test_buying_mfg_manager@example.com", "Manufacturing Manager")
with self.set_user("test_buying_mfg_manager@example.com"):
rows = get_linked_material_requests(["_Test Item"])
self.assertIn(self.material_request.name, {row.mr_name for row in rows})
def test_unpermitted_role_cannot_fetch_linked_material_requests(self):
create_user_with_roles("test_buying_sales_user@example.com", "Sales User")
with self.set_user("test_buying_sales_user@example.com"):
self.assertRaises(frappe.PermissionError, get_linked_material_requests, ["_Test Item"])
def test_role_with_only_select_permission_cannot_fetch_linked_material_requests(self):
"""Material Request grants Delivery and Maintenance roles `select` and nothing else.
`select` is enough to list names, so the permitted set must be resolved through a
filter on the child table, which requires `read`."""
create_user_with_roles("test_buying_delivery_user@example.com", "Delivery User")
with self.set_user("test_buying_delivery_user@example.com"):
self.assertRaises(frappe.PermissionError, get_linked_material_requests, ["_Test Item"])
def test_results_are_restricted_by_user_permissions(self):
other_company_request = make_material_request(
item_code="_Test Item",
company="_Test Company 1",
warehouse="_Test Warehouse 2 - _TC1",
cost_center="Main - _TC1",
)
user = create_user_with_roles("test_buying_restricted_user@example.com", "Purchase User")
frappe.permissions.add_user_permission("Company", "_Test Company", user.name)
try:
with self.set_user(user.name):
mr_names = {row.mr_name for row in get_linked_material_requests(["_Test Item"])}
finally:
frappe.permissions.remove_user_permission("Company", "_Test Company", user.name)
self.assertIn(self.material_request.name, mr_names)
self.assertNotIn(other_company_request.name, mr_names)

View File

@@ -43,9 +43,7 @@ def update_last_purchase_rate(doc, is_submit) -> None:
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate
frappe.db.set_value(
"Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate), update_modified=False
)
frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate))
def validate_for_items(doc) -> None:
@@ -127,37 +125,7 @@ def check_on_hold_or_closed_status(doctype, docname) -> None:
@frappe.whitelist()
def get_linked_material_requests(items):
"""
Retrieve Material Requests linked to a list of items.
"""
try:
items = frappe.parse_json(items)
except (TypeError, ValueError):
frappe.throw(_("Items must be a list of Item codes"))
if isinstance(items, str):
items = [items]
if not isinstance(items, list | tuple) or any(not isinstance(item, str) for item in items):
frappe.throw(_("Items must be a list of Item codes"))
permitted_material_requests = frappe.get_list(
"Material Request",
filters=[
["material_request_type", "=", "Purchase"],
["docstatus", "=", 1],
["status", "!=", "Stopped"],
["per_ordered", "<", 99.99],
["Material Request Item", "item_code", "in", items],
],
pluck="name",
distinct=True,
)
if not permitted_material_requests:
return []
items = json.loads(items)
mr_list = []
for item in items:
material_request = frappe.db.sql(
@@ -167,17 +135,16 @@ def get_linked_material_requests(items):
mr_item.name AS mr_item
FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
WHERE mr.name = mr_item.parent
AND mr.name IN %(permitted)s
AND mr_item.item_code = %(item)s
AND mr.material_request_type = 'Purchase'
AND mr.per_ordered < 99.99
AND mr.docstatus = 1
AND mr.status != 'Stopped'
ORDER BY mr_item.item_code ASC""",
{"item": item, "permitted": permitted_material_requests},
{"item": item},
as_dict=1,
)
if material_request:
mr_list.extend(material_request)
mr_list.append(material_request)
return mr_list

View File

@@ -405,26 +405,12 @@ class AccountsController(TransactionBase):
return any(item.delivered_by_supplier for item in items)
def validate_price_list(self):
if self.get("selling_price_list"):
price_list_field, transaction_side = "selling_price_list", "selling"
else:
price_list_field, transaction_side = "buying_price_list", "buying"
price_list_field = "selling_price_list" if self.get("selling_price_list") else "buying_price_list"
price_list = self.get(price_list_field)
if not price_list:
if not price_list or frappe.db.get_value("Price List", price_list, "enabled"):
return
details = (
frappe.db.get_value("Price List", price_list, ["enabled", transaction_side], as_dict=True)
or frappe._dict()
)
# An internal transfer carries the price list of the outward document into the inward one.
fits_transaction = details.get(transaction_side) or self.is_internal_transfer()
if details.enabled and fits_transaction:
return
# Returns retain a submitted voucher's pricing even if its price list no longer fits.
# Returns retain a submitted voucher's pricing even if its price list is now disabled.
if (
self.get("is_return")
and self.get("return_against")
@@ -435,20 +421,9 @@ class AccountsController(TransactionBase):
):
return
if not details.enabled:
frappe.throw(
_("Price List {0} is disabled").format(get_link_to_form("Price List", price_list)),
title=_("Disabled Price List"),
)
if transaction_side == "selling":
message = _("Price List {0} cannot be used on a selling transaction")
else:
message = _("Price List {0} cannot be used on a buying transaction")
frappe.throw(
message.format(get_link_to_form("Price List", price_list)),
title=_("Invalid Price List"),
_("Price List {0} is disabled").format(get_link_to_form("Price List", price_list)),
title=_("Disabled Price List"),
)
def set_default_letter_head(self):
@@ -1091,15 +1066,12 @@ class AccountsController(TransactionBase):
args = "for_buying"
if self.meta.get_field(fieldname) and self.get(fieldname):
previous_price_list_currency = self.price_list_currency
self.price_list_currency = frappe.db.get_value("Price List", self.get(fieldname), "currency")
if self.price_list_currency == self.company_currency:
self.plc_conversion_rate = 1.0
elif not self.plc_conversion_rate or (
previous_price_list_currency and previous_price_list_currency != self.price_list_currency
):
elif not self.plc_conversion_rate:
self.plc_conversion_rate = get_exchange_rate(
self.price_list_currency, self.company_currency, transaction_date, args
)
@@ -3271,28 +3243,11 @@ def get_tax_rate(account_head):
return frappe.get_cached_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
# the only doctypes a `taxes_and_charges` Link points at; `master_doctype` is caller-supplied and
# reaches get_doc()
TAX_MASTER_DOCTYPES = ("Sales Taxes and Charges Template", "Purchase Taxes and Charges Template")
def validate_tax_master(master_doctype, master_name=None):
"""Reject a caller-supplied doctype that is not a tax template.
`master_name` is accepted so the call sites read the same as on develop, where it also narrows
the caller to their permitted companies. There is no Company Restriction on this branch.
"""
if master_doctype not in TAX_MASTER_DOCTYPES:
frappe.throw(_("Invalid tax master doctype"), frappe.PermissionError)
@frappe.whitelist()
def get_default_taxes_and_charges(master_doctype, tax_template=None, company=None):
if not company:
return {}
validate_tax_master(master_doctype, tax_template)
if tax_template and company:
tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company")
if tax_template_company == company:
@@ -3310,9 +3265,6 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non
def get_taxes_and_charges(master_doctype, master_name):
if not master_name:
return
validate_tax_master(master_doctype, master_name)
from frappe.model import child_table_fields, default_fields
tax_master = frappe.get_doc(master_doctype, master_name)
@@ -4453,7 +4405,7 @@ def update_child_qty_rate(
cancel_stock_reservation_entries(parent.doctype, parent.name)
if parent.per_picked == 0:
parent._create_stock_reservation_entries()
parent.create_stock_reservation_entries()
def check_if_child_table_updated(child_table_before_update, child_table_after_update, fields_to_check):

View File

@@ -23,7 +23,7 @@ from erpnext.stock.get_item_details import (
get_conversion_factor,
get_item_defaults,
)
from erpnext.stock.utils import _get_incoming_rate, is_serial_no_wise_valuation_disabled
from erpnext.stock.utils import get_incoming_rate
class QtyMismatchError(ValidationError):
@@ -181,7 +181,7 @@ class BuyingController(SubcontractingController):
for row in self.items:
if row.rate <= 0:
# override the rate with valuation rate
row.rate = _get_incoming_rate(
row.rate = get_incoming_rate(
{
"item_code": row.item_code,
"warehouse": row.warehouse,
@@ -467,7 +467,7 @@ class BuyingController(SubcontractingController):
net_rate = item.base_net_amount
if item.sales_incoming_rate: # for internal transfer
net_rate = self.get_internal_transfer_qty(item) * item.sales_incoming_rate
net_rate = item.qty * item.sales_incoming_rate
if (
not net_rate
@@ -683,7 +683,7 @@ class BuyingController(SubcontractingController):
if not posting_time:
posting_time = nowtime()
outgoing_rate = _get_incoming_rate(
outgoing_rate = get_incoming_rate(
{
"item_code": d.item_code,
"warehouse": d.get("from_warehouse"),
@@ -803,26 +803,6 @@ class BuyingController(SubcontractingController):
)
)
def get_internal_transfer_qty(self, row) -> float:
if flt(row.qty) or not self.is_internal_receipt():
return flt(row.qty)
return flt(row.rejected_qty)
def is_internal_receipt(self) -> bool:
return self.doctype == "Purchase Receipt" and self.is_internal_transfer()
def get_source_warehouse_qty(self, row, accepted_qty):
if not (self.is_internal_receipt() and flt(row.rejected_qty)):
return accepted_qty
if row.get("serial_and_batch_bundle") or row.get("rejected_serial_and_batch_bundle"):
return accepted_qty
rejected_qty = flt(flt(row.rejected_qty) * flt(row.conversion_factor), row.precision("stock_qty"))
return flt(accepted_qty + rejected_qty, row.precision("stock_qty"))
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_and_reserved_qty()
@@ -835,9 +815,8 @@ class BuyingController(SubcontractingController):
if d.warehouse:
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
source_qty = self.get_source_warehouse_qty(d, pr_qty)
if pr_qty or source_qty:
if pr_qty:
if d.from_warehouse and (
(not cint(self.is_return) and self.docstatus == 1)
or (cint(self.is_return) and self.docstatus == 2)
@@ -853,7 +832,7 @@ class BuyingController(SubcontractingController):
from_warehouse_sle = self.get_sl_entries(
d,
{
"actual_qty": -1 * source_qty,
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse,
"outgoing_rate": d.rate,
"recalculate_rate": 1,
@@ -887,11 +866,9 @@ class BuyingController(SubcontractingController):
)
if self.is_return:
outgoing_rate = 0.0
if not is_serial_no_wise_valuation_disabled(d.item_code):
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
sle.update(
{
@@ -928,7 +905,7 @@ class BuyingController(SubcontractingController):
from_warehouse_sle = self.get_sl_entries(
d,
{
"actual_qty": -1 * source_qty,
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse,
"recalculate_rate": 1,
"serial_and_batch_bundle": (

View File

@@ -34,10 +34,6 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur
:param item: Template Item
:param args: A dictionary with "Attribute" as key and "Attribute Value" as value
"""
# The Item form is the boundary and `read` is loser-free: roles that cannot read Item cannot open
# that form. The two server-side callers already hold the template.
frappe.has_permission("Item", doc=template, throw=True)
item_template = frappe.get_doc("Item", template)
if item_template.variant_based_on == "Manufacturer" and manufacturer:
@@ -313,11 +309,6 @@ def find_variant(template, args, variant_item_code=None):
@frappe.whitelist()
def create_variant(item, args, use_template_image=False):
# Same right as the sibling enqueue_multiple_variant_creation, plus record-level read on the
# template it copies from.
frappe.has_permission("Item", ptype="create", throw=True)
frappe.has_permission("Item", doc=item, throw=True)
use_template_image = frappe.parse_json(use_template_image)
if isinstance(args, str):
args = json.loads(args)
@@ -343,7 +334,7 @@ def create_variant(item, args, use_template_image=False):
return variant
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def enqueue_multiple_variant_creation(item, args, use_template_image=False):
frappe.has_permission("Item", ptype="create", throw=True)
use_template_image = frappe.parse_json(use_template_image)
@@ -536,9 +527,6 @@ def make_variant_item_code(template_item_code, template_item_name, variant):
@frappe.whitelist()
def create_variant_doc_for_quick_entry(template, args):
# get_variant and create_variant carry their own checks; this fails fast rather than relying on them.
frappe.has_permission("Item", doc=template, throw=True)
variant_based_on = frappe.db.get_value("Item", template, "variant_based_on")
args = json.loads(args)
if variant_based_on == "Manufacturer":

View File

@@ -6,17 +6,16 @@ import json
from collections import OrderedDict, defaultdict
import frappe
from frappe import _, qb, scrub
from frappe import qb, scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.permissions import has_permission
from frappe.query_builder import Case, Criterion
from frappe.query_builder import Criterion, CustomFunction
from frappe.query_builder.functions import Concat, Locate, Sum
from frappe.utils import cint, nowdate, today, unique
from pypika import Order
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.stock.doctype.item.item_search import get_item_search_candidates
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template
from erpnext.stock.utils import get_combine_datetime
@@ -177,15 +176,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict | str | None,
as_dict: bool = False,
):
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
doctype = "Item"
conditions = []
@@ -216,7 +207,6 @@ def item_query(
]
if field not in searchfields
]
searched_fields = list(searchfields)
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict):
@@ -273,17 +263,7 @@ def item_query(
if frappe.db.estimate_count(doctype) < 50000:
# scan description only if items are less than 50000
description_cond = "or tabItem.description LIKE %(txt)s"
searched_fields.append("description")
candidate_cond = ""
candidates = get_item_search_candidates(txt, searched_fields)
if candidates is not None:
if not candidates:
return [] if as_dict else ()
candidate_cond = "and tabItem.name in %(candidates)s"
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-sql-format-injection
return frappe.db.sql(
"""select
tabItem.name {columns}
@@ -294,7 +274,7 @@ def item_query(
and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00')
and ({scond} or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s)
{description_cond})
{fcond} {mcond} {candidate_cond}
{fcond} {mcond}
order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
if(locate(%(_txt)s, item_name), locate(%(_txt)s, item_name), 99999),
@@ -306,7 +286,6 @@ def item_query(
fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
mcond=get_match_cond(doctype).replace("%", "%%"),
description_cond=description_cond,
candidate_cond=candidate_cond,
),
{
"today": nowdate(),
@@ -314,7 +293,6 @@ def item_query(
"_txt": txt.replace("%", ""),
"start": start,
"page_len": page_len,
"candidates": tuple(candidates or ()),
},
as_dict=as_dict,
)
@@ -358,65 +336,43 @@ def get_project_name(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None
):
proj = qb.DocType("Project")
meta = frappe.get_meta(doctype)
list_filters = [["status", "not in", ["Completed", "Cancelled", "On hold"]]]
qb_filter_and_conditions = []
qb_filter_or_conditions = []
ifelse = CustomFunction("IF", ["condition", "then", "else"])
if filters:
if filters.get("customer"):
# an `in` containing "" renders as `ifnull(customer,'') in (...)`: this customer, or none
list_filters.append(["customer", "in", [filters.get("customer"), ""]])
qb_filter_and_conditions.append(
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
)
if filters.get("company"):
list_filters.append(["company", "=", filters.get("company")])
qb_filter_and_conditions.append(proj.company == filters.get("company"))
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
# permlevel fields go too: get_list refuses to filter on one, failing the whole call.
searchfields = [
x
for x in meta.get_search_fields()
if x not in ["customer", "status"] and not (meta.get_field(x) and meta.get_field(x).permlevel)
]
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled", "On hold"]))
q = qb.from_(proj)
fields = get_fields(doctype, ["name", "project_name"])
for x in fields:
q = q.select(proj[x])
# get_list applies the doctype check and the caller's record-level conditions
if not txt:
# no search term means no relevance ordering, so the whole query stays paginated in SQL rather
# than materialising every permitted name.
return frappe.get_list(
"Project",
filters=list_filters,
fields=fields,
order_by="idx desc, name",
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
searchfields = [
x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"]
]
# with a search term, resolve the permitted names and rank them below: the relevance ordering is
# a CASE expression, which `order_by` rejects.
permitted = frappe.get_list(
"Project",
filters=list_filters,
or_filters=[[x, "like", f"%{txt}%"] for x in searchfields],
pluck="name",
order_by="",
limit_page_length=0,
)
# pattern search
if txt:
for x in searchfields:
qb_filter_or_conditions.append(proj[x].like(f"%{txt}%"))
if not permitted:
return []
q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions))
q = (
frappe.qb.from_(proj)
.select(*[proj[fieldname] for fieldname in fields])
.where(proj.name.isin(permitted))
)
# CASE, not IF(): IF() is MariaDB-only and is emitted verbatim, so it fails on postgres.
q = q.orderby(
Case().when(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name)).else_(99999)
)
# ordering
if txt:
# project_name containing search string 'txt' will be given higher precedence
q = q.orderby(ifelse(Locate(txt, proj.project_name) > 0, Locate(txt, proj.project_name), 99999))
q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name)
if page_len:
@@ -680,33 +636,28 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
bo_filters = [
["docstatus", "=", 1],
["blanket_order_type", "=", filters.get("blanket_order_type")],
["company", "=", filters.get("company")],
]
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
if frappe.has_permission("Blanket Order", "read"):
bo_filters.append(["Blanket Order Item", "item_code", "=", filters.get("item")])
else:
parents = frappe.get_all(
"Blanket Order Item",
filters={"item_code": filters.get("item"), "parenttype": "Blanket Order"},
pluck="parent",
distinct=True,
query = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo.name)
.distinct()
.select(bo.blanket_order_type, bo.to_date)
.where(
(bo_item.parent == bo.name)
& (bo_item.item_code == filters.get("item"))
& (bo.blanket_order_type == filters.get("blanket_order_type"))
& (bo.company == filters.get("company"))
& (bo.docstatus == 1)
)
bo_filters.append(["name", "in", parents or [""]])
)
if currency := filters.get("currency"):
bo_filters.append(["currency", "=", currency])
query = query.where(bo.currency == currency)
return frappe.get_list(
"Blanket Order",
filters=bo_filters,
fields=["name", "blanket_order_type", "to_date"],
group_by="name",
as_list=True,
)
return query.run()
@frappe.whitelist()
@@ -888,22 +839,15 @@ def get_doctype_wise_filters(filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters):
# get_list applies the select check and the caller's record-level conditions together
batch_filters = [["disabled", "=", 0], ["name", "like", f"%{txt}%"]]
query = """select batch_id from `tabBatch`
where disabled = 0
and (expiry_date >= CURRENT_DATE or expiry_date IS NULL)
and name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%"))
if filters and filters.get("item"):
batch_filters.append(["item", "=", filters.get("item")])
query += " and item = {item}".format(item=frappe.db.escape(filters.get("item")))
return frappe.get_list(
"Batch",
filters=batch_filters,
or_filters=[["expiry_date", "is", "not set"], ["expiry_date", ">=", today()]],
fields=["batch_id"],
order_by="batch_id",
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
return frappe.db.sql(query, filters)
@frappe.whitelist()
@@ -928,67 +872,35 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
pr_filters = [["docstatus", "=", 1], ["name", "like", f"%{txt}%"]]
query = """
select pr.name
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem
where pr.docstatus = 1 and pritem.parent = pr.name
and pr.name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%"))
if filters and filters.get("item_code"):
if frappe.has_permission("Purchase Receipt", "read"):
# one indexed join, deduped by group_by below
pr_filters.append(["Purchase Receipt Item", "item_code", "=", filters.get("item_code")])
else:
# a select-only caller may use this picker but may not filter on a child table, so resolve the
# parents separately rather than losing the filter to a PermissionError.
parents = frappe.get_all(
"Purchase Receipt Item",
filters={"item_code": filters.get("item_code"), "parenttype": "Purchase Receipt"},
pluck="parent",
distinct=True,
)
pr_filters.append(["name", "in", parents or [""]])
query += " and pritem.item_code = {item_code}".format(
item_code=frappe.db.escape(filters.get("item_code"))
)
# group_by, not distinct: frappe drops ORDER BY from a distinct query on Postgres.
return frappe.get_list(
"Purchase Receipt",
filters=pr_filters,
fields=["name"],
group_by="name",
order_by="name",
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
pi_filters = [["docstatus", "=", 1], ["name", "like", f"%{txt}%"]]
query = """
select pi.name
from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem
where pi.docstatus = 1 and piitem.parent = pi.name
and pi.name like {txt}""".format(txt=frappe.db.escape(f"%{txt}%"))
if filters and filters.get("item_code"):
if frappe.has_permission("Purchase Invoice", "read"):
# one indexed join, deduped by group_by below
pi_filters.append(["Purchase Invoice Item", "item_code", "=", filters.get("item_code")])
else:
# a select-only caller may use this picker but may not filter on a child table, so resolve the
# parents separately rather than losing the filter to a PermissionError.
parents = frappe.get_all(
"Purchase Invoice Item",
filters={"item_code": filters.get("item_code"), "parenttype": "Purchase Invoice"},
pluck="parent",
distinct=True,
)
pi_filters.append(["name", "in", parents or [""]])
query += " and piitem.item_code = {item_code}".format(
item_code=frappe.db.escape(filters.get("item_code"))
)
# group_by, not distinct: frappe drops ORDER BY from a distinct query on Postgres.
return frappe.get_list(
"Purchase Invoice",
filters=pi_filters,
fields=["name"],
group_by="name",
order_by="name",
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
return frappe.db.sql(query, filters)
@frappe.whitelist()
@@ -1071,28 +983,9 @@ def get_fields(doctype, fields=None):
def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len, filters) -> list:
terms = []
if filters:
reference = filters.get("reference")
if not reference:
return terms
# only a plain name names one document: a filter operator would widen this past the document the
# caller named, and past the one being authorised below.
if not isinstance(reference, str):
frappe.throw(_("Invalid reference"), frappe.PermissionError)
# Payment Schedule is a child table with no permissions of its own, so its parent decides access.
# Prefer the caller's own reference type: the lookup cannot tell two same-named parents apart.
parenttype = filters.get("reference_doctype") or frappe.db.get_value(
"Payment Schedule", {"parent": reference}, "parenttype"
)
if not parenttype:
return terms
frappe.has_permission(parenttype, doc=reference, throw=True)
terms = frappe.db.get_all(
"Payment Schedule",
filters={"parent": reference, "parenttype": parenttype},
filters={"parent": filters.get("reference")},
fields=["payment_term"],
limit=page_len,
as_list=1,
@@ -1103,30 +996,6 @@ def get_payment_terms_for_references(doctype, txt, searchfield, start, page_len,
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters) -> list:
parent = filters.get("parent") if filters else None
# a plain name, never a filter operator: ["like", "%"] would span parents, only one of which is
# the document authorised below.
if not parent or not isinstance(parent, str):
frappe.throw(_("Parent document is required to search child rows"), frappe.PermissionError)
# `doctype` is caller supplied, so it has to be a child table: any other would put the caller's
# filters on a table this query never meant to read.
if not frappe.get_meta(doctype).istable:
frappe.throw(_("{0} is not a child table").format(doctype), frappe.PermissionError)
# child tables carry no permissions of their own, so the parent decides access. Read the parent
# type off the rows, not off `filters`, so the document authorised is the one returned.
parenttype = frappe.db.get_value(doctype, {"parent": parent}, "parenttype")
if not parenttype or not frappe.db.exists(parenttype, parent):
return []
frappe.has_permission(doctype, parent_doctype=parenttype, throw=True)
# and on the parent record itself, so that User Permissions still apply
frappe.has_permission(parenttype, doc=parent, throw=True)
table = frappe.qb.DocType(doctype)
query = (
frappe.get_query(table, filters=filters)
@@ -1151,11 +1020,7 @@ def get_filtered_child_rows(doctype, txt, searchfield, start, page_len, filters)
@frappe.validate_and_sanitize_search_inputs
def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
if frappe.get_single_value("Stock Settings", "allow_uom_with_conversion_rate_defined_in_item"):
item_code = filters.get("item_code")
if not item_code or not frappe.get_list("Item", filters=[["name", "=", item_code]], pluck="name"):
return []
query_filters = {"parent": item_code, "parenttype": "Item"}
query_filters = {"parent": filters.get("item_code")}
if txt:
query_filters["uom"] = ["like", f"%{txt}%"]
@@ -1170,7 +1035,7 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
as_list=1,
)
return frappe.get_list(
return frappe.get_all(
"UOM",
filters={"name": ["like", f"%{txt}%"], "enabled": 1},
fields=["name"],

View File

@@ -12,7 +12,7 @@ from frappe.utils import cint, flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.utils import _get_incoming_rate, get_combine_datetime, get_valuation_method, getdate
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method, getdate
class StockOverReturnError(frappe.ValidationError):
@@ -812,7 +812,7 @@ def get_rate_for_return(
rate = frappe.db.get_value(f"{voucher_type} Item", voucher_detail_no, "incoming_rate")
if not rate and sle:
rate = _get_incoming_rate(
rate = get_incoming_rate(
{
"item_code": sle.item_code,
"warehouse": sle.warehouse,
@@ -1300,38 +1300,14 @@ def get_available_serial_nos(serial_nos, warehouse):
)
# the only doctypes these endpoints are called for; both reach get_value()/get_all() as the doctype itself
RETURNABLE_INVOICE_DOCTYPES = ("Sales Invoice", "POS Invoice")
@frappe.whitelist()
def get_payment_data(invoice):
# `invoice` may be a Sales Invoice or a POS Invoice -- both share the Sales Invoice Payment child
# table -- so resolve which before authorising.
parenttype = frappe.db.get_value("Sales Invoice Payment", {"parent": invoice}, "parenttype")
if not parenttype:
return []
if parenttype not in RETURNABLE_INVOICE_DOCTYPES:
frappe.throw(_("Invalid document type"), frappe.PermissionError)
frappe.has_permission(parenttype, doc=invoice, throw=True)
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
return payment
def validate_returnable_invoice(doctype, invoice):
if doctype not in RETURNABLE_INVOICE_DOCTYPES:
frappe.throw(_("Invalid document type"), frappe.PermissionError)
frappe.has_permission(doctype, doc=invoice, throw=True)
@frappe.whitelist()
def get_invoice_item_returned_qty(doctype, invoice, customer, item_row_name):
validate_returnable_invoice(doctype, invoice)
is_return, docstatus = frappe.db.get_value(doctype, invoice, ["is_return", "docstatus"])
if not is_return and docstatus == 1:
return get_returned_qty_map_for_row(invoice, customer, item_row_name, doctype)
@@ -1339,8 +1315,6 @@ def get_invoice_item_returned_qty(doctype, invoice, customer, item_row_name):
@frappe.whitelist()
def is_invoice_returnable(doctype, invoice):
validate_returnable_invoice(doctype, invoice)
is_return, docstatus, customer = frappe.db.get_value(
doctype, invoice, ["is_return", "docstatus", "customer"]
)

View File

@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return, i
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
from erpnext.stock.utils import _get_incoming_rate, get_combine_datetime, get_valuation_method
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method
class SellingController(StockController):
@@ -573,7 +573,7 @@ class SellingController(StockController):
and self.get("is_return")
)
):
d.incoming_rate = _get_incoming_rate(
d.incoming_rate = get_incoming_rate(
{
"item_code": d.item_code,
"warehouse": d.warehouse,
@@ -904,77 +904,6 @@ class SellingController(StockController):
title=_("Not Allowed"),
)
def validate_produced_serial_nos_against_reservation(self):
"""Restrict delivery to the serial nos reserved for a Sales Order Item with ensure delivery by serial no."""
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
get_sre_reserved_serial_nos_for_voucher_detail_nos,
)
if self.is_return or not frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
return
so_field = "sales_order" if self.doctype == "Sales Invoice" else "against_sales_order"
rows = [d for d in self.items if d.get(so_field) and d.so_detail]
if not rows:
return
flagged_so_details = frappe.get_all(
"Sales Order Item",
filters={
"name": ("in", [d.so_detail for d in rows]),
"ensure_delivery_based_on_produced_serial_no": 1,
},
pluck="name",
)
rows = [d for d in rows if d.so_detail in flagged_so_details]
if not rows:
return
reserved_serial_nos = get_sre_reserved_serial_nos_for_voucher_detail_nos(
"Sales Order", flagged_so_details
)
bundle_map = dict(
frappe.get_all(
rows[0].doctype,
filters={"name": ("in", [d.name for d in rows])},
fields=["name", "serial_and_batch_bundle"],
as_list=True,
)
)
bundle_serial_nos = frappe._dict()
if bundles := [b for b in bundle_map.values() if b]:
for entry in frappe.get_all(
"Serial and Batch Entry",
filters={"parent": ("in", bundles), "serial_no": ("is", "set")},
fields=["parent", "serial_no"],
):
bundle_serial_nos.setdefault(entry.parent, []).append(entry.serial_no)
for row in rows:
if not reserved_serial_nos.get(row.so_detail):
frappe.throw(
_(
"Row #{0}: Delivery of Item {1} is ensured by produced Serial No, but no Serial No is reserved against Sales Order {2}. Reserve the produced Serial Nos from the Sales Order."
).format(row.idx, frappe.bold(row.item_code), frappe.bold(row.get(so_field))),
title=_("Serial No Not Reserved"),
)
bundle = bundle_map.get(row.name)
serial_nos = bundle_serial_nos.get(bundle, []) if bundle else get_serial_nos(row.serial_no)
if invalid_serial_nos := [
sn for sn in serial_nos if sn not in reserved_serial_nos[row.so_detail]
]:
frappe.throw(
_(
"Row #{0}: Serial No {1} is not reserved against Sales Order {2}. Deliver only the Serial Nos produced and reserved for it."
).format(
row.idx, frappe.bold(", ".join(invalid_serial_nos)), frappe.bold(row.get(so_field))
),
title=_("Serial No Not Reserved"),
)
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""

View File

@@ -21,7 +21,7 @@ from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle impor
)
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation, get_serial_nos_from_bundle
from erpnext.stock.utils import _get_incoming_rate
from erpnext.stock.utils import get_incoming_rate
class SubcontractingController(StockController):
@@ -99,7 +99,7 @@ class SubcontractingController(StockController):
}
)
rate = _get_incoming_rate(kwargs)
rate = get_incoming_rate(kwargs)
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate")
if flt(rate, precision) != flt(row.rate, precision):
row.rate = rate
@@ -859,7 +859,7 @@ class SubcontractingController(StockController):
args["batch_no"] = rm_obj.batch_no
args["serial_no"] = rm_obj.serial_no
rm_obj.rate = _get_incoming_rate(args)
rm_obj.rate = get_incoming_rate(args)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
@@ -1068,7 +1068,7 @@ class SubcontractingController(StockController):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f"The Batch No {frappe.bold(row.get('batch_no'))} has not supplied against the {self.subcontract_data.order_doctype} {link}"
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
@@ -1247,7 +1247,7 @@ class SubcontractingController(StockController):
and reset_outgoing_rate
and frappe.get_cached_value("Item", item.rm_item_code, "is_stock_item")
):
rate = _get_incoming_rate(
rate = get_incoming_rate(
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,

View File

@@ -1,59 +0,0 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# For license information, please see license.txt
from unittest.mock import Mock, patch
import frappe
from frappe.tests import UnitTestCase
from erpnext.controllers.accounts_controller import AccountsController
class TestPriceListCurrency(UnitTestCase):
def test_price_list_currency_transition(self):
cases = (
("USD", "CDF", 1, 0.000444444, True),
("EUR", "CDF", 1.2, 0.000444444, True),
("CDF", "USD", 0.000444444, 1, False),
("CDF", "CDF", 0.0005, 0.0005, False),
("CDF", "CDF", 0, 0.000444444, True),
(None, "CDF", 0.0005, 0.0005, False),
)
for direction in ("Selling", "Buying"):
for previous_currency, currency, previous_rate, expected_rate, fetch_rate in cases:
with self.subTest(
direction=direction,
previous_currency=previous_currency,
currency=currency,
previous_rate=previous_rate,
):
doc = frappe._dict(
meta=Mock(),
posting_date="2026-09-18",
selling_price_list="New Selling Price List",
buying_price_list="New Buying Price List",
price_list_currency=previous_currency,
plc_conversion_rate=previous_rate,
company_currency="USD",
currency="CDF",
conversion_rate=0.000444444,
)
with (
patch("erpnext.controllers.accounts_controller.frappe") as mock_frappe,
patch(
"erpnext.controllers.accounts_controller.get_exchange_rate",
return_value=0.000444444,
) as exchange_rate,
):
mock_frappe.db.get_value.return_value = currency
mock_frappe.db.get_single_value.return_value = False
AccountsController.set_price_list_currency(doc, direction)
self.assertEqual(doc.price_list_currency, currency)
self.assertEqual(doc.plc_conversion_rate, expected_rate)
self.assertEqual(doc.conversion_rate, 0.000444444)
if fetch_rate:
exchange_rate.assert_called_once_with(
currency, "USD", "2026-09-18", f"for_{direction.lower()}"
)
else:
exchange_rate.assert_not_called()

View File

@@ -1,93 +0,0 @@
import frappe
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import ERPNextTestSuite
class TestPriceListValidation(ERPNextTestSuite):
def create_price_list(self, selling=0, buying=0, enabled=1):
return (
frappe.get_doc(
{
"doctype": "Price List",
"price_list_name": frappe.generate_hash(length=10),
"currency": "INR",
"selling": selling,
"buying": buying,
"enabled": enabled,
}
)
.insert()
.name
)
def test_selling_transaction_should_reject_a_buying_price_list(self):
invoice = create_sales_invoice(do_not_save=1)
invoice.selling_price_list = self.create_price_list(buying=1)
with self.assertRaisesRegex(frappe.ValidationError, "selling transaction"):
invoice.save()
def test_buying_transaction_should_reject_a_selling_price_list(self):
invoice = make_purchase_invoice(do_not_save=1)
invoice.buying_price_list = self.create_price_list(selling=1)
with self.assertRaisesRegex(frappe.ValidationError, "buying transaction"):
invoice.save()
def test_a_price_list_for_both_sides_should_be_accepted(self):
price_list = self.create_price_list(selling=1, buying=1)
invoice = create_sales_invoice(do_not_save=1)
invoice.selling_price_list = price_list
invoice.save()
self.assertEqual(invoice.selling_price_list, price_list)
def test_a_missing_price_list_should_report_rather_than_crash(self):
invoice = create_sales_invoice(do_not_save=1)
invoice.selling_price_list = frappe.generate_hash(length=10)
with self.assertRaises(frappe.ValidationError):
invoice.validate_price_list()
def test_internal_transfer_should_keep_the_outward_price_list(self):
"""The inward document of an internal transfer takes the price list of the outward one, which
is flagged for the opposite side."""
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
prepare_data_for_internal_transfer,
)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
prepare_data_for_internal_transfer()
company = "_Test Company with perpetual inventory"
selling_only = self.create_price_list(selling=1)
delivery_note = create_delivery_note(
company=company,
customer="_Test Internal Customer 2",
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
warehouse="Stores - TCP1",
target_warehouse=create_warehouse("_Test Transit For Price List", company=company),
do_not_submit=1,
)
delivery_note.selling_price_list = selling_only
delivery_note.save()
delivery_note.submit()
receipt = make_inter_company_purchase_receipt(delivery_note.name)
receipt.items[0].warehouse = "Stores - TCP1"
receipt.save()
self.assertEqual(receipt.buying_price_list, selling_only)
def test_disabled_price_list_should_still_report_as_disabled(self):
invoice = create_sales_invoice(do_not_save=1)
invoice.selling_price_list = self.create_price_list(selling=1, enabled=0)
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
invoice.save()

View File

@@ -543,7 +543,7 @@
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2026-02-10 14:36:37.157961",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -600,10 +600,6 @@
"read": 1,
"report": 1,
"role": "Sales User"
},
{
"role": "Support Team",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -484,14 +484,6 @@ def get_lead_details(lead, posting_date=None, company=None, doctype=None):
def make_lead_from_communication(communication: str, ignore_communication_links: bool = False):
"""raise a issue from email"""
# Communication grants read to `All` only for the owner and carries a has_permission hook, so doc=
# is what decides access.
frappe.has_permission("Communication", doc=communication, throw=True)
# both paths end in a Lead: the insert path checks `create`, but the path reusing an existing Lead
# required nothing at all.
frappe.has_permission("Lead", ptype="create", throw=True)
doc = frappe.get_doc("Communication", communication)
lead_name = None
if doc.sender:

View File

@@ -18,7 +18,7 @@
}
],
"links": [],
"modified": "2026-09-16 11:42:07.000000",
"modified": "2025-12-17 12:09:34.687368",
"modified_by": "Administrator",
"module": "CRM",
"name": "Market Segment",
@@ -36,10 +36,6 @@
"role": "Sales Manager",
"share": 1,
"write": 1
},
{
"role": "Sales Master Manager",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -285,7 +285,7 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
}
if (this.frm.is_new() && this.frm.doc.opportunity_type === undefined) {
this.frm.doc.opportunity_type = "Sales";
this.frm.doc.opportunity_type = __("Sales");
}
this.setup_queries();
}

View File

@@ -167,7 +167,7 @@ class Opportunity(TransactionBase, CRMNote):
def set_opportunity_type(self):
if self.is_new() and not self.opportunity_type:
self.opportunity_type = "Sales"
self.opportunity_type = _("Sales")
def set_exchange_rate(self):
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
@@ -555,13 +555,8 @@ def make_opportunity_from_communication(
):
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
# Communication grants read to `All` only for the owner and carries a has_permission hook, so doc=
# is what decides access.
frappe.has_permission("Communication", doc=communication, throw=True)
doc = frappe.get_doc("Communication", communication)
# make_lead_from_communication() checks, but is skipped when the email already references a Lead.
lead = doc.reference_name if doc.reference_doctype == "Lead" else None
if not lead:
lead = make_lead_from_communication(communication, ignore_communication_links=True)

View File

@@ -150,10 +150,6 @@ def link_open_events(ref_doctype, ref_docname, doc):
@frappe.whitelist()
def get_open_activities(ref_doctype, ref_docname):
# both arguments are caller supplied and the ToDo/Event rows are read with get_all, so the
# referenced document decides who may see its activities. doc= applies User Permissions.
frappe.has_permission(ref_doctype, doc=ref_docname, throw=True)
tasks = get_open_todos(ref_doctype, ref_docname)
events = get_open_events(ref_doctype, ref_docname)
tasks_history = get_closed_todos(ref_doctype, ref_docname)

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