Compare commits

..

41 Commits

Author SHA1 Message Date
Frappe PR Bot
26f0687834 chore(release): Bumped to Version 15.121.3
## [15.121.3](https://github.com/frappe/erpnext/compare/v15.121.2...v15.121.3) (2026-09-15)

### Bug Fixes

* % Amount Billed exceeds 100% on fully rejected Purchase Receipts ([#58897](https://github.com/frappe/erpnext/issues/58897)) ([c31ae57](c31ae577e4))
* **accounts:** add permission checks on `execute_repost_payment_ledger` (backport [#58980](https://github.com/frappe/erpnext/issues/58980)) ([#58981](https://github.com/frappe/erpnext/issues/58981)) ([85a1529](85a152946e))
* **accounts:** add permission checks on `invoice_discounting.get_invoices` (backport [#58975](https://github.com/frappe/erpnext/issues/58975)) ([#58989](https://github.com/frappe/erpnext/issues/58989)) ([62b9f31](62b9f31297))
* **accounts:** reject disabled price lists in buying and selling ([#58891](https://github.com/frappe/erpnext/issues/58891)) ([47662b9](47662b9af2))
* **accounts:** stop counting received qty on non-stock invoice returns (backport [#58924](https://github.com/frappe/erpnext/issues/58924)) ([#58928](https://github.com/frappe/erpnext/issues/58928)) ([228108a](228108ad4c))
* Backfill `not_applicable` on Item Tax Template Details for German companies (backport [#54682](https://github.com/frappe/erpnext/issues/54682)) (backport [#54686](https://github.com/frappe/erpnext/issues/54686)) ([#58952](https://github.com/frappe/erpnext/issues/58952)) ([f943368](f9433683ec))
* keep a dunning claimable until its interest is paid too ([11c6f0d](11c6f0d02c))
* **manufacturing:** handle empty raw materials in workstation ([#58927](https://github.com/frappe/erpnext/issues/58927)) ([c72bc69](c72bc6925b))
* prevent disabled items from being used in bom ([#58997](https://github.com/frappe/erpnext/issues/58997)) ([7429a26](7429a260f6))
* reset price list fallback when changing parties ([#58893](https://github.com/frappe/erpnext/issues/58893)) ([f981791](f981791012))
* resolve conflicts ([e84a946](e84a94642d))
* resolve conflicts ([00729a8](00729a8d62))
* **selling:** add email permission check on `send_emails` (backport [#58935](https://github.com/frappe/erpnext/issues/58935)) ([#58937](https://github.com/frappe/erpnext/issues/58937)) ([e37ba3f](e37ba3f8cf))
* **selling:** exclude fully billed orders from the invoice picker (backport [#58966](https://github.com/frappe/erpnext/issues/58966)) ([#58968](https://github.com/frappe/erpnext/issues/58968)) ([52ea7ed](52ea7ed606))
* **stock:** distribute additional costs when incoming items have no value ([10987b1](10987b11cc))
* **stock:** do not rebuild the Warehouse tree on an account map miss (backport [#58956](https://github.com/frappe/erpnext/issues/58956)) ([#58972](https://github.com/frappe/erpnext/issues/58972)) ([f59a6ec](f59a6ec882))
* **stock:** guard inventory dimension lookup without reference document ([#58889](https://github.com/frappe/erpnext/issues/58889)) ([7ee54ba](7ee54ba08b))
* **stock:** preserve rates for unsaved mapped rows (backport [#58852](https://github.com/frappe/erpnext/issues/58852)) ([#58898](https://github.com/frappe/erpnext/issues/58898)) ([32d8c35](32d8c359ed))
* **stock:** recalculate billing status of returned delivery notes (backport [#58953](https://github.com/frappe/erpnext/issues/58953)) ([#58986](https://github.com/frappe/erpnext/issues/58986)) ([563615c](563615c77e))
* **stock:** remove empty delivery stops after mapping ([#58896](https://github.com/frappe/erpnext/issues/58896)) ([0099e7e](0099e7ec4f))
* **stock:** set item_name to fetch if empty in packing slip item ([#58925](https://github.com/frappe/erpnext/issues/58925)) ([99afc2e](99afc2e6eb))
* **stock:** set item_name to fetch if empty in packing slip item ([#58925](https://github.com/frappe/erpnext/issues/58925)) ([9ba2159](9ba2159720))
* **stock:** show actual outgoing rate in ledger preview ([#58700](https://github.com/frappe/erpnext/issues/58700)) ([372244e](372244e6b2))
* **subcontracting:** validate project across the subcontracting flow ([#58965](https://github.com/frappe/erpnext/issues/58965)) ([1150d8e](1150d8e9b0))
* user not able to set valuation rate zero in stock reco (backport [#58800](https://github.com/frappe/erpnext/issues/58800)) ([#58821](https://github.com/frappe/erpnext/issues/58821)) ([3bd194e](3bd194e986))
* validate shelf life for automatically created expiring batches ([#58911](https://github.com/frappe/erpnext/issues/58911)) ([00e02f0](00e02f07ac))
2026-09-15 18:35:09 +00:00
Diptanil Saha
f2cfe30596 Merge pull request #59072 from frappe/version-15-hotfix
chore: release v15
2026-09-16 00:03:25 +05:30
Frappe PR Bot
df8b7f9648 chore(release): Bumped to Version 15.121.2
## [15.121.2](https://github.com/frappe/erpnext/compare/v15.121.1...v15.121.2) (2026-09-09)

### Bug Fixes

* **stock:** preserve rates for unsaved mapped rows (backport [#58852](https://github.com/frappe/erpnext/issues/58852)) (backport [#58898](https://github.com/frappe/erpnext/issues/58898)) ([#58916](https://github.com/frappe/erpnext/issues/58916)) ([0deed27](0deed27cfe))
2026-09-09 09:24:59 +00:00
mergify[bot]
0deed27cfe fix(stock): preserve rates for unsaved mapped rows (backport #58852) (backport #58898) (#58916)
fix(stock): preserve rates for unsaved mapped rows (backport #58852) (#58898)

* fix: keep each row's own rate under maintain same rate

* test: cover maintain-same-rate rate lock for batch items

(cherry picked from commit 32d8c359ed)

Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
2026-09-09 14:53:20 +05:30
Frappe PR Bot
fdc48277a8 chore(release): Bumped to Version 15.121.1
## [15.121.1](https://github.com/frappe/erpnext/compare/v15.121.0...v15.121.1) (2026-09-08)

### Bug Fixes

* **accounts:** reject same-account internal transfers ([#58529](https://github.com/frappe/erpnext/issues/58529)) ([05a1127](05a1127587))
* **accounts:** round journal entry totals to field precision (backport [#58629](https://github.com/frappe/erpnext/issues/58629)) ([#58874](https://github.com/frappe/erpnext/issues/58874)) ([96357b4](96357b4fdc))
* **accounts:** support drop-shipped items in Gross Profit report ([#58686](https://github.com/frappe/erpnext/issues/58686)) ([6c6ee7c](6c6ee7c524))
* add reconciliation after submit logic for bank transactions ([#57330](https://github.com/frappe/erpnext/issues/57330)) ([2ee5f6b](2ee5f6b1c4))
* **batch:** show Expired status only after expiry date has passed ([#58736](https://github.com/frappe/erpnext/issues/58736)) ([cd93007](cd93007780))
* check material request price list permission ([#58740](https://github.com/frappe/erpnext/issues/58740)) ([a348d41](a348d413d0))
* check write permission in whitelisted document methods (backport [#58689](https://github.com/frappe/erpnext/issues/58689)) ([#58702](https://github.com/frappe/erpnext/issues/58702)) ([baa30e7](baa30e703d))
* **crm:** check read permission on lead in add_lead_to_prospect ([4dd56ec](4dd56ec4e6))
* **crm:** validate contact email before saving an email campaign (backport [#58667](https://github.com/frappe/erpnext/issues/58667)) ([#58671](https://github.com/frappe/erpnext/issues/58671)) ([1ea597d](1ea597d6b5))
* fetch accounts by company (backport [#58523](https://github.com/frappe/erpnext/issues/58523)) ([#58878](https://github.com/frappe/erpnext/issues/58878)) ([2c2a3e9](2c2a3e9f10))
* filter cancelled BOMs in BOM Stock Analysis ([#58647](https://github.com/frappe/erpnext/issues/58647)) ([fff2616](fff261680c))
* **gross-profit:** include item_name in export ([#58631](https://github.com/frappe/erpnext/issues/58631)) ([c752bb3](c752bb34a8))
* include payment deductions in sales/purchase register ledger bal… (backport [#58437](https://github.com/frappe/erpnext/issues/58437)) ([#58679](https://github.com/frappe/erpnext/issues/58679)) ([24e7ff5](24e7ff55de))
* persist rendered terms on transaction save ([#58858](https://github.com/frappe/erpnext/issues/58858)) ([24cac30](24cac30aa9))
* **pos:** use company-currency change amount when netting pos gl entries (backport [#58599](https://github.com/frappe/erpnext/issues/58599)) ([#58677](https://github.com/frappe/erpnext/issues/58677)) ([c7d865e](c7d865ef3b))
* resolve conflicts ([95b2030](95b203079a))
* **selling:** fetch orders within billing allowance (backport [#58751](https://github.com/frappe/erpnext/issues/58751)) ([#58822](https://github.com/frappe/erpnext/issues/58822)) ([a64b78d](a64b78d283))
* **setup:** strict permissions for transaction deletion record (backport [#58687](https://github.com/frappe/erpnext/issues/58687)) ([#58722](https://github.com/frappe/erpnext/issues/58722)) ([934c7d8](934c7d833c))
* show cost center for unmatched payments in receivables (backport [#58453](https://github.com/frappe/erpnext/issues/58453)) ([#58880](https://github.com/frappe/erpnext/issues/58880)) ([69f38a0](69f38a04c8))
* skip zero quantity items in production plan material requests ([#58642](https://github.com/frappe/erpnext/issues/58642)) ([3217a69](3217a69fc2))
* **stock:** recalculate delivery note billing after return ([#58869](https://github.com/frappe/erpnext/issues/58869)) ([b409ded](b409ded2f6))

### Reverts

* **manufacturing:** remove material coverage changes on version 15 ([#58715](https://github.com/frappe/erpnext/issues/58715)) ([7cbf8b8](7cbf8b8660))
2026-09-08 13:48:11 +00:00
Diptanil Saha
8635652c90 Merge pull request #58867 from frappe/version-15-hotfix
chore: release v15
2026-09-08 19:16:20 +05:30
Frappe PR Bot
1a0bf0bf6c chore(release): Bumped to Version 15.121.0
# [15.121.0](https://github.com/frappe/erpnext/compare/v15.120.0...v15.121.0) (2026-09-02)

### Bug Fixes

* **accounts:** set pos profile on invoices respecting user permissions (backport [#58508](https://github.com/frappe/erpnext/issues/58508)) ([#58518](https://github.com/frappe/erpnext/issues/58518)) ([1187fb8](1187fb8e01))
* **accounts:** validate frozen accounts in period closing voucher ([#58477](https://github.com/frappe/erpnext/issues/58477)) ([045d708](045d7086d1))
* add missing perm checks on various whitelisted methods (backport [#58652](https://github.com/frappe/erpnext/issues/58652)) ([#58653](https://github.com/frappe/erpnext/issues/58653)) ([547d82d](547d82da56))
* add type hints to make_request_for_quotation ([567730e](567730e38e))
* **bank_statement_import:** add missing permission check on multiple whitelisted methods (backport [#58221](https://github.com/frappe/erpnext/issues/58221)) ([#58223](https://github.com/frappe/erpnext/issues/58223)) ([fa903f4](fa903f40fd))
* clarify duplicate internal party messages ([#58469](https://github.com/frappe/erpnext/issues/58469)) ([17208e7](17208e7434))
* **company:** throw if linked to demo_company field ([de260bd](de260bd43c))
* **crm:** add missing permission validation on `get_contract_template` (backport [#58621](https://github.com/frappe/erpnext/issues/58621)) ([#58622](https://github.com/frappe/erpnext/issues/58622)) ([d1b615b](d1b615b851))
* **crm:** check write permission in edit_note ([2b2a005](2b2a005682))
* do not map the same row twice in "Get Items From" (backport [#58617](https://github.com/frappe/erpnext/issues/58617)) ([#58618](https://github.com/frappe/erpnext/issues/58618)) ([28175a5](28175a5c9d))
* filter fully ordered items when creating RFQ from Material Request ([08ec311](08ec31174c))
* keep Currency and Price List section open for foreign currency (backport [#58074](https://github.com/frappe/erpnext/issues/58074)) ([#58112](https://github.com/frappe/erpnext/issues/58112)) ([f5ce20f](f5ce20f122))
* keep source rate on re-fetch when maintain same rate is enabled (backport [#57479](https://github.com/frappe/erpnext/issues/57479)) ([#58332](https://github.com/frappe/erpnext/issues/58332)) ([228ab2d](228ab2d97e)), closes [frappe/erpnext#57436](https://github.com/frappe/erpnext/issues/57436)
* **manufacturing:** preserve job card qty in mr transfer (backport [#58482](https://github.com/frappe/erpnext/issues/58482)) ([#58498](https://github.com/frappe/erpnext/issues/58498)) ([099a5a7](099a5a71d9))
* **manufacturing:** use packed row delivery date ([#58569](https://github.com/frappe/erpnext/issues/58569)) ([9c19b02](9c19b024db))
* mark item tax templates as not applicable (backport [#54673](https://github.com/frappe/erpnext/issues/54673)) (backport [#54677](https://github.com/frappe/erpnext/issues/54677)) ([#58462](https://github.com/frappe/erpnext/issues/58462)) ([d62f6e7](d62f6e715f))
* persist redistributed additional costs during stock entry repost (backport [#58433](https://github.com/frappe/erpnext/issues/58433)) ([#58532](https://github.com/frappe/erpnext/issues/58532)) ([c4e7cfa](c4e7cfa6a0))
* **projects:** check read permission on source in create_duplicate_project ([af8c010](af8c010aa8))
* **projects:** ignore cancelled invoices in timesheet portal (backport [#58501](https://github.com/frappe/erpnext/issues/58501)) ([#58503](https://github.com/frappe/erpnext/issues/58503)) ([5a51895](5a518951e4))
* reset hardcoded letter head on Incorrect Serial and Batch Bundle report ([ccdaa92](ccdaa92de8))
* resolve analytics backport conflicts ([605918d](605918d3a2))
* resolve conflicts ([c6717fb](c6717fb758))
* resolve stock entry backport conflicts ([e126c45](e126c4597b))
* round party balances to currency precision in Trial Balance for Party ([b98d193](b98d193b77))
* **selling:** check quotation write permission before marking lost ([#58493](https://github.com/frappe/erpnext/issues/58493)) ([33ebc7d](33ebc7d7e3))
* **selling:** check sales order permission before work order creation ([#58492](https://github.com/frappe/erpnext/issues/58492)) ([0609b06](0609b06564))
* **sms_settings:** add patch to pre-fill roles into SMS Settings Roles Table ([0cef90d](0cef90d079))
* **stock:** keep pick list links when refetching stock entry items ([22377fa](22377fa86d))
* **stock:** load available serial no report (backport [#58558](https://github.com/frappe/erpnext/issues/58558)) ([#58561](https://github.com/frappe/erpnext/issues/58561)) ([2d46bb9](2d46bb962c))
* **stock:** preserve exchange gain loss journals in lcv ([#58505](https://github.com/frappe/erpnext/issues/58505)) ([c6fedf4](c6fedf4aca))
* **stock:** reset bin when a repost finds no stock ledger entries  ([#58434](https://github.com/frappe/erpnext/issues/58434)) ([b00062d](b00062df86)), closes [#58362](https://github.com/frappe/erpnext/issues/58362)
* **stock:** validate serial inventory dimensions ([#58394](https://github.com/frappe/erpnext/issues/58394)) ([bc071d7](bc071d7793))
* sum item and its alternate transferred qty on work order required items ([5851bb3](5851bb3b6e))
* **taxes:** skip tax addition for invoice created from opening invoice tool ([7090c6f](7090c6fdab))
* **timesheet:** scoping whitelisted methods output to projects and timesheets that are acccessible to users (backport [#58267](https://github.com/frappe/erpnext/issues/58267)) ([#58576](https://github.com/frappe/erpnext/issues/58576)) ([c656497](c656497aac))
* translate doctype in map msg (backport [#58515](https://github.com/frappe/erpnext/issues/58515)) ([#58516](https://github.com/frappe/erpnext/issues/58516)) ([d87a4b2](d87a4b2541))
* translate label in party validation (backport [#58624](https://github.com/frappe/erpnext/issues/58624)) ([#58625](https://github.com/frappe/erpnext/issues/58625)) ([307ffcd](307ffcd5c3))
* validate items against source Sales Order in Material Request ([32d44c6](32d44c6e91))
* validate POS Settings invoice and search fields on the server (backport [#58611](https://github.com/frappe/erpnext/issues/58611)) ([#58612](https://github.com/frappe/erpnext/issues/58612)) ([c5bc5f9](c5bc5f9c31))
* Work Order picks wrong Delivery Date when Sales Order has the same item in multiple rows ([#58527](https://github.com/frappe/erpnext/issues/58527)) ([7454735](7454735595))

### Features

* **analytics:** filter sales and purchase analytics by entity ([#58402](https://github.com/frappe/erpnext/issues/58402)) ([0dddd00](0dddd00073))
2026-09-02 07:16:53 +00:00
Diptanil Saha
57266ab474 Merge pull request #58646 from frappe/version-15-hotfix
chore: release v15
2026-09-02 12:45:04 +05:30
Frappe PR Bot
945e825bee chore(release): Bumped to Version 15.120.0
# [15.120.0](https://github.com/frappe/erpnext/compare/v15.119.3...v15.120.0) (2026-08-25)

### Bug Fixes

* **accounts:** disallow reversing a reverse journal entry ([15041a6](15041a62dd))
* **accounts:** supplier group filter not applied on accounts payable report ([743f7d8](743f7d8713))
* aggregate child warehouses in Stock Qty vs Serial No Count report (backport [#58134](https://github.com/frappe/erpnext/issues/58134)) ([#58365](https://github.com/frappe/erpnext/issues/58365)) ([61238e7](61238e7c4c))
* allow custom remark on reversal journal entry ([#58308](https://github.com/frappe/erpnext/issues/58308)) ([b578fb5](b578fb52d5))
* asset scrap flow related changes (backport [#55126](https://github.com/frappe/erpnext/issues/55126)) ([#58144](https://github.com/frappe/erpnext/issues/58144)) ([167cc1e](167cc1e5b9))
* **assets:** split FIFO/LIFO rate across grouped stock item rows ([c2c4e5e](c2c4e5ee8b))
* attributeerror on budget against a non profit and loss account ([2095411](2095411a28))
* **bank_statement_import:** add missing permission check on `get_import_status` (backport [#58217](https://github.com/frappe/erpnext/issues/58217)) ([#58218](https://github.com/frappe/erpnext/issues/58218)) ([1464a34](1464a34fc6))
* block disabled/frozen party on Opportunity and Request for Quotation (backport [#57983](https://github.com/frappe/erpnext/issues/57983)) ([#58034](https://github.com/frappe/erpnext/issues/58034)) ([88a36a8](88a36a800a))
* **buying:** allow purchase returns against a closed purchase order ([#58140](https://github.com/frappe/erpnext/issues/58140)) ([81c5393](81c53931b5))
* correct Item Group doctype name in item tax template dashboard  (backport [#58192](https://github.com/frappe/erpnext/issues/58192)) ([#58212](https://github.com/frappe/erpnext/issues/58212)) ([46d883d](46d883d00d))
* **crm_settings:** create custom fields for Frappe CRM on enabling synchronization ([eb85ca6](eb85ca68f6))
* don't set work order status to In Process only due to skip material transfer ([#58246](https://github.com/frappe/erpnext/issues/58246)) ([73b7ec3](73b7ec32b6))
* drop removed Restaurant doctype from sales tax template dashboard  (backport [#58191](https://github.com/frappe/erpnext/issues/58191)) ([#58210](https://github.com/frappe/erpnext/issues/58210)) ([32a5b23](32a5b23b3c))
* **email_digest:** added permission check for `get_msg_html` (backport [#58197](https://github.com/frappe/erpnext/issues/58197)) ([#58199](https://github.com/frappe/erpnext/issues/58199)) ([c6211eb](c6211eb075))
* escape interpolated values in portal, print and desk templates (backport [#58273](https://github.com/frappe/erpnext/issues/58273)) ([#58277](https://github.com/frappe/erpnext/issues/58277)) ([37f2770](37f2770809))
* escape interpolated values in text positions across portal and desk templates (backport [#58286](https://github.com/frappe/erpnext/issues/58286)) ([#58287](https://github.com/frappe/erpnext/issues/58287)) ([1210c61](1210c6187d))
* escape on status image for workstations in production status (backport [#58279](https://github.com/frappe/erpnext/issues/58279)) ([#58280](https://github.com/frappe/erpnext/issues/58280)) ([d047caf](d047caf4aa))
* filter available batch report by company (backport [#57995](https://github.com/frappe/erpnext/issues/57995)) ([#58076](https://github.com/frappe/erpnext/issues/58076)) ([8c8a4b6](8c8a4b6f20))
* fix conflicts ([f8e614f](f8e614f0c7))
* get items from sales order in sales invoice (backport [#58163](https://github.com/frappe/erpnext/issues/58163)) ([#58187](https://github.com/frappe/erpnext/issues/58187)) ([89d3701](89d3701e3b))
* hide rfq status in supplier portal ([#58368](https://github.com/frappe/erpnext/issues/58368)) ([2dbd224](2dbd224643))
* hide supplier name in rfq portal (backport [#58373](https://github.com/frappe/erpnext/issues/58373)) ([#58375](https://github.com/frappe/erpnext/issues/58375)) ([91fc999](91fc999575))
* ignore historical negative batch stock in outward validation (backport [#58148](https://github.com/frappe/erpnext/issues/58148)) ([#58150](https://github.com/frappe/erpnext/issues/58150)) ([9cc9aa0](9cc9aa0fa5))
* include time logs ending at midnight in timesheet billing summary (backport [#58355](https://github.com/frappe/erpnext/issues/58355)) ([#58356](https://github.com/frappe/erpnext/issues/58356)) ([3d4245b](3d4245b9b4))
* **italy:** handle none price_list_rate in e-invoice xml generation (backport [#58242](https://github.com/frappe/erpnext/issues/58242)) ([#58369](https://github.com/frappe/erpnext/issues/58369)) ([cb8ae93](cb8ae93fa3))
* **manufacturing:** derive material transfers from actual coverage ([#58114](https://github.com/frappe/erpnext/issues/58114)) ([a1ecea1](a1ecea1794))
* **manufacturing:** fall back to item group defaults for work order w… ([#58236](https://github.com/frappe/erpnext/issues/58236)) ([48bd313](48bd3139f3))
* mirror rounding adjustment on distributed_discount_amount (backport [#58047](https://github.com/frappe/erpnext/issues/58047)) ([#58054](https://github.com/frappe/erpnext/issues/58054)) ([b03e098](b03e098684))
* new docs should refetch incoming rates (backport [#58097](https://github.com/frappe/erpnext/issues/58097)) ([#58294](https://github.com/frappe/erpnext/issues/58294)) ([fd82c7d](fd82c7d691))
* **party_ledger_summary:** added missing filters for `cost_center` and `projects` ([#58411](https://github.com/frappe/erpnext/issues/58411)) ([d4815cb](d4815cb231))
* patch to delete the `crm_deal` custom fields ([ff1a191](ff1a1914a1))
* prevent duplicate supplier quotations from portal ([e1246ae](e1246ae95d))
* remove ignore_permissions from get_party_details signature ([#55491](https://github.com/frappe/erpnext/issues/55491)) ([4cd39aa](4cd39aa147))
* render missing terms before printing ([#58367](https://github.com/frappe/erpnext/issues/58367)) ([b33475e](b33475e7cf))
* respect zero currency precision ([#58395](https://github.com/frappe/erpnext/issues/58395)) ([6a84621](6a8462116b))
* **stock:** confirm before changing item qty from the batch selector (backport [#58123](https://github.com/frappe/erpnext/issues/58123)) ([#58124](https://github.com/frappe/erpnext/issues/58124)) ([954a5ec](954a5ec006))
* **stock:** fetch item stock UOM in stock reconciliation (backport [#58284](https://github.com/frappe/erpnext/issues/58284)) ([#58290](https://github.com/frappe/erpnext/issues/58290)) ([7883f59](7883f595d7))
* **stock:** honour pick serial / batch based on in the batch selector (backport [#58176](https://github.com/frappe/erpnext/issues/58176)) ([#58181](https://github.com/frappe/erpnext/issues/58181)) ([6d06b43](6d06b43434))
* update stock variance account logic which defaults to default expense (backport [#57656](https://github.com/frappe/erpnext/issues/57656)) ([#57675](https://github.com/frappe/erpnext/issues/57675)) ([c132b99](c132b99b4e))
* use user data fields hook (backport [#58274](https://github.com/frappe/erpnext/issues/58274)) ([#58282](https://github.com/frappe/erpnext/issues/58282)) ([74c3eea](74c3eeaa44))
* validation for task end date check ([8dc9919](8dc9919691))

### Features

* **accounts:** opt-in 'Consider Accounting Dimension' filter on General Ledger Report (backport [#58156](https://github.com/frappe/erpnext/issues/58156)) ([#58157](https://github.com/frappe/erpnext/issues/58157)) ([e704e58](e704e589ca))
* add status filter to Supplier Quotation Comparison report ([9e6a166](9e6a16658d))
* Belgian Charts of Accounts (commercial + non-profit, FR + NL) (backport [#54679](https://github.com/frappe/erpnext/issues/54679)) ([#58185](https://github.com/frappe/erpnext/issues/58185)) ([4c9a76e](4c9a76ef9f))
* sync serial no status from stock ledger in Stock Qty vs Serial No Count report (version-15-hotfix) ([#57865](https://github.com/frappe/erpnext/issues/57865)) ([faaaa07](faaaa0776d))
* taxable-base resolver hook for custom charge types ([#56175](https://github.com/frappe/erpnext/issues/56175)) ([8c9e941](8c9e941614))
* validate purchase receipt exchange rate parity on purchase invoice (backport [#58177](https://github.com/frappe/erpnext/issues/58177)) ([#58189](https://github.com/frappe/erpnext/issues/58189)) ([a5f4d3a](a5f4d3abeb))
2026-08-25 16:59:18 +00:00
Diptanil Saha
5d60bd9f54 Merge pull request #58396 from frappe/version-15-hotfix
chore: release v15
2026-08-25 22:27:36 +05:30
Frappe PR Bot
0526834071 chore(release): Bumped to Version 15.119.3
## [15.119.3](https://github.com/frappe/erpnext/compare/v15.119.2...v15.119.3) (2026-08-18)

### Bug Fixes

* get items from sales order in sales invoice (backport [#58163](https://github.com/frappe/erpnext/issues/58163)) (backport [#58187](https://github.com/frappe/erpnext/issues/58187)) ([#58259](https://github.com/frappe/erpnext/issues/58259)) ([88f32f1](88f32f1d1b))
2026-08-18 07:37:05 +00:00
mergify[bot]
88f32f1d1b fix: get items from sales order in sales invoice (backport #58163) (backport #58187) (#58259)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-08-18 07:35:00 +00:00
Frappe PR Bot
d707cb1e0e chore(release): Bumped to Version 15.119.2
## [15.119.2](https://github.com/frappe/erpnext/compare/v15.119.1...v15.119.2) (2026-08-14)

### Bug Fixes

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

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

(cherry picked from commit 9239d1c2a3)


(cherry picked from commit 9cc9aa0fa5)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2026-08-14 11:48:34 +05:30
Frappe PR Bot
be176617cc chore(release): Bumped to Version 15.119.1
## [15.119.1](https://github.com/frappe/erpnext/compare/v15.119.0...v15.119.1) (2026-08-11)

### Bug Fixes

* **accounts receivable:** made territory field multi select (backport [#57322](https://github.com/frappe/erpnext/issues/57322)) ([#57808](https://github.com/frappe/erpnext/issues/57808)) ([69c00b3](69c00b3362))
* allow selecting a warehouse for new items in the update items dialog ([#57876](https://github.com/frappe/erpnext/issues/57876)) ([76f485b](76f485ba43))
* clear deferred revenue/expense fields on uncheck (backport [#57140](https://github.com/frappe/erpnext/issues/57140)) ([ce898a9](ce898a9695))
* **coa_importer:** added server-side validations for importing chart of accounts (backport [#58065](https://github.com/frappe/erpnext/issues/58065)) ([#58067](https://github.com/frappe/erpnext/issues/58067)) ([d0ef967](d0ef967890))
* **coa_importer:** allow importing COA through `import_coa` only for `Accounts Manager` (backport [#56132](https://github.com/frappe/erpnext/issues/56132)) ([#58064](https://github.com/frappe/erpnext/issues/58064)) ([00d8dc7](00d8dc73b9))
* convert hours to minutes in workstation complete_job ([5e753ec](5e753ec6e4))
* escape `customer_details` on lead creation from appointment (backport [#57947](https://github.com/frappe/erpnext/issues/57947)) ([#57948](https://github.com/frappe/erpnext/issues/57948)) ([2ca71f1](2ca71f16c4))
* field validation and perm checks on `get_stock_reservation_entries_for_voucher` (backport [#57968](https://github.com/frappe/erpnext/issues/57968)) ([#57986](https://github.com/frappe/erpnext/issues/57986)) ([a0d1561](a0d156120b))
* get_valuation_method takes no company argument on version-15 ([c45ea35](c45ea3545d))
* guard reconciliation table deletes when tables are missing ([140e06d](140e06dfa4))
* incorrect entry detection in Stock Ledger Invariant Check ([#57886](https://github.com/frappe/erpnext/issues/57886)) ([529ff25](529ff251fb))
* keep asset repair downtime in sync with entered dates ([0e9c08d](0e9c08d494))
* **manufacturing:** avoid child BOM cache lookups ([5c2123f](5c2123f6ed))
* **manufacturing:** correct nested BOM quantities ([a310346](a3103469e2))
* **manufacturing:** keep item code searchable when a barcode matches the same text ([0310db2](0310db22ec))
* optimize product bundle item search ([52a1b12](52a1b124b2))
* preserve custom title on new JV (backport [#57987](https://github.com/frappe/erpnext/issues/57987)) ([#57988](https://github.com/frappe/erpnext/issues/57988)) ([713af31](713af31eda))
* purchase return of batchwise valuation batch valued at original receipt rate instead of batch avg rate (version-15-hotfix) ([#57837](https://github.com/frappe/erpnext/issues/57837)) ([e2ded11](e2ded11e09))
* re-check future sle before queuing repost on submit ([#57664](https://github.com/frappe/erpnext/issues/57664)) ([#57960](https://github.com/frappe/erpnext/issues/57960)) ([d510978](d510978a44))
* reflect in-invoice receivable settlements in Sales Register ledger view ([91afc04](91afc04d9f))
* repost read stale sibling SLE rate for moving average returns ([5738cfc](5738cfce79))
* require material transfer before job card time logs ([2e5ae18](2e5ae188d6))
* resolve version-15 backport conflicts ([b25a2f2](b25a2f2cdf))
* **selling:** bill re-delivered sales order quantities ([47c6274](47c6274b13))
* **selling:** reset stale item details on item change (backport [#58051](https://github.com/frappe/erpnext/issues/58051)) ([#58052](https://github.com/frappe/erpnext/issues/58052)) ([01372cf](01372cf295))
* set `restrict_globals=True` in `frappe.render_template` (backport [#57899](https://github.com/frappe/erpnext/issues/57899)) ([#57901](https://github.com/frappe/erpnext/issues/57901)) ([af7953a](af7953a933))
* **setup:** fetch driver address by supplier link ([2a4a9ff](2a4a9fff3d))
* skip incoming rate calc when serial no qty is zero (backport [#57427](https://github.com/frappe/erpnext/issues/57427)) ([#57956](https://github.com/frappe/erpnext/issues/57956)) ([f9a09f0](f9a09f0ac4))
* **stock:** grant account access in blanket order role test ([5e78603](5e7860362e))
* **stock:** handle multi-item opening balance in Stock Ledger report (backport [#57591](https://github.com/frappe/erpnext/issues/57591)) ([#57795](https://github.com/frappe/erpnext/issues/57795)) ([ebdc0cd](ebdc0cdf7f))
* **stock:** scope over deliver/receive role check to delivery and receipt overflow ([328c72d](328c72da18))
* **stock:** validate new warehouse inventory account after naming ([5e3d094](5e3d0947c8))
* **stock:** validate over delivery/receipt allowance in stock settings ([7be8a99](7be8a99a23))
* **stock:** validate warehouse accounts when used ([d620720](d620720445))
* **subscription:** don't reactivate a cancelled subscription (backport [#57774](https://github.com/frappe/erpnext/issues/57774)) ([#57780](https://github.com/frappe/erpnext/issues/57780)) ([edc8daa](edc8daae21)), closes [#57761](https://github.com/frappe/erpnext/issues/57761) [#57761](https://github.com/frappe/erpnext/issues/57761)
* sync open reference forms after Quality Inspection updates them ([c1d198d](c1d198d205))
* tolerate floating-point drift in sales team allocated percentage ([262fbc5](262fbc52d8))
* use stock settings for warehouse defaults ([d4ed207](d4ed2074d5))
* validate webform for project ([680ce70](680ce70c35))
* zero-rate repost fallback could still read sibling SLE ([8131af9](8131af9405))
2026-08-11 23:19:46 +00:00
Diptanil Saha
942a1ddb2c Merge pull request #58025 from frappe/version-15-hotfix
chore: release v15
2026-08-12 04:47:53 +05:30
Frappe PR Bot
866688931b chore(release): Bumped to Version 15.119.0
# [15.119.0](https://github.com/frappe/erpnext/compare/v15.118.3...v15.119.0) (2026-08-04)

### Bug Fixes

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

### Features

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

### Bug Fixes

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

### Bug Fixes

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

### Bug Fixes

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

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

### Bug Fixes

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

### Features

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

### Bug Fixes

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

### Features

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

### Bug Fixes

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

### Features

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

### Performance Improvements

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

### Bug Fixes

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

### Features

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

### Features

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

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

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

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

### Bug Fixes

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

### Features

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

View File

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

View File

@@ -49,10 +49,6 @@ class ERPNextAddress(Address):
@frappe.whitelist()
def get_shipping_address(company, address=None):
# `select`, not `read`: the roles that fill in these transactions hold no Company `read` row.
# doc= so the named company is evaluated and User Permissions apply.
frappe.has_permission("Company", ptype="select", doc=company, throw=True)
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],

View File

@@ -25,7 +25,6 @@ def get(
):
if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name)
chart.check_permission("read")
else:
chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan
@@ -54,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

@@ -191,7 +191,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2024-08-19 15:19:11.095045",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -244,46 +244,6 @@
"role": "Accounts 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": "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
}
],
"search_fields": "account_number",

View File

@@ -502,19 +502,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

@@ -120,8 +120,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

@@ -207,11 +207,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.flags.in_test:
toggle_disabling(doc=doc)
else:

View File

@@ -101,7 +101,7 @@
}
],
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2020-07-17 14:00:13.105433",
"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

@@ -254,7 +254,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2026-01-20 00:46:16.633364",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -284,22 +284,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");
},
@@ -102,24 +101,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

@@ -3,14 +3,13 @@
import json
from datetime import date
import frappe
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
@@ -48,12 +47,8 @@ class BankReconciliationTool(Document):
@frappe.whitelist()
def get_bank_transactions(
bank_account: str, from_date: str | date | None = None, to_date: str | date | None = None
):
def get_bank_transactions(bank_account, from_date=None, to_date=None):
# 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])
@@ -377,14 +372,13 @@ def create_payment_entry_bts(
@frappe.whitelist()
def auto_reconcile_vouchers(
bank_account: str,
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,
from_reference_date: str | date | None = None,
to_reference_date: str | date | None = None,
bank_account,
from_date=None,
to_date=None,
filter_by_reference_date=None,
from_reference_date=None,
to_reference_date=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:
@@ -494,16 +488,15 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
@frappe.whitelist()
def get_linked_payments(
bank_transaction_name: str,
document_types: str | list[str] | None = None,
from_date: str | date | None = None,
to_date: str | date | None = None,
filter_by_reference_date: bool | None = None,
from_reference_date: str | date | None = None,
to_reference_date: str | date | None = None,
bank_transaction_name,
document_types=None,
from_date=None,
to_date=None,
filter_by_reference_date=None,
from_reference_date=None,
to_reference_date=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
@@ -523,23 +516,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

@@ -10,7 +10,6 @@ from frappe.utils import add_days, today
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
auto_reconcile_vouchers,
get_bank_transactions,
get_linked_payments,
)
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
@@ -99,42 +98,3 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
# assert API output post reconciliation
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
self.assertEqual(len(transactions), 0)
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 = (
frappe.get_doc(
{
"doctype": "Bank Transaction",
"date": today(),
"deposit": 100,
"bank_account": self.bank_account,
"currency": "INR",
}
)
.insert()
.submit()
)
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,
)

View File

@@ -7,7 +7,6 @@ frappe.ui.form.on("Bank Statement Import", {
return {
filters: {
company: doc.company,
is_company_account: 1,
},
};
});

View File

@@ -296,10 +296,6 @@ def get_import_logs(docname: str):
@frappe.whitelist()
def upload_bank_statement(**args):
# `create`: the sole caller is the Bank Reconciliation Tool's upload button, which routes
# straight into a new Bank Statement Import form.
frappe.has_permission("Bank Statement Import", "create", throw=True)
args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import")

View File

@@ -11,9 +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
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()
@@ -39,11 +36,6 @@ def upload_bank_statement():
@frappe.whitelist()
def create_bank_entries(columns, data, bank_account):
# insert()/submit() enforce this per document, but only after the row loop has written an
# Error Log for every rejected row -- so 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,11 +110,7 @@ def import_coa(file_name, company):
def get_file(file_name):
# look the file up through find_file_by_url, which returns it only when the caller may download it
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,7 +176,6 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
@frappe.whitelist()
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":

View File

@@ -125,7 +125,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2024-04-24 10:55:54.083042",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -172,54 +172,6 @@
"role": "Employee",
"select": 1,
"share": 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
}
],
"search_fields": "parent_cost_center, is_group",

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

@@ -115,7 +115,7 @@
"read_only": 1
}
],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2019-10-19 14:48:14.602481",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Coupon Code",
@@ -168,14 +168,6 @@
"role": "Website Manager",
"share": 1,
"write": 1
},
{
"role": "Maintenance Manager",
"select": 1
},
{
"role": "Maintenance User",
"select": 1
}
],
"sort_field": "modified",

View File

@@ -41,23 +41,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.get("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

@@ -115,46 +115,8 @@ class TestCouponCode(unittest.TestCase):
test_create_test_data()
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
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

@@ -56,7 +56,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2026-09-16 12:00:00.000000",
"modified": "2018-05-09 14:55:01.394387",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Finance Book",
@@ -119,18 +119,6 @@
"share": 1,
"submit": 0,
"write": 0
},
{
"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 12:00:00.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 12:00:00.000000",
"modified": "2023-07-09 18:11:23.105589",
"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

@@ -1479,10 +1479,6 @@ class JournalEntryTaxWithholding:
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
# `select`, not `read`: also runs server-side from get_payment_entry, and Auditor/HR User/
# Desk User hold only the select row on Company. doc= for User Permissions.
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")
@@ -1511,10 +1507,6 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
account = account_list[0].name
if account:
# `account` is caller-supplied; get_balance_on() checks it only on the branch that reads a
# balance, and `fetch_balance` is caller-supplied too.
frappe.has_permission("Account", doc=account, throw=True)
account_details = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"], as_dict=1
)
@@ -1700,39 +1692,29 @@ 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")
party = filters.get("party")
# each names one value. A list would be read as a filter operator below and widen the search
# past what the caller named.
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", "user_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 frappe.db.sql(
f"""
SELECT jv.name, jv.posting_date, jv.user_remark
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
WHERE jv_detail.parent = jv.name
AND jv_detail.account = %(account)s
AND IFNULL(jv_detail.party, '') = %(party)s
AND (
jv_detail.reference_type IS NULL
OR jv_detail.reference_type = ''
)
AND jv.docstatus = 1
AND jv.`{searchfield}` LIKE %(txt)s
ORDER BY jv.name DESC
LIMIT %(limit)s offset %(offset)s
""",
dict(
account=filters.get("account"),
party=cstr(filters.get("party")),
txt=f"%{txt}%",
offset=start,
limit=page_len,
),
)
@@ -1937,10 +1919,6 @@ def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Docume
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

@@ -152,7 +152,7 @@
"options": "Project"
}
],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2019-05-26 09:11:46.120251",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Loyalty Program",
@@ -169,14 +169,6 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"role": "Sales Master Manager",
"select": 1
},
{
"role": "Sales User",
"select": 1
}
],
"quick_entry": 1,

View File

@@ -86,8 +86,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)
lp_details.update(
@@ -118,10 +116,6 @@ def get_loyalty_program_details(
silent=False,
include_expired_entry=False,
):
# authorise the customer, not the programme: a Loyalty Program check is read-only to System
# Manager and would deny every role that fills in the two calling forms.
frappe.has_permission("Customer", doc=customer, throw=True)
lp_details = frappe._dict()
if not loyalty_program:
@@ -143,13 +137,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 12:00:00.000000",
"modified": "2020-09-18 17:57:23.835236",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Mode of Payment",
@@ -68,30 +68,6 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"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 12:00:00.000000",
"modified": "2024-01-30 13:57:55.802744",
"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": "modified",

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

@@ -455,13 +455,6 @@ class PaymentReconciliation(Document):
default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
account_currency = frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
)
allocated_amount_precision = get_field_precision(
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount"),
currency=account_currency,
)
entries = []
for pay in args.get("payments"):
@@ -469,17 +462,11 @@ class PaymentReconciliation(Document):
for inv in args.get("invoices"):
if pay.get("amount") >= inv.get("outstanding_amount"):
res = self.get_allocated_entry(pay, inv, inv["outstanding_amount"])
pay["amount"] = flt(
flt(pay.get("amount")) - flt(inv.get("outstanding_amount")),
allocated_amount_precision,
)
pay["amount"] = flt(pay.get("amount")) - flt(inv.get("outstanding_amount"))
inv["outstanding_amount"] = 0
else:
res = self.get_allocated_entry(pay, inv, pay["amount"])
inv["outstanding_amount"] = flt(
flt(inv.get("outstanding_amount")) - flt(pay.get("amount")),
allocated_amount_precision,
)
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))

View File

@@ -1484,75 +1484,6 @@ class TestPaymentReconciliation(FrappeTestCase):
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
pr.reconcile()
@change_settings("System Settings", {"currency_precision": 2})
def test_allocate_entries_rounds_running_balance_to_currency_precision(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Customer"
pr.party = self.customer
pr.receivable_payable_account = self.debit_to
pr.set("invoices", [{"invoice_number": "INV-1"}])
pr.set("payments", [{"reference_name": "PAY-1"}])
invoices = [
{
"invoice_type": "Sales Invoice",
"invoice_number": "INV-1",
"outstanding_amount": 17592.415,
"currency": "INR",
},
]
payments = [
{
"reference_type": "Payment Entry",
"reference_name": "PAY-1",
"amount": 18230,
"currency": "INR",
}
]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(payments[0]["amount"], flt(637.585, 2))
@change_settings("System Settings", {"currency_precision": "", "use_number_format_from_currency": 1})
def test_allocate_entries_rounds_running_balance_to_account_currency_precision(self):
account_currency = frappe.get_cached_value("Account", self.debit_to, "account_currency")
original_number_format = frappe.db.get_value("Currency", account_currency, "number_format")
frappe.db.set_value("Currency", account_currency, "number_format", "#,###.###")
self.addCleanup(
frappe.db.set_value, "Currency", account_currency, "number_format", original_number_format
)
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Customer"
pr.party = self.customer
pr.receivable_payable_account = self.debit_to
pr.set("invoices", [{"invoice_number": "INV-1"}])
pr.set("payments", [{"reference_name": "PAY-1"}])
invoices = [
{
"invoice_type": "Sales Invoice",
"invoice_number": "INV-1",
"outstanding_amount": 17592.415,
"currency": account_currency,
},
]
payments = [
{
"reference_type": "Payment Entry",
"reference_name": "PAY-1",
"amount": 18230,
"currency": account_currency,
}
]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(payments[0]["amount"], flt(637.585, 3))
def test_reverse_payment_against_payment_for_supplier(self):
"""
Reconcile a payment against a reverse payment, for a supplier.

View File

@@ -116,7 +116,7 @@
}
],
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2021-02-15 20:30:56.256403",
"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

@@ -147,22 +147,12 @@ 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]
@frappe.whitelist()
def get_pos_invoices(start, end, pos_profile, user):
frappe.has_permission("POS Profile", doc=pos_profile, throw=True)
frappe.has_permission("POS Invoice", throw=True)
data = frappe.db.sql(
"""
select

View File

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

View File

@@ -801,21 +801,6 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
# POS Profile is the only boundary that fits: Item/Bin `read` exclude Accounts Manager, Item
# `select` is granted to every desk user, and POS Invoice `read` is granted to `All`.
# select-or-read: the shipped rows give Sales Manager only `select`.
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

@@ -217,8 +217,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:
map_doc(doc, invoice, table_map={"doctype": invoice.doctype})
@@ -241,13 +239,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(
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)
@@ -445,29 +439,6 @@ def update_item_wise_tax_detail(consolidate_tax_row, tax_row):
consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(",", ":"))
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))
& (sales_invoice.docstatus == 1)
)
).run(as_dict=True)
return {row.pos_invoice_item: row for row in rows}
def get_all_unconsolidated_invoices():
filters = {
"consolidated_invoice": ["in", ["", None]],
@@ -712,3 +683,27 @@ def get_error_message(message) -> str:
return message["message"]
except Exception:
return str(message)
def get_sales_invoice_item(return_against_pos_invoice, pos_invoice_item):
try:
SalesInvoice = DocType("Sales Invoice")
SalesInvoiceItem = DocType("Sales Invoice Item")
query = (
frappe.qb.from_(SalesInvoice)
.from_(SalesInvoiceItem)
.select(SalesInvoiceItem.name)
.where(
(SalesInvoice.name == SalesInvoiceItem.parent)
& (SalesInvoice.is_return == 0)
& (SalesInvoiceItem.pos_invoice == return_against_pos_invoice)
& (SalesInvoiceItem.pos_invoice_item == pos_invoice_item)
& (SalesInvoice.docstatus == 1)
)
)
result = query.run(as_dict=True)
return result[0].name if result else None
except Exception:
return None

View File

@@ -3,11 +3,9 @@
import json
import unittest
from contextlib import contextmanager
import frappe
from frappe.tests.utils import change_settings
from frappe.utils import flt
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
@@ -21,67 +19,6 @@ from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@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(unittest.TestCase):
def test_consolidated_invoice_creation(self):
frappe.db.sql("delete from `tabPOS Invoice`")
@@ -573,92 +510,3 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
@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 consolidation:
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.
"""
frappe.db.sql("delete from `tabPOS Invoice`")
try:
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)
init_user_and_profile()
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},
)
consolidate_pos_invoices()
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),
)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -422,7 +422,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2026-02-22 04:17:03.308876",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@@ -446,10 +446,6 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"role": "Sales Manager",
"select": 1
}
],
"row_format": "Dynamic",

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)
# an allow-list, not a type check: `doctype` is caller-chosen, and any doctype the caller can
# read would otherwise satisfy the has_permission below while the pricing engine still ran
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
@@ -424,7 +389,6 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
set_transaction_type(args_copy)
data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)
@@ -769,18 +733,14 @@ def make_pricing_rule(doctype, docname):
@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=["distinct uom"],
as_list=1,
)

View File

@@ -130,17 +130,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, not inside the branch: otherwise an unentitled caller
# learns the document's status from whether this returns or 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)
@@ -157,7 +154,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)
@@ -173,7 +170,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)
@@ -258,11 +255,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

@@ -293,13 +293,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

@@ -1664,7 +1664,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2026-08-12 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1717,18 +1717,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 12:00:00.000000",
"modified": "2024-01-30 13:08:09.537242",
"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

@@ -524,10 +524,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):
# `filters` goes straight to db.get_all, so reaching this needs gating rather than typing.
# Reached only from this doctype's own form, which is System-Manager-only.
frappe.has_permission("Repost Accounting Ledger", throw=True)
filters = {"allowed": True}
if txt:

View File

@@ -494,7 +494,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.process_asset_depreciation()
@@ -2591,28 +2590,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 this resolves a reference field for. 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 source document decides access, not the targets: those belong to the counterpart company
# and the caller legitimately may not read them. doc= for User Permissions.
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

@@ -47,7 +47,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
class TestSalesInvoice(FrappeTestCase):
@@ -2943,7 +2943,7 @@ class TestSalesInvoice(FrappeTestCase):
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,
@@ -5042,34 +5042,6 @@ class TestSalesInvoice(FrappeTestCase):
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
@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 12:00:00.000000",
"modified": "2024-01-30 13:07:28.801104",
"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

@@ -147,7 +147,7 @@
"icon": "fa fa-truck",
"idx": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2026-07-22 14:53:27.315435",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Shipping Rule",
@@ -195,38 +195,6 @@
"set_user_permissions": 1,
"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

@@ -15,39 +15,6 @@ test_records = frappe.get_test_records("Shipping Rule")
class TestShippingRule(unittest.TestCase):
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(test_records[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(test_records[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(test_records[0])
shipping_rule.name = test_records[0].get("name")

View File

@@ -29,7 +29,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2022-01-18 21:13:41.161017",
"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 of these names a single Address. A dict is read as a filter instead, and `get_doc`
# would resolve it to whichever Address happens to match, so only a plain name is accepted
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

@@ -102,7 +102,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2025-07-30 07:13:51.785735",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
@@ -144,26 +144,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
}
],
"sort_field": "modified",

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,
@@ -157,7 +156,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,
@@ -379,33 +378,13 @@ def get_default_price_list(party):
return frappe.get_cached_value("Customer Group", party.customer_group, "default_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")
@@ -417,10 +396,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:
party_details.price_list_currency = frappe.db.get_value(
"Price List", price_list, "currency", cache=True

View File

@@ -151,7 +151,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

@@ -178,7 +178,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

@@ -406,15 +406,7 @@ def get_invoices(filters, additional_query_columns):
if filters.get("supplier"):
query = query.where(pi.supplier == filters.supplier)
if filters.get("supplier_group"):
# read the group from the supplier master, to match the Supplier Group column
supplier = frappe.qb.DocType("Supplier")
query = query.where(
pi.supplier.isin(
frappe.qb.from_(supplier)
.select(supplier.name)
.where(supplier.supplier_group == filters.supplier_group)
)
)
query = query.where(pi.supplier_group == filters.supplier_group)
query = get_conditions(filters, query, "Purchase Invoice")

View File

@@ -97,23 +97,6 @@ class TestPurchaseRegister(FrappeTestCase):
self.assertEqual(first_row.credit, 600)
self.assertEqual(first_row.balance, 500)
def test_supplier_group_filter_uses_supplier_master(self):
# invoices created before the supplier_group field existed have it blank
pi = make_purchase_invoice()
pi.db_set("supplier_group", None, update_modified=False)
supplier_group = frappe.db.get_value("Supplier", pi.supplier, "supplier_group")
filters = frappe._dict(
company="_Test Company 6",
from_date=add_months(today(), -1),
to_date=today(),
supplier_group=supplier_group,
)
rows = [frappe._dict(row) for row in execute(filters)[1] if row.get("voucher_no") == pi.name]
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0].supplier_group, supplier_group)
def make_purchase_invoice():
from erpnext.accounts.doctype.account.test_account import create_account

View File

@@ -1,8 +1,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
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.accounts.party import get_default_price_list
class PartyTestCase(FrappeTestCase):
@@ -13,170 +12,3 @@ class PartyTestCase(FrappeTestCase):
customer.save()
price_list = get_default_price_list(customer)
assert price_list is None
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):
price_list = frappe.get_doc(
{
"doctype": "Price List",
"price_list_name": frappe.generate_hash(length=10),
"currency": "INR",
"selling": selling,
"buying": buying,
"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(
{
"doctype": "Customer",
"customer_name": frappe.generate_hash(length=10),
"customer_group": "Individual",
**values,
}
).insert(ignore_permissions=True, ignore_mandatory=True)
customer.customer_group = None
customer.save()
return customer

View File

@@ -597,7 +597,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2026-09-23 12:00:00.000000",
"modified": "2026-03-13 12:15:25.734623",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -633,38 +633,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

@@ -1211,22 +1211,6 @@ def is_cwip_accounting_enabled(asset_category):
@frappe.whitelist()
def get_asset_value_after_depreciation(asset_name, finance_book=None):
# 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)
@@ -1236,8 +1220,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}
)
@@ -1246,15 +1228,6 @@ def has_active_capitalization(asset):
@frappe.whitelist()
def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
# `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)
purchase_doc = frappe.get_doc(doctype, purchase_doc_name)
matching_items = [item for item in purchase_doc.items if item.item_code == item_code]

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_gl_entries_on_asset_disposal,
@@ -31,7 +31,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",
@@ -205,7 +205,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
@@ -358,8 +358,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)
@@ -370,13 +368,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
@@ -700,15 +696,8 @@ class AssetCapitalization(StockController):
)
def check_capitalization_access():
"""Every lookup in this file feeds the Asset Capitalization form, so that form is the boundary."""
frappe.has_permission("Asset Capitalization", throw=True)
@frappe.whitelist()
def get_target_item_details(item_code=None, company=None):
check_capitalization_access()
out = frappe._dict()
# Get Item Details
@@ -746,8 +735,6 @@ def get_target_item_details(item_code=None, company=None):
@frappe.whitelist()
def get_target_asset_details(asset=None, company=None):
check_capitalization_access()
out = frappe._dict()
# Get Asset Details
@@ -842,25 +829,17 @@ 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
@frappe.whitelist()
def get_consumed_asset_details(args):
check_capitalization_access()
if isinstance(args, str):
args = json.loads(args)
args = frappe._dict(args)
# 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 args.get("asset"):
ptype = "select" if frappe.only_has_select_perm("Asset") else "read"
frappe.has_permission("Asset", ptype, doc=args.get("asset"), throw=True)
out = frappe._dict()
asset_details = frappe._dict()
@@ -877,7 +856,7 @@ def get_consumed_asset_details(args):
if args.asset:
out.current_asset_value = flt(
_get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book)
get_asset_value_after_depreciation(args.asset, finance_book=args.finance_book)
)
out.asset_value = get_value_after_depreciation_on_disposal_date(
args.asset, args.posting_date, finance_book=args.finance_book
@@ -906,8 +885,6 @@ def get_consumed_asset_details(args):
@frappe.whitelist()
def get_service_item_details(args):
check_capitalization_access()
if isinstance(args, str):
args = json.loads(args)
@@ -936,8 +913,6 @@ def get_service_item_details(args):
@frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(params):
check_capitalization_access()
if isinstance(params, str):
params = json.loads(params)

View File

@@ -73,7 +73,7 @@
}
],
"links": [],
"modified": "2026-09-16 12:00:00.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

@@ -61,7 +61,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2021-01-22 15:09:03.347345",
"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

@@ -36,7 +36,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2023-11-29 04:04:24.272872",
"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": "modified",

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 (
@@ -81,7 +81,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 make_depreciation_entry(self):
asset = frappe.get_doc("Asset", self.asset)

View File

@@ -142,7 +142,7 @@
],
"is_tree": 1,
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2023-08-29 12:49:33.290527",
"modified_by": "Administrator",
"module": "Assets",
"name": "Location",
@@ -219,10 +219,6 @@
"role": "Agriculture User",
"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

@@ -699,19 +699,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 and never consults User Permissions, so on its own it lets
# a caller restricted to one company close another company's orders
po = frappe.get_doc("Purchase Order", name)
po.check_permission("submit")
if po.docstatus == 1:
if status == "Closed":
if po.status not in ("Cancelled", "Closed") and (

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
@@ -682,34 +683,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

@@ -491,7 +491,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2026-05-29 16:52:59.441272",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
@@ -547,30 +547,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

@@ -233,15 +233,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 was interpolated into qb.DocType(), so any doctype could be
# joined to Dynamic Link and read. The two pickers send only these 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

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

View File

@@ -55,7 +55,8 @@ class SupplierScorecard(Document):
self.update_standing()
def on_update(self):
if make_all_scorecards(self.name) > 0:
score = make_all_scorecards(self.name)
if score > 0:
self.save()
def validate_standings(self):
@@ -197,7 +198,7 @@ def refresh_scorecards():
@frappe.whitelist()
def make_all_scorecards(docname: str):
def make_all_scorecards(docname):
sc = frappe.get_doc("Supplier Scorecard", docname)
supplier = frappe.get_doc("Supplier", sc.supplier)
supplier.check_permission("write")
@@ -212,18 +213,25 @@ def make_all_scorecards(docname: str):
while (start_date < todays) and (end_date <= todays):
# check to make sure there is no scorecard period already created
# (inclusive bounds: a single-day period — supplier created on a month's
# last day — must match its own window, else it is re-created every run)
scorecards = frappe.get_all(
"Supplier Scorecard Period",
fields=["name"],
filters={
"scorecard": docname,
"docstatus": 1,
"start_date": ["<=", end_date],
"end_date": [">=", start_date],
},
order_by="end_date desc",
scorecards = frappe.db.sql(
"""
SELECT
scp.name
FROM
`tabSupplier Scorecard Period` scp
WHERE
scp.scorecard = %(sc)s
AND scp.docstatus = 1
AND (
(scp.start_date > %(end_date)s
AND scp.end_date < %(start_date)s)
OR
(scp.start_date < %(end_date)s
AND scp.end_date > %(start_date)s))
ORDER BY
scp.end_date DESC""",
{"sc": docname, "start_date": start_date, "end_date": end_date},
as_dict=1,
)
if len(scorecards) == 0:
period_card = make_supplier_scorecard(docname, None)

View File

@@ -4,9 +4,6 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, get_last_day, getdate
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
class TestSupplierScorecard(FrappeTestCase):
@@ -21,32 +18,6 @@ class TestSupplierScorecard(FrappeTestCase):
d.weight = 0
self.assertRaises(frappe.ValidationError, my_doc.insert)
def test_no_recursion_for_supplier_created_on_month_end(self):
make_supplier_scorecard() # ensures the "Delivery" criteria master exists
supplier = create_supplier(supplier_name="_Test Month End Scorecard Supplier")
month_end = get_last_day(add_months(getdate(), -1))
frappe.db.set_value("Supplier", supplier.name, "creation", month_end, update_modified=False)
scorecard = frappe.get_doc(valid_scorecard[0])
scorecard.supplier = supplier.name
scorecard.name = supplier.name
scorecard.insert()
periods = frappe.get_all(
"Supplier Scorecard Period",
filters={"scorecard": scorecard.name},
fields=["start_date", "end_date"],
)
self.assertEqual(len(periods), 1)
self.assertEqual(periods[0].start_date, month_end)
self.assertEqual(periods[0].end_date, month_end)
# saving again must not re-create the single-day period or recurse
frappe.get_doc("Supplier Scorecard", scorecard.name).save()
periods = frappe.get_all("Supplier Scorecard Period", filters={"scorecard": scorecard.name})
self.assertEqual(len(periods), 1)
def make_supplier_scorecard():
my_doc = frappe.get_doc(valid_scorecard[0])

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:
@@ -120,35 +118,7 @@ def check_on_hold_or_closed_status(doctype, docname) -> None:
@frappe.whitelist()
def get_linked_material_requests(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"))
# get_list, not the raw query below on its own: it applies the caller's Material Request
# permission and their User Permissions, so the loop can only ever return permitted documents
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(
@@ -159,13 +129,12 @@ def get_linked_material_requests(items):
FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
WHERE mr.name = mr_item.parent
AND mr_item.item_code = %(item)s
AND mr.name in %(permitted)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:

View File

@@ -387,26 +387,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")
@@ -417,20 +403,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):
@@ -3224,22 +3199,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):
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)
if tax_template and company:
tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company")
if tax_template_company == company:
@@ -3257,9 +3221,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)
from frappe.model import child_table_fields, default_fields
tax_master = frappe.get_doc(master_doctype, master_name)

View File

@@ -17,7 +17,7 @@ from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import _get_incoming_rate
from erpnext.stock.utils import get_incoming_rate
class QtyMismatchError(ValidationError):
@@ -136,7 +136,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,
@@ -366,7 +366,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
@@ -459,7 +459,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"),
@@ -572,26 +572,6 @@ class BuyingController(SubcontractingController):
if status in ("Closed", "On Hold"):
frappe.throw(_("{0} {1} is {2}").format(ref_doctype, d.get(ref_fieldname), status))
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()
@@ -604,9 +584,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)
@@ -622,7 +601,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,
@@ -695,7 +674,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 template Item is the boundary and `read` is loser-free: the roles that cannot read Item
# cannot open the calling form either
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 its sibling enqueue_multiple_variant_creation requires, 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,10 +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):
# Delegates to get_variant and create_variant below, which carry their own checks; this one
# fails fast rather than relying on that delegation.
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,7 +6,7 @@ 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 Criterion, CustomFunction
@@ -310,61 +310,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)
qb_filter_and_conditions = []
qb_filter_or_conditions = []
ifelse = CustomFunction("IF", ["condition", "then", "else"])
list_filters = [["status", "not in", ["Completed", "Cancelled", "On hold"]]]
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, which would fail 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 is expressible here and
# 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 (already LIKE-narrowed) 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 = qb.from_(proj).select(*[proj[fieldname] for fieldname in fields]).where(proj.name.isin(permitted))
# 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))
# 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:
@@ -627,31 +609,27 @@ 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,
blanket_orders = (
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 [""]])
return frappe.get_list(
"Blanket Order",
filters=bo_filters,
fields=["name", "blanket_order_type", "to_date"],
group_by="name",
as_list=True,
.run()
)
return blanket_orders
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -832,22 +810,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()
@@ -872,69 +843,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: it dedupes the child join just the same, and 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: it dedupes the child join just the same, and 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()
@@ -1015,28 +952,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 (["like", "%"], ["!=", ""]) 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 reference type: the lookup cannot tell same-named parents of two types 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,
@@ -1047,30 +965,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", "%"] here would span parents, and only one
# of them would be 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 before it is worth checking:
# any other doctype 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)
@@ -1095,11 +989,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.db.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}%"]
@@ -1114,7 +1004,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

@@ -11,7 +11,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.serial_batch_bundle import get_serial_nos as get_serial_nos_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):
@@ -736,7 +736,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,
@@ -1224,23 +1224,8 @@ def get_available_serial_nos(serial_nos, warehouse):
)
# the only doctypes this endpoint is called for; it reaches 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 either a Sales Invoice or a POS Invoice — both share the Sales Invoice
# Payment child table — so resolve which one it is before authorising rather than guessing.
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

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):
@@ -565,7 +565,7 @@ class SellingController(StockController):
or self.is_internal_transfer()
or (get_valuation_method(d.item_code) == "Moving Average" 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,
@@ -896,77 +896,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

@@ -20,7 +20,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):
@@ -82,7 +82,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
@@ -764,7 +764,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))
@@ -1148,7 +1148,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,95 +0,0 @@
import frappe
from frappe.tests.utils import FrappeTestCase
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
class TestPriceListValidation(FrappeTestCase):
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 (
make_purchase_receipt,
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)
make_purchase_receipt(warehouse="Stores - TCP1", company=company)
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

@@ -517,7 +517,7 @@
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2026-09-16 12:00:00.000000",
"modified": "2025-06-26 11:02:01.158901",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -574,10 +574,6 @@
"read": 1,
"report": 1,
"role": "Sales User"
},
{
"role": "Support Team",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -474,14 +474,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` is caller-supplied. 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)
# the insert path checks `create` itself, but the path that reuses an existing Lead required
# nothing, so it returned a Lead name to callers with no access to Leads 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 12:00:00.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

@@ -553,14 +553,8 @@ def make_opportunity_from_communication(
):
from erpnext.crm.doctype.lead.lead import make_lead_from_communication
# `communication` is caller-supplied. 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() carries its own check, but it is skipped entirely when the
# email already references a Lead, so this cannot rely on it.
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)

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import re
from typing import TYPE_CHECKING
import frappe
@@ -79,48 +78,8 @@ class CodeList(Document):
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
def _version_key(version: str | None) -> list:
"""Natural sort key for the version formats publishers use: integers and ISO dates.
Orders 3 < 10 (which a lexical sort gets wrong) and 2020-01-01 < 2020-11-05.
"""
return [int(p) if p.isdigit() else p for p in re.split(r"(\d+)", version or "")]
@frappe.request_cache
def resolve_code_list(code_list: str) -> str | None:
"""Return the Code List for a document name or a canonical URI.
Code Lists are named after their CanonicalVersionUri, so one canonical URI can
map to several documents, one per version. An exact document name takes
precedence, which lets a caller request a specific version; a canonical URI
resolves to the latest version available.
"""
if frappe.db.exists("Code List", code_list):
return code_list
candidates = frappe.get_all(
"Code List",
filters={"canonical_uri": code_list},
fields=["name", "version"],
)
if not candidates:
return None
# ponytail: assumes one publisher sticks to one version format. An integer and an
# ISO date under the same canonical URI compare numerically (3 < 2020), so the date
# would win; import the genericode ValidityDate and sort on that if it ever happens.
return max(candidates, key=lambda cl: _version_key(cl.version)).name
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
"""Return the common code for a given record.
`code_list` may be a Code List name or a canonical URI (latest version wins).
"""
if not (code_list := resolve_code_list(code_list)):
return ()
"""Return the common code for a given record"""
CommonCode = frappe.qb.DocType("Common Code")
DynamicLink = frappe.qb.DocType("Dynamic Link")
@@ -142,13 +101,7 @@ def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
"""Return the record name for a given common code.
`code_list` may be a Code List name or a canonical URI (latest version wins).
"""
if not (code_list := resolve_code_list(code_list)):
return ()
"""Return the record name for a given common code"""
CommonCode = frappe.qb.DocType("Common Code")
DynamicLink = frappe.qb.DocType("Dynamic Link")
@@ -170,12 +123,6 @@ def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
def get_default_code(code_list: str) -> str | None:
"""Return the default common code for a given code list.
`code_list` may be a Code List name or a canonical URI (latest version wins).
"""
if not (code_list := resolve_code_list(code_list)):
return None
"""Return the default common code for a given code list"""
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None

View File

@@ -20,11 +20,8 @@ class CodeListSelectionMismatchError(Exception):
pass
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def import_genericode():
# check before save(), which only runs after the XML is fetched and parsed; denies exactly who save() would, sooner
frappe.has_permission("Code List", "create", throw=True)
try:
content, file_name = get_uploaded_genericode_file()
@@ -164,7 +161,7 @@ def parse_genericode_content(content: bytes):
return etree.fromstring(content, parser=parser)
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def process_genericode_import(
code_list_name: str,
file_name: str,
@@ -175,11 +172,6 @@ def process_genericode_import(
):
from erpnext.edi.doctype.common_code.common_code import import_genericode
# Same reasoning as above: common_code.save() enforces this per document, but only after the
# file has been read and its XML parsed and queried.
frappe.has_permission("Common Code", "create", throw=True)
frappe.has_permission("Code List", doc=code_list_name, throw=True)
column_map = {"code": code_column, "title": title_column, "description": description_column}
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)

View File

@@ -1,83 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
# import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.edi.doctype.code_list.code_list import (
_version_key,
get_codes_for,
get_default_code,
get_docnames_for,
resolve_code_list,
)
CANONICAL_URI = "urn:test:erpnext:codeliste:resolve"
OLD_VERSION = f"{CANONICAL_URI}:3"
NEW_VERSION = f"{CANONICAL_URI}:10"
UNKNOWN_URI = "urn:test:erpnext:codeliste:missing"
class TestCodeList(FrappeTestCase):
def setUp(self):
"""Create two versions of one code list. FrappeTestCase rolls back once per class, so the inserts are guarded."""
for name, version in ((OLD_VERSION, "3"), (NEW_VERSION, "10")):
if not frappe.db.exists("Code List", name):
frappe.get_doc(
doctype="Code List",
name=name,
title=name,
canonical_uri=CANONICAL_URI,
version=version,
).insert()
default_code = frappe.get_doc(
doctype="Common Code",
title="Test Default",
common_code="XYZ",
code_list=NEW_VERSION,
).insert()
frappe.db.set_value("Code List", NEW_VERSION, "default_common_code", default_code.name)
# resolution is request-cached, so fixtures must not be masked by earlier lookups
frappe.local.request_cache.clear()
def test_version_key_orders_integers_and_iso_dates(self):
"""Integer and ISO date versions must both order correctly, unlike a lexical sort."""
self.assertEqual(sorted(["10", "3", None, "9"], key=_version_key), [None, "3", "9", "10"])
self.assertEqual(
sorted(["2020-11-05", "2019-12-31", "2020-01-01"], key=_version_key),
["2019-12-31", "2020-01-01", "2020-11-05"],
)
def test_canonical_uri_resolves_to_latest_version(self):
self.assertEqual(resolve_code_list(CANONICAL_URI), NEW_VERSION)
def test_name_resolves_to_itself(self):
"""Passing a version-specific name must return that version, not the latest one."""
self.assertEqual(resolve_code_list(OLD_VERSION), OLD_VERSION)
def test_name_takes_precedence_over_canonical_uri(self):
"""A document named like a canonical URI must not redirect to another version."""
frappe.get_doc(
doctype="Code List",
name=CANONICAL_URI,
title=CANONICAL_URI,
canonical_uri=CANONICAL_URI,
version="1",
).insert()
frappe.local.request_cache.clear()
self.assertEqual(resolve_code_list(CANONICAL_URI), CANONICAL_URI)
def test_unknown_uri_resolves_to_none(self):
self.assertIsNone(resolve_code_list(UNKNOWN_URI))
def test_lookups_are_empty_for_unknown_code_list(self):
"""An unresolved code list must not fall through to an unfiltered query."""
self.assertEqual(get_codes_for(UNKNOWN_URI, "UOM", "Nos"), ())
self.assertEqual(get_docnames_for(UNKNOWN_URI, "UOM", "XYZ"), ())
self.assertIsNone(get_default_code(UNKNOWN_URI))
def test_default_code_follows_latest_version(self):
self.assertEqual(get_default_code(CANONICAL_URI), "XYZ")
pass

View File

@@ -37,12 +37,8 @@ class PlaidSettings(Document):
return plaid.get_link_token()
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def get_plaid_configuration():
# Returns plaid_env and a freshly minted Plaid link_token. Plaid Settings is a System-Manager-only
# single doctype and every caller reaches this from its own form, so that is the boundary.
frappe.has_permission("Plaid Settings", throw=True)
if frappe.db.get_single_value("Plaid Settings", "enabled"):
plaid_settings = frappe.get_single("Plaid Settings")
return {
@@ -56,8 +52,6 @@ def get_plaid_configuration():
@frappe.whitelist()
def add_institution(token, response):
frappe.has_permission("Plaid Settings", throw=True)
response = json.loads(response)
plaid = PlaidConnector()
@@ -86,8 +80,6 @@ def add_institution(token, response):
@frappe.whitelist()
def add_bank_accounts(response, bank, company):
frappe.has_permission("Plaid Settings", throw=True)
try:
response = json.loads(response)
except TypeError:
@@ -327,10 +319,8 @@ def automatic_synchronization():
enqueue_synchronization()
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def enqueue_synchronization():
frappe.has_permission("Plaid Settings", throw=True)
plaid_accounts = frappe.get_all(
"Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]
)
@@ -343,12 +333,8 @@ def enqueue_synchronization():
)
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def get_link_token_for_update(access_token):
# `access_token` is caller-supplied and is used to mint a link token at Plaid, so this creates
# state at the provider even though it writes nothing here.
frappe.has_permission("Plaid Settings", throw=True)
plaid = PlaidConnector(access_token)
return plaid.get_link_token(update_mode=True)
@@ -367,10 +353,8 @@ def get_company(bank_account_name):
frappe.throw(_("Could not detect the Company for updating Bank Accounts"))
@frappe.whitelist(methods=["POST"])
@frappe.whitelist()
def update_bank_account_ids(response):
frappe.has_permission("Plaid Settings", throw=True)
data = json.loads(response)
institution_name = data["institution"]["name"]
bank = frappe.get_doc("Bank", institution_name).as_dict()

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