Compare commits

..

36 Commits

Author SHA1 Message Date
Frappe PR Bot
8aede87290 chore(release): Bumped to Version 15.104.2
## [15.104.2](https://github.com/frappe/erpnext/compare/v15.104.1...v15.104.2) (2026-04-09)

### Bug Fixes

* set default posting time in RIV ([041f99c](041f99c926))
2026-04-09 11:15:07 +00:00
rohitwaghchaure
1b2c7ca21f Merge pull request #54169 from frappe/mergify/bp/version-15/pr-54162
fix: set default posting time in RIV (backport #54161) (backport #54162)
2026-04-09 16:43:37 +05:30
rohitwaghchaure
db3a40409f chore: fix conflicts
Removed unused method reset_repost_only_accounting_ledgers and fixed the validate method to set default posting time.

(cherry picked from commit 2df574baae)
2026-04-09 09:58:50 +00:00
Rohit Waghchaure
041f99c926 fix: set default posting time in RIV
(cherry picked from commit a7ece65536)

# Conflicts:
#	erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
(cherry picked from commit 6e438e71eb)
2026-04-09 09:58:50 +00:00
Frappe PR Bot
b88f3f69b0 chore(release): Bumped to Version 15.104.1
## [15.104.1](https://github.com/frappe/erpnext/compare/v15.104.0...v15.104.1) (2026-04-09)

### Bug Fixes

* last SLE not updated in the file ([3a2dc6f](3a2dc6f9ee))
2026-04-09 05:20:24 +00:00
rohitwaghchaure
dba8abbabf Merge pull request #54154 from frappe/mergify/bp/version-15/pr-54150
fix: last SLE not updated in the file (backport #54132) (backport #54150)
2026-04-09 10:48:53 +05:30
rohitwaghchaure
c1591c37db chore: fix conflicts
(cherry picked from commit c70259687a)
2026-04-09 04:48:38 +00:00
Rohit Waghchaure
3a2dc6f9ee fix: last SLE not updated in the file
(cherry picked from commit 38ed425ee2)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/test_work_order.py
(cherry picked from commit 8408e81335)
2026-04-09 04:48:38 +00:00
Frappe PR Bot
a2626ed55f chore(release): Bumped to Version 15.104.0
# [15.104.0](https://github.com/frappe/erpnext/compare/v15.103.1...v15.104.0) (2026-04-07)

### Bug Fixes

* add support to fetch items based on manufacture stock entry; fix how it's done from work order ([e9ce0a4](e9ce0a41e6))
* add v15 compatibility for scrap item ([652bd39](652bd396d4))
* auto-set source_stock_entry ([b87b445](b87b445802))
* avg stock entries for disassembly from WO ([44d4079](44d40795df))
* correct warehouse preference for disassemble ([b8ddc2f](b8ddc2f2b9))
* create source_stock_entry to refer to original manufacturing entry ([55ee1dc](55ee1dcd04))
* custom button to disassemble manufactured stock entry with work order ([835ae27](835ae27b38))
* disassembly prompt with source stock entry field ([44f2e94](44f2e9480d))
* do not repost GL if no change in valuation ([0063201](0063201818))
* do not show inv dimension unnecessarily in stock entry (backport [#53946](https://github.com/frappe/erpnext/issues/53946)) ([#53950](https://github.com/frappe/erpnext/issues/53950)) ([e159c79](e159c79766))
* ensure compatibility with v15 ([8b42fcf](8b42fcf274))
* GL entries for different exchange rate in the purchase invoice ([def62cf](def62cf3fe))
* handle disassembly for secondary / scrap items ([229dc23](229dc23f97))
* include rejected qty in tax (purchase receipt) (backport [#53624](https://github.com/frappe/erpnext/issues/53624)) ([#53971](https://github.com/frappe/erpnext/issues/53971)) ([3fbfad1](3fbfad1b9b))
* manufacture entry with group_by support ([841b507](841b507502))
* **manufacturing:** handle null cur_dialog in BOM work order dialog (backport [#54011](https://github.com/frappe/erpnext/issues/54011)) ([#54014](https://github.com/frappe/erpnext/issues/54014)) ([cb0a548](cb0a548a95))
* not able to set operation in work order ([62d5870](62d58702a0))
* prevent selection of group type customer group in customer master ([7a227e0](7a227e048e))
* process loss with bom path disassembly ([eee6d7e](eee6d7e566))
* **promotional_scheme:** toggle enable state between Buying and Selli… (backport [#54110](https://github.com/frappe/erpnext/issues/54110)) ([#54111](https://github.com/frappe/erpnext/issues/54111)) ([5b7e6eb](5b7e6eb831))
* remove reference in serial/batch when document is cancelled (backport [#53979](https://github.com/frappe/erpnext/issues/53979)) ([#53988](https://github.com/frappe/erpnext/issues/53988)) ([e33abee](e33abeef7f))
* remove unnecessary param, and use value from self ([0b0dccd](0b0dccd294))
* resolve user permission error on status change by updating user … (backport [#54033](https://github.com/frappe/erpnext/issues/54033)) ([#54059](https://github.com/frappe/erpnext/issues/54059)) ([14085de](14085de332))
* set bom details on disassembly; abs batch qty ([84d5b52](84d5b52483))
* set serial and batch from source stock entry - on disassemble ([df049cd](df049cd277))
* set_query for source stock entry ([849b2e6](849b2e6ebf))
* show current stock qty in Stock Entry PDF (backport [#53761](https://github.com/frappe/erpnext/issues/53761)) ([#54031](https://github.com/frappe/erpnext/issues/54031)) ([af0116c](af0116cdc5))
* skip discount amount validation when not saving ([13eab9f](13eab9f993))
* **stock:** update stock queue in SABE for return entries ([05d6cf5](05d6cf5c9a))
* support creating disassembly (without link of WO) ([ef15c05](ef15c0581d))
* sync paid and received amount (backport [#53039](https://github.com/frappe/erpnext/issues/53039)) ([#54107](https://github.com/frappe/erpnext/issues/54107)) ([0505684](0505684d22))
* **test:** do not use is_group enabled customer group in test ([97684d3](97684d3dae))
* **test:** pin posting date in test_depreciation_on_cancel_invoice ([7f72189](7f72189665))
* **test:** use non-group customer group in test setup ([ea3fcc2](ea3fcc214b))
* transactions where update stock is 0 should not create SLEs (backport [#54035](https://github.com/frappe/erpnext/issues/54035)) ([#54076](https://github.com/frappe/erpnext/issues/54076)) ([bcf59e7](bcf59e7171))
* update min date based on transaction_date (backport [#53803](https://github.com/frappe/erpnext/issues/53803)) ([#54024](https://github.com/frappe/erpnext/issues/54024)) ([a71d32e](a71d32e668))
* use get_value ([8f01d12](8f01d12b5e))
* **ux:** refresh grid to correctly persist the state of fields ([3c327d5](3c327d5225))
* validate qty that can be disassembled from source stock entry. ([583c7b9](583c7b9819))
* validate work order consistency in stock entry ([d690a0c](d690a0c6bd))
* validation test for customer group ([7794f30](7794f3033e))
* **warehouse_capacity_dashboard:** removed `escape` from template (backport [#53907](https://github.com/frappe/erpnext/issues/53907)) ([#53908](https://github.com/frappe/erpnext/issues/53908)) ([efdb004](efdb004f0b))

### Features

* Allow Editing of Items and Quantities in Work Order ([1d36cb5](1d36cb55cd))
* croatian_address_template (backport [#53888](https://github.com/frappe/erpnext/issues/53888)) ([#54057](https://github.com/frappe/erpnext/issues/54057)) ([ee81268](ee812687e6))
* **timesheet:** allow partial billing and handled return ([21805bd](21805bde1f))

### Reverts

* botched backport ([#53967](https://github.com/frappe/erpnext/issues/53967)) ([22774fd](22774fdf87)), closes [#53776](https://github.com/frappe/erpnext/issues/53776) [#53766](https://github.com/frappe/erpnext/issues/53766) [#53767](https://github.com/frappe/erpnext/issues/53767)
2026-04-07 18:01:31 +00:00
diptanilsaha
0cc77274cb Merge pull request #54101 from frappe/version-15-hotfix 2026-04-07 22:19:03 +05:30
Frappe PR Bot
2597eaad51 chore(release): Bumped to Version 15.103.1
## [15.103.1](https://github.com/frappe/erpnext/compare/v15.103.0...v15.103.1) (2026-03-31)

### Bug Fixes

* trigger release ([39aaefc](39aaefc202))

### Reverts

* botched backport (backport [#53967](https://github.com/frappe/erpnext/issues/53967)) ([#53968](https://github.com/frappe/erpnext/issues/53968)) ([75344e9](75344e9e82)), closes [#53776](https://github.com/frappe/erpnext/issues/53776) [#53766](https://github.com/frappe/erpnext/issues/53766) [#53767](https://github.com/frappe/erpnext/issues/53767)
2026-03-31 14:30:20 +00:00
Mihir Kandoi
39aaefc202 fix: trigger release 2026-03-31 19:58:36 +05:30
mergify[bot]
75344e9e82 revert: botched backport (backport #53967) (#53968)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix(manufacturing): apply work order status filter in job card (#53776)"
fix(manufacturing): apply work order status filter in job card (backport #53766) (#53767)"
2026-03-31 19:24:30 +05:30
Frappe PR Bot
d39072a689 chore(release): Bumped to Version 15.103.0
# [15.103.0](https://github.com/frappe/erpnext/compare/v15.102.0...v15.103.0) (2026-03-30)

### Bug Fixes

* **bank_account:** added validation to fetch bank account details using `get_bank_account_details` (backport [#53926](https://github.com/frappe/erpnext/issues/53926)) ([#53929](https://github.com/frappe/erpnext/issues/53929)) ([d16061f](d16061f1bc))
* change shipment parcel dimension fields from Int to Float (backport [#53867](https://github.com/frappe/erpnext/issues/53867)) ([#53872](https://github.com/frappe/erpnext/issues/53872)) ([a21b82b](a21b82b238))
* **contract_template:** restrict `create`, `write` and `delete` access only to `System Manager` (backport [#53787](https://github.com/frappe/erpnext/issues/53787)) ([#53788](https://github.com/frappe/erpnext/issues/53788)) ([d50c727](d50c727f89))
* correct item valuation when "Deduct" is used in Purchase Invoice and Receipt. ([2585287](25852879f6))
* **email_campaign:** prevent unsubscribing entire campaign when email group member unsubscribes ([6151a49](6151a496e7))
* flaky currency exchange test (backport [#53813](https://github.com/frappe/erpnext/issues/53813)) ([#53816](https://github.com/frappe/erpnext/issues/53816)) ([d9cd09b](d9cd09b24a))
* invalid dynamic link filter for address doctype (backport [#53849](https://github.com/frappe/erpnext/issues/53849)) ([#53851](https://github.com/frappe/erpnext/issues/53851)) ([f7536f6](f7536f645b))
* **item_dashboard:** escaping `warehouse`, `item_code`, `stock_uom` and `item_name` on `get_data` (backport [#53904](https://github.com/frappe/erpnext/issues/53904)) ([#53912](https://github.com/frappe/erpnext/issues/53912)) ([db70d2e](db70d2e4df))
* **manufacturing:** apply work order status filter in job card ([#53776](https://github.com/frappe/erpnext/issues/53776)) ([78635eb](78635ebe99))
* **manufacturing:** apply work order status filter in job card (backport [#53766](https://github.com/frappe/erpnext/issues/53766)) ([#53767](https://github.com/frappe/erpnext/issues/53767)) ([d6afb9b](d6afb9b10a))
* **manufacturing:** close work order status when stock reservation is… (backport [#53714](https://github.com/frappe/erpnext/issues/53714)) ([#53720](https://github.com/frappe/erpnext/issues/53720)) ([468ca2b](468ca2bde1))
* **manufacturing:** update condition for base hour rate calculation ([#53777](https://github.com/frappe/erpnext/issues/53777)) ([64956ab](64956ab59c))
* **manufacturing:** update the qty precision (backport [#53874](https://github.com/frappe/erpnext/issues/53874)) ([#53884](https://github.com/frappe/erpnext/issues/53884)) ([46f751e](46f751e403))
* **opening_invoice_creation_tool:** sanitize summary content for dashboard (backport [#53917](https://github.com/frappe/erpnext/issues/53917)) ([#53923](https://github.com/frappe/erpnext/issues/53923)) ([b35a6c2](b35a6c2e73))
* purchase invoice for internal transfers should not require PO (backport [#53791](https://github.com/frappe/erpnext/issues/53791)) ([#53792](https://github.com/frappe/erpnext/issues/53792)) ([0a28fb3](0a28fb3ae1))
* purchase invoice missing item ([bcd56ab](bcd56abb62))
* **stock:** add warehouse filter to pick work order raw materials (backport [#53748](https://github.com/frappe/erpnext/issues/53748)) ([#53897](https://github.com/frappe/erpnext/issues/53897)) ([fffd3a7](fffd3a785c))
* **stock:** handle legacy single sle recon entries ([d09207a](d09207ab82))
* **stock:** update company validation for expense account in lcv ([40c2b3c](40c2b3c0f6))
* **templates:** escape attachment `file_url` and `file_name` in `order.html` and `projects.html` ([7b9f262](7b9f2626f8))
* **templates:** using correct syntax of `include` in `projects.html` ([979c594](979c594e98))
* **test:** enable perpetual inventory ([88c16c8](88c16c8378))
* validate if quantity greater than 0 in item dashboard (backport [#53846](https://github.com/frappe/erpnext/issues/53846)) ([#53847](https://github.com/frappe/erpnext/issues/53847)) ([ddf6eab](ddf6eab013))
* **warehouse_capacity_dashboard:** escaping `warehouse`, `item_code` and `company` on `get_data` (backport [#53894](https://github.com/frappe/erpnext/issues/53894)) ([#53899](https://github.com/frappe/erpnext/issues/53899)) ([1eda22c](1eda22c2bd))

### Features

* **report:** add service start/end date and amount with roll-ups in deferred revenue/expense report ([14088ee](14088ee7ac))
2026-03-30 18:03:28 +00:00
diptanilsaha
f4a1f04566 Merge pull request #53916 from frappe/version-15-hotfix 2026-03-30 23:31:54 +05:30
Frappe PR Bot
1d14ba1639 chore(release): Bumped to Version 15.102.0
# [15.102.0](https://github.com/frappe/erpnext/compare/v15.101.3...v15.102.0) (2026-03-23)

### Bug Fixes

* Adding validation for operation time in BOM ([7707a79](7707a79d44))
* batch validation for subcontracting receipt ([32c0532](32c0532dec))
* **budget-variance-report:** validate 'budget_against' filter (backport [#53079](https://github.com/frappe/erpnext/issues/53079)) ([#53663](https://github.com/frappe/erpnext/issues/53663)) ([d96590c](d96590c4d9))
* check for `submit` permissions instead of `write` permissions when updating status (backport [#53697](https://github.com/frappe/erpnext/issues/53697)) ([#53702](https://github.com/frappe/erpnext/issues/53702)) ([46e784d](46e784d094))
* check posting_date in args (backport [#53303](https://github.com/frappe/erpnext/issues/53303)) ([#53611](https://github.com/frappe/erpnext/issues/53611)) ([e0f1e75](e0f1e757f3))
* consider returned qty in subcontracting report (backport [#53616](https://github.com/frappe/erpnext/issues/53616)) ([#53620](https://github.com/frappe/erpnext/issues/53620)) ([af86fd3](af86fd3cb4))
* deadlock issue for SLE ([540a854](540a8540d6))
* do not overwrite expense account in stock entry (backport [#53658](https://github.com/frappe/erpnext/issues/53658)) ([#53660](https://github.com/frappe/erpnext/issues/53660)) ([90e4f90](90e4f9026d))
* ignore cost center (backport [#53063](https://github.com/frappe/erpnext/issues/53063)) ([#53613](https://github.com/frappe/erpnext/issues/53613)) ([562f93e](562f93e75c))
* incorrect sle calculation when doc has project ([#53599](https://github.com/frappe/erpnext/issues/53599)) ([7acd435](7acd435835))
* initialize all tax columns to resolve Key error in `item_wise_sales_register` and `item_wise_purchase_register` reports (backport [#53323](https://github.com/frappe/erpnext/issues/53323)) ([#53583](https://github.com/frappe/erpnext/issues/53583)) ([119195c](119195c6fa))
* **manufacturing:** update non-stock item dict (backport [#53689](https://github.com/frappe/erpnext/issues/53689)) ([#53698](https://github.com/frappe/erpnext/issues/53698)) ([c0ce34e](c0ce34e12c))
* merge conflicts ([b3f0e2a](b3f0e2a00d))
* PO should not be required for internal transfers (backport [#53681](https://github.com/frappe/erpnext/issues/53681)) ([#53683](https://github.com/frappe/erpnext/issues/53683)) ([04d74ad](04d74ad6eb))
* python error in manufacture entry if transfer against is job card (backport [#53615](https://github.com/frappe/erpnext/issues/53615)) ([#53617](https://github.com/frappe/erpnext/issues/53617)) ([5a3bc27](5a3bc27e2c))
* **sales_invoice:** using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods ([#53636](https://github.com/frappe/erpnext/issues/53636)) ([f8ab56e](f8ab56ecc9))
* set customer details on customer creation at login ([#53509](https://github.com/frappe/erpnext/issues/53509)) ([4f39dfd](4f39dfd642))
* shipping rule applied twice on non stock items (backport [#53655](https://github.com/frappe/erpnext/issues/53655)) ([#53686](https://github.com/frappe/erpnext/issues/53686)) ([5e767ea](5e767ea595))
* stock queue for SABB ([461bc17](461bc1733f))
* **stock:** add company filter while fetching batches (backport [#53369](https://github.com/frappe/erpnext/issues/53369)) ([#53580](https://github.com/frappe/erpnext/issues/53580)) ([c09c599](c09c5999dc))
* **stock:** fix email error message (backport [#53606](https://github.com/frappe/erpnext/issues/53606)) ([#53632](https://github.com/frappe/erpnext/issues/53632)) ([6ea3d56](6ea3d56972))
* **trends:** added validation for `period_based_on` filter (backport [#53690](https://github.com/frappe/erpnext/issues/53690)) ([#53691](https://github.com/frappe/erpnext/issues/53691)) ([974755b](974755b224))
* validate permission before updating status (backport [#53651](https://github.com/frappe/erpnext/issues/53651)) ([#53652](https://github.com/frappe/erpnext/issues/53652)) ([defa1d4](defa1d4a76))

### Features

* add cost center field to the stock entry accounting dimension tab ([e17b5df](e17b5dfe61))
2026-03-23 14:59:14 +00:00
diptanilsaha
a270c02bb4 Merge pull request #53700 from frappe/version-15-hotfix 2026-03-23 20:27:38 +05:30
Frappe PR Bot
94900cb8b8 chore(release): Bumped to Version 15.101.3
## [15.101.3](https://github.com/frappe/erpnext/compare/v15.101.2...v15.101.3) (2026-03-19)

### Bug Fixes

* **sales_invoice:** using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods ([#53636](https://github.com/frappe/erpnext/issues/53636)) ([65d8a17](65d8a176a6))
2026-03-19 10:07:46 +00:00
diptanilsaha
c1be262357 Merge pull request #53639 from frappe/mergify/bp/version-15/pr-53636
fix(sales_invoice): using `msgprint` and removed condition checking for `is_created_using_pos` to refetch payment methods (backport #53636)
2026-03-19 15:36:17 +05:30
diptanilsaha
65d8a176a6 fix(sales_invoice): using msgprint and removed condition checking for is_created_using_pos to refetch payment methods (#53636)
(cherry picked from commit f8ab56ecc9)
2026-03-19 08:49:49 +00:00
Frappe PR Bot
572d8530b6 chore(release): Bumped to Version 15.101.2
## [15.101.2](https://github.com/frappe/erpnext/compare/v15.101.1...v15.101.2) (2026-03-18)

### Bug Fixes

* incorrect sle calculation when doc has project ([#53599](https://github.com/frappe/erpnext/issues/53599)) ([9e10dec](9e10dec903))
2026-03-18 13:42:20 +00:00
rohitwaghchaure
0fa8cc76f5 Merge pull request #53602 from frappe/mergify/bp/version-15/pr-53600
fix: incorrect sle calculation when doc has project (backport #53599) (backport #53600)
2026-03-18 19:10:44 +05:30
Mihir Kandoi
9e10dec903 fix: incorrect sle calculation when doc has project (#53599)
(cherry picked from commit 6cb6a52ded)
(cherry picked from commit 7acd435835)
2026-03-18 13:38:17 +00:00
Frappe PR Bot
c912df95cb chore(release): Bumped to Version 15.101.1
## [15.101.1](https://github.com/frappe/erpnext/compare/v15.101.0...v15.101.1) (2026-03-18)

### Bug Fixes

* add item_name to quick entry fields in Item doctype (backport [#53530](https://github.com/frappe/erpnext/issues/53530)) ([#53532](https://github.com/frappe/erpnext/issues/53532)) ([0e770c0](0e770c0bbd))
* Append existing ignored doctypes in Journal Entry on_cancel instead of overwriting ([b73d970](b73d9700d0))
* **banking:** include paid purchase invoices in reports and bank clearance ([#52675](https://github.com/frappe/erpnext/issues/52675)) ([ab9d960](ab9d960aa8))
* broke cost center filter in get outstanding reference docs ([53e3bfb](53e3bfbf22))
* change "Date" label to "Posting Date" in Sales Invoice and Purchase Invoice (backport [#53503](https://github.com/frappe/erpnext/issues/53503)) ([#53516](https://github.com/frappe/erpnext/issues/53516)) ([eec8cf8](eec8cf8a71))
* coderebbit review ([05d614e](05d614eb04))
* correct function syntax in TDS Computation Report ([94972da](94972da845))
* correct overlap detection in JobCard.has_overlap (backport [#53473](https://github.com/frappe/erpnext/issues/53473)) ([#53522](https://github.com/frappe/erpnext/issues/53522)) ([d262a65](d262a65b00))
* correct payment terms fetching and recalculation logic ([79b0482](79b04826d9))
* correct payment terms fetching and recalculation logic ([3148816](3148816451))
* Creating new item price incase of changes in expired item price (backport [#53534](https://github.com/frappe/erpnext/issues/53534)) ([#53544](https://github.com/frappe/erpnext/issues/53544)) ([526ffc1](526ffc1176))
* **delivery note:** avoid maintaining si_detail on return delivery note (backport [#52456](https://github.com/frappe/erpnext/issues/52456)) ([#53352](https://github.com/frappe/erpnext/issues/53352)) ([034d460](034d460ae1))
* do not modify rate in the child item merely for comparison (backport [#53301](https://github.com/frappe/erpnext/issues/53301)) ([#53375](https://github.com/frappe/erpnext/issues/53375)) ([0e00ab8](0e00ab8865))
* do not set valuation rate for invoice without update stock ([284ccd1](284ccd1def))
* enhance sorting and optimize GL entry retrieval ([93ebec9](93ebec90ef))
* **italy:** fix e-invoice ScontoMaggiorazione structure and included_in_print_rate support ([#53334](https://github.com/frappe/erpnext/issues/53334)) ([b9c8e8d](b9c8e8d478))
* **manufacturing:** update working hours validation (backport [#53559](https://github.com/frappe/erpnext/issues/53559)) ([#53566](https://github.com/frappe/erpnext/issues/53566)) ([9771ed4](9771ed4c57))
* **minor:** filter bank accounts in bank statement import ([#53481](https://github.com/frappe/erpnext/issues/53481)) ([a5d1afe](a5d1afe304))
* NoneType error when template description is to be copied to variant (backport [#53358](https://github.com/frappe/erpnext/issues/53358)) ([#53365](https://github.com/frappe/erpnext/issues/53365)) ([0612f1c](0612f1c941))
* **p&l_statement:** disable accumulated value filter by default (backport [#53488](https://github.com/frappe/erpnext/issues/53488)) ([#53489](https://github.com/frappe/erpnext/issues/53489)) ([b63b532](b63b5320f2))
* precision issue in production plan (backport [#53370](https://github.com/frappe/erpnext/issues/53370)) ([#53373](https://github.com/frappe/erpnext/issues/53373)) ([5737d2a](5737d2afa3))
* re-calculate taxes and totals after resetting bundle item rate (backport [#53342](https://github.com/frappe/erpnext/issues/53342)) ([#53349](https://github.com/frappe/erpnext/issues/53349)) ([db251c6](db251c6e11))
* refactor GL entry mapping to include voucher type ([c2e6759](c2e67599f5))
* **regional:** rename duplicate Customer fields in Italy setup (backport [#50921](https://github.com/frappe/erpnext/issues/50921)) ([#53397](https://github.com/frappe/erpnext/issues/53397)) ([2a70203](2a70203cab))
* remove redundant pos print format ([#53348](https://github.com/frappe/erpnext/issues/53348)) ([8497d1f](8497d1f8cf))
* sales order indicator should be based on available qty rather th… (backport [#53456](https://github.com/frappe/erpnext/issues/53456)) ([#53457](https://github.com/frappe/erpnext/issues/53457)) ([a6cf31e](a6cf31edad))
* **sales_invoice:** reset payment methods on `pos_profile` change (backport [#53514](https://github.com/frappe/erpnext/issues/53514)) ([#53560](https://github.com/frappe/erpnext/issues/53560)) ([239728e](239728e4d9))
* **serial_and_batch_bundle_selector:** handle CSV attachment properly (backport [#53460](https://github.com/frappe/erpnext/issues/53460)) ([#53461](https://github.com/frappe/erpnext/issues/53461)) ([7a7c4a0](7a7c4a03f0))
* skip validate_stock_accounts when perpetual inventory is disabled ([3bc9190](3bc9190795))
* stock adjustment entry ([ac6c06d](ac6c06daf9))
* **stock:** fix the property setter (backport [#53422](https://github.com/frappe/erpnext/issues/53422)) ([#53573](https://github.com/frappe/erpnext/issues/53573)) ([57815a0](57815a07ac))
* **support-settings:** disable the auto-close tickets feature if `close_issue_after_days` is set to 0 (backport [#53499](https://github.com/frappe/erpnext/issues/53499)) ([#53504](https://github.com/frappe/erpnext/issues/53504)) ([30fe711](30fe711c44))
* **tds-report:** correct party type filtering and refactor ([e5eb540](e5eb5406da))
* test case ([c384564](c384564314))
* update delivery date in line items ([#53331](https://github.com/frappe/erpnext/issues/53331)) ([85c4cc3](85c4cc3e1b))
* update item description in Production Plan Assembly Items table ([e3e9d7b](e3e9d7b19e))
* update label on company change ([908e185](908e185cfe))
* update user status depends on employee status ([c5796fe](c5796fed4a))
* use completion_date not posting date ([6d47660](6d476604df))
* use qb to prevent incorrect sql due to user permissions ([03f0922](03f09222cf))
* valuation rate for no Use Batch wise Valuation batches ([ca6872c](ca6872c768))
2026-03-18 04:58:29 +00:00
diptanilsaha
915315ef1b Merge pull request #53541 from frappe/version-15-hotfix 2026-03-18 10:26:58 +05:30
Frappe PR Bot
1ffd814f92 chore(release): Bumped to Version 15.101.0
# [15.101.0](https://github.com/frappe/erpnext/compare/v15.100.2...v15.101.0) (2026-03-10)

### Bug Fixes

* **accounts:** compute tax net_amount in JS controller ([6ad84d6](6ad84d66cc))
* **accounts:** round and convert net_amount to company currency in JS tax controller ([516ad90](516ad9021b))
* balance qty for inv dimension ([6898d70](6898d70382))
* better validation message for Purchase Invoice with Update Stock ([b7fd9ae](b7fd9aea6a))
* client-side taxes calculation ([#44510](https://github.com/frappe/erpnext/issues/44510)) ([717c5b2](717c5b25eb)), closes [#44328](https://github.com/frappe/erpnext/issues/44328)
* correct logic for repair cost in asset repair ([c71557f](c71557f432))
* disallow all actions on job card if work order is closed ([7b2e483](7b2e4832aa))
* enforce permission check for purchase invoice and update test to use service expense account ([a6dd078](a6dd07802a))
* **gross-profit:** apply precision-based rounding to grouped totals ([b59dc17](b59dc173b8))
* **help:** escape query (backport [#53192](https://github.com/frappe/erpnext/issues/53192)) ([#53194](https://github.com/frappe/erpnext/issues/53194)) ([ba4a99b](ba4a99b22c))
* **manufacturing:** ignore sales order validation for subassembly item ([624d1d4](624d1d4759))
* **manufacturing:** show returned qty in progress bar ([260d87a](260d87a80c))
* removed non existent patch ([fd8fac7](fd8fac7d40))
* **selling:** update delivery date in line items ([dfbb3e9](dfbb3e97a8))
* set default repair cost to 0 if no value is returned ([0b1746a](0b1746a4c8))
* skip asset sale processing for internal transfer invoices ([a7e8f31](a7e8f31f56))
* stock balance report qty ([180e232](180e232eb0))
* **test:** ensure warehouse is consistently referenced in asset repair tests ([ed428ce](ed428ceb1c))
* **test:** include warehouse parameter in asset repair test case ([bcc542b](bcc542b1f9))
* updating costing based on employee change in timesheet ([be59810](be598108b6))
* validation for cancellation ([c142a2b](c142a2be9c))

### Features

* allowing rate modification in update item in quotation (backport [#53147](https://github.com/frappe/erpnext/issues/53147)) ([#53150](https://github.com/frappe/erpnext/issues/53150)) ([072ab8d](072ab8d5f3))
* **manufacturing:** show disassembled qty in progress bar ([c572a01](c572a019b4))
2026-03-10 14:48:03 +00:00
ruthra kumar
c6e7cf13b5 Merge pull request #53293 from frappe/version-15-hotfix
chore: release v15
2026-03-10 20:16:23 +05:30
Frappe PR Bot
1ee03f41f2 chore(release): Bumped to Version 15.100.2
## [15.100.2](https://github.com/frappe/erpnext/compare/v15.100.1...v15.100.2) (2026-03-06)

### Bug Fixes

* stock balance report qty ([9b49a27](9b49a27af6))
2026-03-06 08:35:24 +00:00
rohitwaghchaure
c2f2331d49 Merge pull request #53209 from frappe/mergify/bp/version-15/pr-53207
fix: stock balance report qty (backport #53200) (backport #53207)
2026-03-06 14:04:01 +05:30
rohitwaghchaure
5af5de3315 chore: fix conflicts
(cherry picked from commit 54fdce648e)
2026-03-06 07:38:33 +00:00
Rohit Waghchaure
9b49a27af6 fix: stock balance report qty
(cherry picked from commit a15e5fdc4e)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.py
(cherry picked from commit 180e232eb0)
2026-03-06 07:38:32 +00:00
Frappe PR Bot
bcc52090c9 chore(release): Bumped to Version 15.100.1
## [15.100.1](https://github.com/frappe/erpnext/compare/v15.100.0...v15.100.1) (2026-03-05)

### Bug Fixes

* balance qty for inv dimension ([68c79a4](68c79a4a79))
2026-03-05 09:43:52 +00:00
rohitwaghchaure
2ca9b75aa6 Merge pull request #53183 from frappe/mergify/bp/version-15/pr-53180
fix: balance qty for inv dimension (backport #52745) (backport #53180)
2026-03-05 14:59:14 +05:30
Rohit Waghchaure
68c79a4a79 fix: balance qty for inv dimension
(cherry picked from commit a3eafe5b18)
(cherry picked from commit 6898d70382)
2026-03-05 08:58:30 +00:00
Frappe PR Bot
2574c4c18c chore(release): Bumped to Version 15.100.0
# [15.100.0](https://github.com/frappe/erpnext/compare/v15.99.1...v15.100.0) (2026-03-03)

### Bug Fixes

* **accounts receivable:** include invoice payment terms template (backport [#51940](https://github.com/frappe/erpnext/issues/51940)) ([#53105](https://github.com/frappe/erpnext/issues/53105)) ([f5f40db](f5f40dbcc3))
* allow allowed roles to bypass over billing validation ([8e59fe9](8e59fe90cc))
* correct sle voucher_type comparison in get_ref_doctype ([a587b6a](a587b6a57c))
* ensure cache is cleared on fiscal year update and trash ([7278166](7278166b2c))
* ensure contacts are processed only if present in create_prospect_against_crm_deal ([e2b6179](e2b6179b17))
* handle html email template separately in RFQ to avoid jinja context error ([90169a3](90169a39bb))
* item description html validation error ([3c6a120](3c6a120c5c))
* old stock reco entries causing issue in the stock balance report ([b712467](b712467049))
* opening qty in stock balance ([470a9b3](470a9b38b1))
* **payment entry:** round unallocated amount ([76a1907](76a19076bf))
* populate mr owner and set po owner as fallback ([c1f2991](c1f2991694))
* **pricing_rule:** strict validation of `transaction_type` ([5f82db2](5f82db200e))
* resolve conflicts ([6846f02](6846f02cea))
* **selling:** handle selling price validation for FG item ([d5cc51b](d5cc51b426))
* serial no status for Disassemble entry ([8ce541b](8ce541bf46))
* set company based expense account ([74e71f3](74e71f3868))
* **stock:** validate company for receipt documents and expense accounts ([4462088](44620884c1))
* **trial-balance:** totals with filter  `show_group_accounts` enabled ([eabaef2](eabaef2f0b))
* use conversion factor when creating stock entry from pick list ([f2e1482](f2e1482f63))
* use stock qty instead of qty when creating stock entry from MR ([2984f79](2984f79a69))
* use the correct precision value in stock reco ([6d726e4](6d726e4b64))
* validate warehouse of SABB for draft entry ([d5e2515](d5e25153f8))
* Variant Items, List View Enabled to Variant Status Change ([#38468](https://github.com/frappe/erpnext/issues/38468)) ([25b1690](25b169059d))
* voucher detail no in SABB ([6e5738e](6e5738e95c))

### Features

* UOM query filter for opportunity items ([f6a05ec](f6a05ec85e))

### Performance Improvements

* add index on reference_purchase_receipt column ([0766c0e](0766c0ea43))
2026-03-03 17:57:48 +00:00
diptanilsaha
bc9f3a38ce Merge pull request #53142 from frappe/version-15-hotfix 2026-03-03 23:26:05 +05:30
107 changed files with 981 additions and 2158 deletions

View File

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

View File

@@ -34,13 +34,6 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -324,21 +317,13 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Expense Account"
"account_type": "Stock Adjustment"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -355,7 +340,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Depreciation"
"account_type": "Accumulated Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -82,15 +82,13 @@ class AccountingDimension(Document):
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def on_update(self):
def after_insert(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
frappe.enqueue(
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def on_trash(self):
if frappe.flags.in_test:
@@ -105,6 +103,10 @@ class AccountingDimension(Document):
if not self.fieldname:
self.fieldname = scrub(self.label)
def on_update(self):
frappe.flags.accounting_dimensions = None
frappe.flags.accounting_dimensions_details = None
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:

View File

@@ -1,41 +1,108 @@
{
"actions": [],
"creation": "2018-11-22 23:47:02.804568",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"tax_type",
"tax_rate"
],
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-11-22 23:47:02.804568",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"fieldname": "tax_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Tax",
"options": "Account",
"reqd": 1
},
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_type",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tax_rate",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"istable": 1,
"links": [],
"modified": "2026-04-30 23:49:27.020639",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-12-21 23:51:39.445198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@@ -839,7 +839,7 @@ frappe.ui.form.on("Payment Entry", {
paid_amount: function (frm) {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (!frm.doc.received_amount) {
if (frm.doc.paid_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
@@ -860,7 +860,7 @@ frappe.ui.form.on("Payment Entry", {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate)
);
if (!frm.doc.paid_amount) {
if (frm.doc.received_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("paid_amount", frm.doc.received_amount);
if (frm.doc.target_exchange_rate) {

View File

@@ -2306,20 +2306,22 @@ def get_outstanding_reference_documents(args, validate=False):
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
condition = " and voucher_type={} and voucher_no={}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
condition += " and cost_center='%s'" % args.get("cost_center")
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
@@ -2328,19 +2330,18 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += f" and {fieldname} between {from_date} and {to_date}"
condition += " and {} between '{}' and '{}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= {from_date}"
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= {to_date}"
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
@@ -2560,7 +2561,7 @@ def get_orders_to_be_billed(
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"

View File

@@ -200,30 +200,6 @@ class TestPaymentEntry(FrappeTestCase):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
def test_reference_outstanding_amount_on_advance_pull(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=1, rate=1000)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.received_amount = 500
pe.references[0].allocated_amount = 500
pe.insert()
pe.submit()
so.reload()
self.assertEqual(so.advance_paid, 500)
si = make_sales_invoice(so.name)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -1961,37 +1937,6 @@ class TestPaymentEntry(FrappeTestCase):
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,

View File

@@ -812,7 +812,6 @@
},
{
"default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -859,7 +858,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-04-20 16:16:12.322024",
"modified": "2024-05-07 15:56:54.343317",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -658,7 +658,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.is_recursive:
transaction_qty = sum(
[
flt(row.qty)
row.qty
for row in doc.items
if not row.is_free_item
and row.item_code == args.item_code

View File

@@ -115,12 +115,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -131,13 +126,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -471,14 +460,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
}
on_submit() {
@@ -587,6 +575,12 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {

View File

@@ -109,7 +109,6 @@
"sales_invoice_item",
"material_request",
"material_request_item",
"delivered_by_supplier",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -732,6 +731,7 @@
"label": "Valuation Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -979,21 +979,12 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency"
},
{
"default": "0",
"fieldname": "delivered_by_supplier",
"fieldtype": "Check",
"hidden": 1,
"label": "Delivered by Supplier",
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-05-06 08:08:40.782395",
"modified": "2025-10-14 13:01:54.441511",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -31,7 +31,6 @@ class PurchaseInvoiceItem(Document):
conversion_factor: DF.Float
cost_center: DF.Link | None
deferred_expense_account: DF.Link | None
delivered_by_supplier: DF.Check
description: DF.TextEditor | None
discount_amount: DF.Currency
discount_percentage: DF.Percent

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -136,15 +136,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
}
cur_frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
},
__("Create")
);
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -168,7 +166,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
);
}
}
this.toggle_get_items();
// Show buttons only when pos view is active
if (cint(doc.docstatus == 0) && cur_frm.page.current_view_name !== "pos" && !doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.delivery_note_btn();
this.frm.cscript.quotation_btn();
}
this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
@@ -254,93 +258,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
}
toggle_get_items() {
const buttons = ["Sales Order", "Quotation", "Timesheet", "Delivery Note"];
buttons.forEach((label) => {
this.frm.remove_custom_button(label, "Get Items From");
});
if (cint(this.frm.doc.docstatus) !== 0 || this.frm.page.current_view_name === "pos") {
return;
}
if (!this.frm.doc.is_return) {
this.frm.cscript.sales_order_btn();
this.frm.cscript.quotation_btn();
this.frm.cscript.timesheet_btn();
}
this.frm.cscript.delivery_note_btn();
}
timesheet_btn() {
var me = this;
me.frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: me.frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: me.frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
me.frm.events.add_timesheet_data(me.frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
sales_order_btn() {
var me = this;
this.$sales_order_btn = this.frm.add_custom_button(
@@ -405,12 +322,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.$delivery_note_btn = this.frm.add_custom_button(
__("Delivery Note"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
source_doctype: "Delivery Note",
@@ -423,7 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
var filters = {
docstatus: 1,
company: me.frm.doc.company,
is_return: me.frm.doc.is_return,
is_return: 0,
};
if (me.frm.doc.customer) filters["customer"] = me.frm.doc.customer;
return {
@@ -542,14 +453,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
}
set_dynamic_labels() {
@@ -685,14 +594,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
this.calculate_taxes_and_totals();
}
apply_tds(frm) {
this.frm.clear_table("tax_withholding_entries");
}
is_return() {
this.toggle_get_items();
}
};
// for backward compatibility: combine new and previous states
@@ -1138,6 +1039,71 @@ frappe.ui.form.on("Sales Invoice", {
},
refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Item Code"),
fieldname: "item_code",
fieldtype: "Link",
options: "Item",
get_query: () => {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
is_sales_item: 1,
customer: frm.doc.customer,
has_variants: 0,
},
};
},
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
item_code: data.item_code,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}

View File

@@ -2917,7 +2917,7 @@ class TestSalesInvoice(FrappeTestCase):
si.submit()
# Check if adjustment entry is created
self.assertFalse(
self.assertTrue(
frappe.db.exists(
"GL Entry",
{

View File

@@ -25,10 +25,6 @@ frappe.ui.form.on("Shipping Rule", {
},
calculate_based_on: function (frm) {
frm.trigger("toggle_reqd");
if (frm.doc.calculate_based_on === "Fixed") {
frm.clear_table("conditions");
frm.refresh_field("conditions");
}
},
toggle_reqd: function (frm) {
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");

View File

@@ -58,11 +58,6 @@ class ShippingRule(Document):
self.validate_overlapping_shipping_rule_conditions()
def validate_from_to_values(self):
if self.calculate_based_on == "Fixed":
if self.conditions:
self.set("conditions", [])
return
zero_to_values = []
for d in self.get("conditions"):

View File

@@ -34,17 +34,6 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -120,49 +120,3 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,17 +53,6 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -36,17 +36,6 @@ frappe.query_reports["Accounts Receivable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -194,7 +194,6 @@ class ReceivablePayableReport:
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.voucher_balance[key].project = ple.project
self.get_invoices(ple)
@@ -361,7 +360,6 @@ class ReceivablePayableReport:
posting_date,
account_currency,
cost_center,
project,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
@@ -390,7 +388,6 @@ class ReceivablePayableReport:
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
"project",
]:
_d[field] = x.get(field)
@@ -928,7 +925,6 @@ class ReceivablePayableReport:
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.project,
ple.party,
ple.posting_date,
ple.due_date,
@@ -996,9 +992,6 @@ class ReceivablePayableReport:
if self.filters.cost_center:
self.get_cost_center_conditions()
if self.filters.project:
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
@@ -1238,7 +1231,6 @@ class ReceivablePayableReport:
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
self.add_column(
label=_("Voucher No"),
@@ -1411,7 +1403,6 @@ class InitSQLProceduresForAR:
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
@@ -1431,7 +1422,6 @@ class InitSQLProceduresForAR:
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
@@ -1460,7 +1450,7 @@ class InitSQLProceduresForAR:
begin
if not exists (select name from `{_voucher_balance_name}` where name = `{genkey_function_name}`(ple, false))
then
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, false), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
@@ -1502,7 +1492,7 @@ class InitSQLProceduresForAR:
end if;
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
insert into `{_voucher_balance_name}` values (`{genkey_function_name}`(ple, true), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""

View File

@@ -1204,52 +1204,3 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 100.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project even when the payment entry
has no project set.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
# payment has no project — report row must still show the invoice's project
self.create_payment_entry(si.name)
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,17 +53,6 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -501,7 +501,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
and base_tax_amount_after_discount_amount != 0
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),

View File

@@ -6,7 +6,6 @@ from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestPurchaseRegister(FrappeTestCase):
@@ -27,52 +26,6 @@ class TestPurchaseRegister(FrappeTestCase):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
pr = make_purchase_receipt(
company="_Test Company 6",
supplier="_Test Supplier",
item="_Test Item",
warehouse="_Test Warehouse - _TC6",
cost_center="_Test Cost Center - _TC6",
do_not_save=1,
do_not_submit=1,
qty=1,
rate=1000,
)
pr.append(
"taxes",
{
"account_head": "GST - _TC6",
"cost_center": "_Test Cost Center - _TC6",
"add_deduct_tax": "Add",
"category": "Valuation and Total",
"charge_type": "Actual",
"description": "PR Tax",
"tax_amount": 100.0,
"rate": 100,
},
)
pr.insert()
pr.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
report_results = execute(filters)
first_row = frappe._dict(report_results[1][0])
self.assertEqual(first_row.voucher_no, pi.name)
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")

View File

@@ -5,7 +5,6 @@ from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
@@ -76,43 +75,6 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
report_output = {k: v for k, v in res[0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
si = self.create_sales_invoice(rate=98)
# Real workflow setup: create a Sales Order with taxes in the shared child table.
so = make_sales_order(
item=self.item,
company=self.company,
customer=self.customer,
rate=77,
do_not_save=1,
do_not_submit=1,
)
so.append(
"taxes",
{
"charge_type": "Actual",
"account_head": self.income_account,
"description": "SO Tax",
"tax_amount": 55.0,
},
)
so.insert()
so.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
res = [x for x in report[1] if x.get("voucher_no") == si.name]
self.assertEqual(len(res), 1)
result = frappe._dict(res[0])
self.assertEqual(result.net_total, 98.0)
self.assertEqual(result.tax_total, 0)
self.assertEqual(result.grand_total, 98.0)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{

View File

@@ -119,8 +119,8 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map):
row.update(
{
"tax_withholding_category": tax_withholding_category or "",
"party_entity_type": party_map.get(party, {}).get(party_type),
"section_code": tax_withholding_category or "",
"entity_type": party_map.get(party, {}).get(party_type),
"rate": rate,
"total_amount": total_amount,
"grand_total": grand_total,
@@ -141,7 +141,7 @@ def get_result(filters, tds_accounts, tax_category_map, net_total_map):
else:
entries[key] = row
out = list(entries.values())
out.sort(key=lambda x: (x["tax_withholding_category"], x["transaction_date"], x["ref_no"]))
out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"]))
return out
@@ -205,9 +205,9 @@ def get_columns(filters):
pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id"
columns = [
{
"label": _("Tax Withholding Category"),
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "tax_withholding_category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 90,
},
@@ -236,12 +236,7 @@ def get_columns(filters):
columns.extend(
[
{
"label": _(f"{filters.get('party_type', 'Party')} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 100,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100},
]
)
if filters.party_type == "Supplier":

View File

@@ -118,7 +118,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
voucher_expected_values = expected_values[i]
voucher_actual_values = (
voucher.ref_no,
voucher.tax_withholding_category,
voucher.section_code,
voucher.rate,
voucher.base_tax_withholding_net_total,
voucher.base_total,

View File

@@ -48,25 +48,28 @@ def group_by_party_and_category(data, filters):
party_category_wise_map = {}
for row in data:
key = (row.get("party_type"), row.get("party"), row.get("tax_withholding_category"))
party_category_wise_map.setdefault(
key,
(row.get("party"), row.get("section_code")),
{
"pan": row.get("pan"),
"tax_id": row.get("tax_id"),
"party": row.get("party"),
"party_type": row.get("party_type"),
"party_name": row.get("party_name"),
"tax_withholding_category": row.get("tax_withholding_category"),
"party_entity_type": row.get("party_entity_type"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
"rate": row.get("rate"),
"total_amount": 0.0,
"tax_amount": 0.0,
},
)
party_category_wise_map.get(key)["total_amount"] += row.get("total_amount", 0.0)
party_category_wise_map.get(key)["tax_amount"] += row.get("tax_amount", 0.0)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
"total_amount", 0.0
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
"tax_amount", 0.0
)
final_result = get_final_result(party_category_wise_map)
@@ -107,18 +110,13 @@ def get_columns(filters):
columns.extend(
[
{
"label": _("Tax Withholding Category"),
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "tax_withholding_category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 180,
},
{
"label": _(f"{filters.get('party_type', 'Party')} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 180,
},
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
{
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
"fieldname": "rate",

View File

@@ -500,7 +500,7 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
if referenced_row.get("outstanding_amount"):
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
reposting_rows.append(referenced_row)
@@ -2320,7 +2320,6 @@ def create_gain_loss_journal(
ref2_detail_no,
cost_center,
dimensions,
project=None,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2347,7 +2346,6 @@ def create_gain_loss_journal(
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
@@ -2365,7 +2363,6 @@ def create_gain_loss_journal(
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,

View File

@@ -41,7 +41,7 @@ frappe.ui.form.on("Asset Movement", {
});
},
refresh: (frm) => {
onload: (frm) => {
frm.trigger("set_required_fields");
},

View File

@@ -440,11 +440,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (
frappe.model.can_create("Payment Entry") &&
flt(doc.per_billed) < 100 &&
doc.status != "Delivered"
) {
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -452,7 +448,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
if (flt(doc.per_billed) < 100) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -709,20 +705,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
var row = frappe.get_doc(cdt, cdn);
if (doc.schedule_date) {
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
row.schedule_date = doc.schedule_date;
refresh_field("schedule_date", cdn, "items");
} else {
field_copy.push("schedule_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
}
}
@@ -797,6 +785,12 @@ cur_frm.cscript.update_status = function (label, status) {
});
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -283,7 +283,7 @@ class RequestforQuotation(BuyingController):
}
)
user.save(ignore_permissions=True)
update_password_link = user._reset_password()
update_password_link = user.reset_password()
return user, update_password_link
@@ -474,11 +474,6 @@ def create_supplier_quotation(doc):
if isinstance(doc, str):
doc = json.loads(doc)
if frappe.session.user not in frappe.get_all(
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
try:
sq_doc = frappe.get_doc(
{

View File

@@ -263,13 +263,6 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
for data in supplier_data:
rfq.append("suppliers", data)
frappe.new_doc(
"Portal User",
user="Administrator",
parent=data.get("supplier"),
parentfield="portal_users",
parenttype="Supplier",
).insert()
rfq.append(
"items",

View File

@@ -115,3 +115,9 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
// for backward compatibility: combine new and previous states
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};

View File

@@ -68,7 +68,6 @@ from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
_get_item_tax_template,
_get_item_tax_template_from_item_group,
get_conversion_factor,
get_item_details,
get_item_tax_map,
@@ -326,7 +325,6 @@ class AccountsController(TransactionBase):
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Purchase Invoice",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
@@ -1753,7 +1751,6 @@ class AccountsController(TransactionBase):
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
arg.get("project"),
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1838,7 +1835,6 @@ class AccountsController(TransactionBase):
d.idx,
self.cost_center,
dimensions_dict,
self.project,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -3648,10 +3644,6 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
}
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
if not child_item.get("item_tax_template"):
child_item.item_tax_template = _get_item_tax_template_from_item_group(args, item.item_group)
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(
parent_doc.get("company"), child_item.item_tax_template, as_json=True

View File

@@ -364,17 +364,7 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = (
flt(
(item.base_net_amount / item.received_qty) * item.qty,
item.precision("base_net_amount"),
)
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
net_rate = item.qty * item.base_net_rate
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate

View File

@@ -356,43 +356,38 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
):
DeliveryNote = frappe.qb.DocType("Delivery Note")
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
doctype = "Delivery Note"
fields = get_fields(doctype, ["name", "customer", "posting_date"])
original_dn = (
frappe.qb.from_(DeliveryNote)
.select(DeliveryNote.name)
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = (
frappe.qb.from_(DeliveryNote)
.select(*[DeliveryNote[f] for f in fields])
.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (DeliveryNote[searchfield].like(f"%{txt}%"))
& (
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
| (
(DeliveryNote.is_return == 1)
& (DeliveryNote.per_billed < 100)
& (DeliveryNote.return_against.isin(original_dn))
return frappe.db.sql(
"""
select {fields}
from `tabDelivery Note`
where `tabDelivery Note`.`{key}` like {txt} and
`tabDelivery Note`.docstatus = 1
and status not in ('Stopped', 'Closed') {fcond}
and (
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
or (
`tabDelivery Note`.is_return = 1
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
)
)
)
{mcond} order by `tabDelivery Note`.`{key}` asc limit {page_len} offset {start}
""".format(
fields=", ".join([f"`tabDelivery Note`.{f}" for f in fields]),
key=searchfield,
fcond=get_filters_cond(doctype, filters, []),
mcond=get_match_cond(doctype),
start=start,
page_len=page_len,
txt="%(txt)s",
),
{"txt": ("%%%s%%" % txt)},
as_dict=as_dict,
)
if filters and isinstance(filters, dict):
for key, value in filters.items():
query = query.where(DeliveryNote[key] == value)
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
return query.run(as_dict=as_dict)
@frappe.whitelist()

View File

@@ -616,11 +616,11 @@ class SellingController(StockController):
if allow_at_arms_length_price:
continue
rate = flt(flt(d.incoming_rate) * flt(d.conversion_factor or 1.0))
if flt(d.rate, d.precision("incoming_rate")) != flt(
rate, d.precision("incoming_rate")
):
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
)
if d.rate != rate:
d.rate = rate
frappe.msgprint(
_(

View File

@@ -186,11 +186,8 @@ class calculate_taxes_and_totals:
bill_for_rejected_quantity_in_purchase_invoice = frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
do_not_round_fields = ["valuation_rate", "incoming_rate"]
for item in self.doc.items:
self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields)
self.doc.round_floats_in(item)
if item.discount_percentage == 100:
item.rate = 0.0
@@ -693,17 +690,18 @@ class calculate_taxes_and_totals:
if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = 0
self.doc.base_rounded_total = 0
self.doc.rounding_adjustment = 0
return
else:
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
self.doc.rounded_total = round_based_on_smallest_currency_fraction(
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
)
# rounding adjustment should always be the difference between grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
# rounding adjustment should always be the difference vetween grand and rounded total
self.doc.rounding_adjustment = flt(
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
)
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])

View File

@@ -1,37 +0,0 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestTaxesAndTotals(FrappeTestCase):
def test_disabling_rounded_total_resets_base_fields(self):
"""Disabling rounded total should also clear base rounded values."""
so = make_sales_order(do_not_save=True)
so.items[0].qty = 1
so.items[0].rate = 1000.25
so.items[0].price_list_rate = 1000.25
so.items[0].discount_percentage = 0
so.items[0].discount_amount = 0
so.set("taxes", [])
so.disable_rounded_total = 0
calculate_taxes_and_totals(so)
self.assertEqual(so.grand_total, 1000.25)
self.assertEqual(so.rounded_total, 1000.0)
self.assertEqual(so.rounding_adjustment, -0.25)
self.assertEqual(so.base_grand_total, 1000.25)
self.assertEqual(so.base_rounded_total, 1000.0)
self.assertEqual(so.base_rounding_adjustment, -0.25)
# User toggles disable_rounded_total after values are already set.
so.disable_rounded_total = 1
calculate_taxes_and_totals(so)
self.assertEqual(so.rounded_total, 0)
self.assertEqual(so.rounding_adjustment, 0)
self.assertEqual(so.base_rounded_total, 0)
self.assertEqual(so.base_rounding_adjustment, 0)

View File

@@ -4,9 +4,7 @@
import frappe
from frappe import _
from frappe.utils import DateTimeLikeObject, getdate, today
from erpnext.accounts.utils import get_fiscal_year
from frappe.utils import getdate
def get_columns(filters, trans):
@@ -47,10 +45,6 @@ def get_columns(filters, trans):
def validate_filters(filters):
if not filters.get("fiscal_year"):
filters["fiscal_year"] = get_fiscal_year(today())[0]
if not filters.get("company"):
filters["company"] = frappe.defaults.get_user_default("Company")
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
if not filters.get(f.lower().replace(" ", "_")):
frappe.throw(_("{0} is mandatory").format(_(f)))

View File

@@ -117,7 +117,7 @@ def get_join(filters):
join = """JOIN `tabOpportunity Lost Reason Detail`
ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and
`tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and
`tabOpportunity Lost Reason Detail`.lost_reason=%(lost_reason)s
"""
`tabOpportunity Lost Reason Detail`.lost_reason = '{}'
""".format(filters.get("lost_reason"))
return join

View File

@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
import frappe
from frappe.model.document import Document
from frappe.utils import escape_html
if TYPE_CHECKING:
from lxml.etree import Element
@@ -64,16 +63,14 @@ class CodeList(Document):
def from_genericode(self, root: "Element"):
"""Extract Code List details from genericode XML"""
self.title = escape_html(root.find(".//Identification/ShortName").text)
self.title = root.find(".//Identification/ShortName").text
self.version = root.find(".//Identification/Version").text
self.canonical_uri = root.find(".//CanonicalUri").text
# optionals
self.description = escape_html(getattr(root.find(".//Identification/LongName"), "text", None))
self.publisher = escape_html(getattr(root.find(".//Identification/Agency/ShortName"), "text", None))
self.description = getattr(root.find(".//Identification/LongName"), "text", None)
self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None)
if not self.publisher:
self.publisher = escape_html(
getattr(root.find(".//Identification/Agency/LongName"), "text", None)
)
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)

View File

@@ -10,7 +10,6 @@ erpnext.edi.import_genericode = function (listview_or_form) {
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
doctype: doctype,
docname: docname,
allow_web_link: false,
allow_toggle_private: false,
allow_take_photo: false,
on_success: function (_file_doc, r) {

View File

@@ -1,118 +1,42 @@
import json
from urllib.parse import urlsplit
import frappe
import requests
from frappe import _
from frappe.utils import escape_html
from frappe.utils.file_manager import save_file
from lxml import etree
GENERICODE_FETCH_TIMEOUT = 15
LOCAL_FILE_PREFIXES = ("/files/", "/private/files/")
class RemoteGenericodeUrlNotAllowedError(Exception):
pass
class CodeListSelectionMismatchError(Exception):
pass
URL_PREFIXES = ("http://", "https://")
@frappe.whitelist()
def import_genericode():
doctype = "Code List"
docname = frappe.form_dict.docname
content = frappe.local.uploaded_file
# recover the content, if it's a link
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
try:
# If it's a URL, fetch the content and make it a local file (for durable audit)
response = requests.get(frappe.local.uploaded_file_url)
response.raise_for_status()
frappe.local.uploaded_file = content = response.content
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
frappe.local.uploaded_file_url = None
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
if file_url := frappe.local.uploaded_file_url:
file_path = frappe.utils.file_manager.get_file_path(file_url)
with open(file_path.encode(), mode="rb") as f:
content = f.read()
# Parse the xml content
parser = etree.XMLParser(remove_blank_text=True)
try:
content, file_name = get_uploaded_genericode_file()
return import_genericode_content(
doctype="Code List",
docname=frappe.form_dict.docname,
content=content,
file_name=file_name,
)
except RemoteGenericodeUrlNotAllowedError:
frappe.throw(
_("Importing Code Lists from remote URLs is not allowed."),
title=_("Invalid Upload"),
)
except CodeListSelectionMismatchError:
frappe.throw(_("The uploaded file does not match the selected Code List."))
except etree.XMLSyntaxError:
frappe.throw(
_("The uploaded file could not be parsed as a genericode XML document."),
title=_("Parsing Error"),
)
def import_genericode_from_url(
url: str,
doctype: str = "Code List",
docname: str | None = None,
):
"""Import a Code List from a trusted backend URL."""
content = fetch_genericode_from_url(url)
file_name = urlsplit(url).path.rsplit("/", 1)[-1] or "genericode.xml"
return import_genericode_content(
doctype=doctype,
docname=docname,
content=content,
file_name=file_name,
)
def get_uploaded_genericode_file() -> tuple[bytes, str | None]:
uploaded_data = frappe.local.uploaded_file
file_name = frappe.local.uploaded_filename
if uploaded_data and file_name:
return uploaded_data, file_name
file_url = frappe.local.uploaded_file_url
if not file_url:
raise frappe.ValidationError(_("No file uploaded or URL provided."))
if not is_local_file_url(file_url):
raise RemoteGenericodeUrlNotAllowedError
file_doc = frappe.get_doc("File", {"file_url": file_url})
file_doc.check_permission("read")
return read_file_bytes(file_doc), file_name
def read_file_bytes(file_doc) -> bytes:
"""Return the raw bytes of a File document.
v15's `File.get_content` eagerly decodes to utf-8 and returns `str` for text
files, but `lxml.etree.fromstring` needs bytes when the XML declares an encoding.
"""
content = file_doc.get_content()
if isinstance(content, str):
content = content.encode("utf-8")
return content
def is_local_file_url(file_url: str | None) -> bool:
if not file_url:
return False
parsed = urlsplit(file_url.strip())
return not parsed.scheme and not parsed.netloc and parsed.path.startswith(LOCAL_FILE_PREFIXES)
def fetch_genericode_from_url(url: str) -> bytes:
response = requests.get(url, timeout=GENERICODE_FETCH_TIMEOUT)
response.raise_for_status()
return response.content
def import_genericode_content(
doctype: str,
docname: str | None,
content: bytes,
file_name: str | None,
):
root = parse_genericode_content(content)
root = etree.fromstring(content, parser=parser)
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
# Extract the name (CanonicalVersionUri) from the parsed XML
name = root.find(".//CanonicalVersionUri").text
@@ -121,7 +45,7 @@ def import_genericode_content(
if frappe.db.exists(doctype, docname):
code_list = frappe.get_doc(doctype, docname)
if code_list.name != name:
raise CodeListSelectionMismatchError
frappe.throw(_("The uploaded file does not match the selected Code List."))
else:
# Create a new Code List document with the extracted name
code_list = frappe.new_doc(doctype)
@@ -130,13 +54,19 @@ def import_genericode_content(
code_list.from_genericode(root)
code_list.save()
file_doc = save_file(
fname=file_name,
content=content,
dt=doctype,
dn=code_list.name,
is_private=1,
)
# Attach the file and provide a recoverable identifier
file_doc = frappe.get_doc(
{
"doctype": "File",
"attached_to_doctype": "Code List",
"attached_to_name": code_list.name,
"folder": frappe.db.get_value("File", {"is_attachments_folder": 1}),
"file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url,
"is_private": 1,
"content": content,
}
).save()
# Get available columns and example values
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
@@ -151,16 +81,6 @@ def import_genericode_content(
}
def parse_genericode_content(content: bytes):
parser = etree.XMLParser(
remove_blank_text=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
)
return etree.fromstring(content, parser=parser)
@frappe.whitelist()
def process_genericode_import(
code_list_name: str,
@@ -184,7 +104,7 @@ def get_genericode_columns_and_examples(root):
# Get column names
for column in root.findall(".//Column"):
column_id = escape_html(column.get("Id"))
column_id = column.get("Id")
columns.append(column_id)
example_values[column_id] = []
filterable_columns[column_id] = set()
@@ -192,7 +112,7 @@ def get_genericode_columns_and_examples(root):
# Get all values and count unique occurrences
for row in root.findall(".//SimpleCodeList/Row"):
for value in row.findall("Value"):
column_id = escape_html(value.get("ColumnRef"))
column_id = value.get("ColumnRef")
if column_id not in columns:
# Handle undeclared column
columns.append(column_id)
@@ -203,7 +123,7 @@ def get_genericode_columns_and_examples(root):
if simple_value is None:
continue
filterable_columns[column_id].add(escape_html(simple_value.text))
filterable_columns[column_id].add(simple_value.text)
# Get example values (up to 3) and filter columns with cardinality <= 5
for row in root.findall(".//SimpleCodeList/Row")[:3]:
@@ -213,7 +133,7 @@ def get_genericode_columns_and_examples(root):
if simple_value is None:
continue
example_values[column_id].append(escape_html(simple_value.text))
example_values[column_id].append(simple_value.text)
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}

View File

@@ -1,200 +0,0 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from unittest.mock import Mock, patch
import frappe
import requests
from frappe.tests.utils import FrappeTestCase
from erpnext.edi.doctype.code_list import code_list_import
SAMPLE_GENERICODE = b"""<?xml version="1.0" encoding="UTF-8"?>
<CodeList>
<Identification>
<ShortName>Test Code List</ShortName>
<Version>1.0</Version>
<CanonicalUri>test-code-list</CanonicalUri>
<LongName>Code list for tests</LongName>
<Agency>
<ShortName>Test Agency</ShortName>
<Identifier>TEST</Identifier>
</Agency>
<LocationUri>https://example.com/codelists/test.xml</LocationUri>
</Identification>
<CanonicalVersionUri>test-code-list-v1</CanonicalVersionUri>
<ColumnSet>
<Column Id="code" />
<Column Id="name" />
<Column Id="category" />
</ColumnSet>
<SimpleCodeList>
<Row>
<Value ColumnRef="code"><SimpleValue>A</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Alpha</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>B</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Beta</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 2</SimpleValue></Value>
</Row>
<Row>
<Value ColumnRef="code"><SimpleValue>C</SimpleValue></Value>
<Value ColumnRef="name"><SimpleValue>Gamma</SimpleValue></Value>
<Value ColumnRef="category"><SimpleValue>Group 1</SimpleValue></Value>
</Row>
</SimpleCodeList>
</CodeList>
"""
class TestCodeListImport(FrappeTestCase):
def test_import_genericode_rejects_remote_file_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="https://example.com/codelists/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_rejects_file_scheme_url(self):
self.set_upload_context(
file_name="trusted.xml",
file_url="file:///tmp/trusted.xml",
)
with patch("erpnext.edi.doctype.code_list.code_list_import.requests.get") as mock_get:
with self.assertRaisesRegex(
frappe.ValidationError, "Importing Code Lists from remote URLs is not allowed."
):
code_list_import.import_genericode()
mock_get.assert_not_called()
def test_import_genericode_from_trusted_url(self):
response = Mock()
response.content = SAMPLE_GENERICODE
response.raise_for_status.return_value = None
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
return_value=response,
) as mock_get:
import_result = code_list_import.import_genericode_from_url(
"https://example.com/codelists/trusted.xml"
)
self.assert_import_response(import_result)
mock_get.assert_called_once_with(
"https://example.com/codelists/trusted.xml",
timeout=code_list_import.GENERICODE_FETCH_TIMEOUT,
)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
self.assertFalse(file_doc.file_url.startswith("https://"))
def test_import_genericode_from_trusted_url_propagates_fetch_errors(self):
with patch(
"erpnext.edi.doctype.code_list.code_list_import.requests.get",
side_effect=requests.Timeout,
):
with self.assertRaises(requests.Timeout):
code_list_import.import_genericode_from_url("https://example.com/codelists/trusted.xml")
def test_import_genericode_from_uploaded_file_returns_metadata(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
file_doc = frappe.get_doc("File", import_result["file"])
self.assertEqual(code_list_import.read_file_bytes(file_doc), SAMPLE_GENERICODE)
def test_process_genericode_import_reads_file_doc_content(self):
self.set_upload_context(content=SAMPLE_GENERICODE, file_name="uploaded_genericode.xml")
import_result = code_list_import.import_genericode()
count = code_list_import.process_genericode_import(
code_list_name=import_result["code_list"],
file_name=import_result["file"],
code_column="code",
title_column="name",
)
self.assertEqual(count, 3)
self.assertEqual(frappe.db.count("Common Code", {"code_list": import_result["code_list"]}), 3)
self.assertEqual(
frappe.db.get_value(
"Common Code",
{"code_list": import_result["code_list"], "common_code": "A"},
"title",
),
"Alpha",
)
def test_import_genericode_from_local_file_url(self):
source_file = frappe.get_doc(
{
"doctype": "File",
"file_name": "library_genericode.xml",
"content": SAMPLE_GENERICODE,
"is_private": 1,
}
).insert()
self.set_upload_context(file_name=source_file.file_name, file_url=source_file.file_url)
import_result = code_list_import.import_genericode()
self.assert_import_response(import_result)
def set_upload_context(
self,
content: bytes | None = None,
file_name: str = "genericode.xml",
file_url: str | None = None,
docname: str | None = None,
):
attrs = ("form_dict", "uploaded_file", "uploaded_file_url", "uploaded_filename")
originals = {attr: getattr(frappe.local, attr, None) for attr in attrs}
frappe.local.form_dict = frappe._dict(doctype="Code List", docname=docname)
frappe.local.uploaded_file = content
frappe.local.uploaded_file_url = file_url
frappe.local.uploaded_filename = file_name
def restore():
for attr, value in originals.items():
setattr(frappe.local, attr, value)
self.addCleanup(restore)
def assert_import_response(self, import_result):
self.assertEqual(
set(import_result),
{
"code_list",
"code_list_title",
"file",
"columns",
"example_values",
"filterable_columns",
},
)
self.assertEqual(import_result["code_list"], "test-code-list-v1")
self.assertEqual(import_result["code_list_title"], "Test Code List")
self.assertEqual(import_result["columns"], ["code", "name", "category"])
self.assertEqual(import_result["example_values"]["code"], ["A", "B", "C"])
self.assertEqual(import_result["example_values"]["name"], ["Alpha", "Beta", "Gamma"])
self.assertEqual(import_result["example_values"]["category"], ["Group 1", "Group 2", "Group 1"])
self.assertCountEqual(import_result["filterable_columns"]["category"], ["Group 1", "Group 2"])
self.assertTrue(frappe.db.exists("Code List", import_result["code_list"]))
self.assertTrue(frappe.db.exists("File", import_result["file"]))

View File

@@ -9,8 +9,6 @@ from frappe.model.document import Document
from frappe.utils.data import get_link_to_form
from lxml import etree
from erpnext.edi.doctype.code_list.code_list_import import parse_genericode_content, read_file_bytes
class CommonCode(Document):
# begin: auto-generated types
@@ -88,15 +86,15 @@ def simple_hash(input_string, length=6):
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
"""Import genericode file and create Common Code entries"""
file_doc = frappe.get_doc("File", file_name)
file_doc.check_permission("read")
root = parse_genericode_content(read_file_bytes(file_doc))
file_path = frappe.utils.file_manager.get_file_path(file_name)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser=parser)
root = tree.getroot()
# Construct the XPath expression
xpath_expr = ".//SimpleCodeList/Row"
filter_conditions = [
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'"
for column_ref, value in (filters or {}).items()
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
]
if filter_conditions:
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
@@ -104,7 +102,7 @@ def import_genericode(code_list: str, file_name: str, column_map: dict, filters:
elements = root.xpath(xpath_expr)
total_elements = len(elements)
for i, xml_element in enumerate(elements, start=1):
common_code: CommonCode = frappe.new_doc("Common Code")
common_code: "CommonCode" = frappe.new_doc("Common Code")
common_code.code_list = code_list
common_code.from_genericode(column_map, xml_element)
common_code.save()

View File

@@ -120,7 +120,7 @@ class BlanketOrder(Document):
def validate_item_qty(self):
for d in self.items:
if flt(d.qty) < 0:
if d.qty < 0:
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))

View File

@@ -760,8 +760,6 @@ frappe.ui.form.on("BOM Item", "sourced_by_supplier", function (frm, cdt, cdn) {
if (d.sourced_by_supplier) {
d.rate = 0;
refresh_field("rate", d.name, d.parentfield);
} else {
get_bom_material_detail(frm.doc, cdt, cdn, false);
}
});

View File

@@ -16,8 +16,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Operation",
"options": "Operation",
"reqd": 1
"options": "Operation"
},
{
"default": "0",
@@ -41,7 +40,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-13 12:17:33.776504",
"modified": "2025-08-04 16:15:11.425349",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Sub Operation",

View File

@@ -16,7 +16,7 @@ class SubOperation(Document):
from frappe.types import DF
description: DF.SmallText | None
operation: DF.Link
operation: DF.Link | None
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -228,18 +228,6 @@ class WorkOrder(Document):
if self.production_plan_sub_assembly_item:
return
production_item = self.production_item
if self.material_request_item and (
mr_plan_item := frappe.get_value(
"Material Request Item", self.material_request_item, "material_request_plan_item"
)
):
if main_item_code := frappe.get_value(
"Material Request Plan Item", mr_plan_item, "main_item_code"
):
production_item = main_item_code
if self.sales_order:
self.check_sales_order_on_hold_or_close()
@@ -260,8 +248,8 @@ class WorkOrder(Document):
& (SalesOrder.docstatus == 1)
& (SalesOrder.name == self.sales_order)
& (
(SalesOrderItem.item_code == production_item)
| (ProductBundleItem.item_code == production_item)
(SalesOrderItem.item_code == self.production_item)
| (ProductBundleItem.item_code == self.production_item)
)
)
.run(as_dict=1)
@@ -280,7 +268,7 @@ class WorkOrder(Document):
& (SalesOrder.skip_delivery_note == 0)
& (SalesOrderItem.item_code == PackedItem.parent_item)
& (SalesOrder.docstatus == 1)
& (PackedItem.item_code == production_item)
& (PackedItem.item_code == self.production_item)
)
.run(as_dict=1)
)

View File

@@ -10,6 +10,6 @@ frappe.listview_settings["Workstation"] = {
Setup: "blue",
};
return [__(doc.status), color_map[doc.status], "status,=," + doc.status];
return [__(doc.status), color_map[doc.status], true];
},
};

View File

@@ -432,4 +432,3 @@ erpnext.patches.v16_0.set_ordered_qty_in_quotation_item
erpnext.patches.v15_0.replace_http_with_https_in_sales_partner
erpnext.patches.v16_0.add_portal_redirects
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
erpnext.patches.v16_0.depends_on_inv_dimensions

View File

@@ -1,89 +0,0 @@
import frappe
def get_inventory_dimensions():
return frappe.get_all(
"Inventory Dimension",
fields=[
"target_fieldname as fieldname",
"source_fieldname",
"reference_document as doctype",
"reqd",
"mandatory_depends_on",
],
order_by="creation",
distinct=True,
)
def get_display_depends_on(doctype, fieldname):
if doctype not in [
"Stock Entry Detail",
"Sales Invoice Item",
"Delivery Note Item",
"Purchase Invoice Item",
"Purchase Receipt Item",
]:
return None, None
fieldname_start_with = "to"
display_depends_on = ""
if doctype in ["Purchase Invoice Item", "Purchase Receipt Item"]:
display_depends_on = "eval:parent.is_internal_supplier == 1"
fieldname_start_with = "from"
elif doctype != "Stock Entry Detail":
display_depends_on = "eval:parent.is_internal_customer == 1"
elif doctype == "Stock Entry Detail":
display_depends_on = "eval:doc.t_warehouse"
return f"{fieldname_start_with}_{fieldname}", display_depends_on
def execute():
for dimension in get_inventory_dimensions():
if frappe.db.exists(
"Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"}
):
frappe.set_value(
"Custom Field",
{"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail"},
"depends_on",
"eval:doc.s_warehouse",
)
if frappe.db.exists(
"Custom Field", {"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1}
):
frappe.set_value(
"Custom Field",
{"fieldname": dimension.source_fieldname, "dt": "Stock Entry Detail", "reqd": 1},
{"mandatory_depends_on": "eval:doc.s_warehouse", "reqd": 0},
)
if frappe.db.exists(
"Custom Field",
{
"fieldname": f"to_{dimension.fieldname}",
"dt": "Stock Entry Detail",
"depends_on": "eval:parent.purpose != 'Material Issue'",
},
):
frappe.set_value(
"Custom Field",
{
"fieldname": f"to_{dimension.fieldname}",
"dt": "Stock Entry Detail",
"depends_on": "eval:parent.purpose != 'Material Issue'",
},
"depends_on",
"eval:doc.t_warehouse",
)
fieldname, display_depends_on = get_display_depends_on(dimension.doctype, dimension.fieldname)
if display_depends_on and frappe.db.exists(
"Custom Field", {"fieldname": fieldname, "dt": dimension.doctype}
):
frappe.set_value(
"Custom Field",
{"fieldname": fieldname, "dt": dimension.doctype},
"mandatory_depends_on",
display_depends_on if dimension.reqd else dimension.mandatory_depends_on,
)

View File

@@ -10,18 +10,7 @@ def execute():
)
if data:
frappe.db.auto_commit_on_many_writes = 1
try:
frappe.db.bulk_update(
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
)
quotations = frappe.get_all(
"Quotation Item",
filters={"name": ["in", [d.quotation_item for d in data]]},
pluck="parent",
distinct=True,
)
for quotation in quotations:
doc = frappe.get_doc("Quotation", quotation)
doc.set_status(update=True, update_modified=False)
finally:
frappe.db.auto_commit_on_many_writes = 0
frappe.db.bulk_update(
"Quotation Item", {d.quotation_item: {"ordered_qty": d.ordered_qty} for d in data}
)
frappe.db.auto_commit_on_many_writes = 0

View File

@@ -364,18 +364,13 @@ class Project(Document):
)
for user in self.users:
# process only users who haven't received the welcome email yet
if user.welcome_email_sent == 0:
# fetch canonical User data (enabled status + latest email)
user_info = frappe.db.get_value("User", user.user, ["enabled", "email"], as_dict=True)
# send email only if user is enabled and has a valid email
if user_info and user_info.enabled and user_info.email:
frappe.sendmail(
recipients=[user_info.email],
subject=_("Project Collaboration Invitation"),
content=content,
)
user.welcome_email_sent = 1
frappe.sendmail(
user.user,
subject=_("Project Collaboration Invitation"),
content=content,
)
user.welcome_email_sent = 1
def get_timeline_data(doctype: str, name: str) -> dict[int, int]:

View File

@@ -25,16 +25,14 @@ erpnext.buying = {
};
});
const get_project_filters = () => ({
query: "erpnext.controllers.queries.get_project_name",
filters: {
company: this.frm.doc.company,
},
this.frm.set_query("project", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
this.frm.set_query("project", get_project_filters);
this.frm.set_query("project", "items", get_project_filters);
if (this.frm.doc.__islocal
&& frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) {
@@ -176,14 +174,9 @@ erpnext.buying = {
callback: (r) => {
if (!r.message) return;
if (!this.frm.doc.billing_address) {
this.frm.set_value("billing_address", r.message.primary_address || "");
}
this.frm.set_value("billing_address", r.message.primary_address || "");
if (
frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") &&
!this.frm.doc.shipping_address
) {
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
this.frm.set_value("shipping_address", r.message.shipping_address || "");
}
},

View File

@@ -674,27 +674,24 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
set_rounded_total() {
var disable_rounded_total = 0;
if (frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total", this.frm.doc.name)) {
if(frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total", this.frm.doc.name)) {
disable_rounded_total = this.frm.doc.disable_rounded_total;
} else if (frappe.sys_defaults.disable_rounded_total) {
disable_rounded_total = frappe.sys_defaults.disable_rounded_total;
}
if (frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
if (cint(disable_rounded_total)) {
this.frm.doc.rounded_total = 0;
this.frm.doc.rounding_adjustment = 0;
} else {
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(
this.frm.doc.grand_total,
this.frm.doc.currency,
precision("rounded_total"),
);
this.frm.doc.rounding_adjustment = flt(
this.frm.doc.rounded_total - this.frm.doc.grand_total,
precision("rounding_adjustment"),
);
}
if (cint(disable_rounded_total)) {
this.frm.doc.rounded_total = 0;
this.frm.doc.base_rounded_total = 0;
this.frm.doc.rounding_adjustment = 0;
return;
}
if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total,
this.frm.doc.currency, precision("rounded_total"));
this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
precision("rounding_adjustment"));
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
}

View File

@@ -1032,7 +1032,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doctype) &&
!this.frm.doc.shipping_address
) {
const is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
let is_drop_ship = me.frm.doc.items.some((item) => item.delivered_by_supplier);
if (!is_drop_ship) {
erpnext.utils.get_shipping_address(this.frm, function() {

View File

@@ -106,19 +106,15 @@ $.extend(erpnext.queries, {
});
}
let filters = { link_doctype: "Company", link_name: doc.company || "" };
const is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
if (is_drop_ship) filters = {};
return {
query: "frappe.contacts.doctype.address.address.address_query",
filters: filters,
filters: { link_doctype: "Company", link_name: doc.company },
};
},
dispatch_address_query: function (doc) {
let filters = { link_doctype: "Company", link_name: doc.company || "" };
const is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
var filters = { link_doctype: "Company", link_name: doc.company || "" };
var is_drop_ship = doc.items.some((item) => item.delivered_by_supplier);
if (is_drop_ship) filters = {};
return {
query: "frappe.contacts.doctype.address.address.address_query",

View File

@@ -38,7 +38,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
{
fieldtype: "Section Break",
label: __("Primary Contact Details"),
collapsible: 0,
collapsible: 1,
},
{
label: __("First Name"),
@@ -69,7 +69,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
{
fieldtype: "Section Break",
label: __("Primary Address Details"),
collapsible: 0,
collapsible: 1,
},
{
label: __("Address Line 1"),

View File

@@ -1,4 +0,0 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -1,4 +0,0 @@
{{ address_line1 }}<br>
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{{ pincode }} {{ city | upper }}<br>
{{ country | upper }}

View File

@@ -1,12 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
# import frappe
from frappe.model.document import Document
from erpnext import get_region
class SouthAfricaVATSettings(Document):
# begin: auto-generated types
@@ -25,9 +22,4 @@ class SouthAfricaVATSettings(Document):
vat_accounts: DF.Table[SouthAfricaVATAccount]
# end: auto-generated types
def validate(self):
self.validate_company_region()
def validate_company_region(self):
if self.company and get_region(self.company) != "South Africa":
frappe.throw(_("Company {0} is not in South Africa.").format(frappe.bold(self.company)))
pass

View File

@@ -10,13 +10,6 @@ frappe.query_reports["VAT Audit Report"] = {
options: "Company",
reqd: 1,
default: frappe.defaults.get_user_default("Company"),
get_query: function () {
return {
filters: {
country: "South Africa",
},
};
},
},
{
fieldname: "from_date",

View File

@@ -6,11 +6,8 @@ import json
import frappe
from frappe import _
from frappe.query_builder.functions import Coalesce, NullIf
from frappe.utils import formatdate, get_link_to_form
from erpnext import get_region
def execute(filters=None):
return VATAuditReport(filters).run()
@@ -24,10 +21,19 @@ class VATAuditReport:
self.doctypes = ["Purchase Invoice", "Sales Invoice"]
def run(self):
self.validate_company_region()
self.get_sa_vat_accounts()
self.get_columns()
for doctype in self.doctypes:
self.select_columns = """
name as voucher_no,
posting_date, remarks"""
columns = (
", supplier as party, credit_to as account"
if doctype == "Purchase Invoice"
else ", customer as party, debit_to as account"
)
self.select_columns += columns
self.get_invoice_data(doctype)
if self.invoices:
@@ -37,14 +43,6 @@ class VATAuditReport:
return self.columns, self.data
def validate_company_region(self):
if self.filters.company and get_region(self.filters.company) != "South Africa":
frappe.throw(
_(
"The company {0} is not in South Africa. VAT Audit Report is only available for companies in South Africa."
).format(frappe.bold(self.filters.company))
)
def get_sa_vat_accounts(self):
self.sa_vat_accounts = frappe.get_all(
"South Africa VAT Account", filters={"parent": self.filters.company}, pluck="account"
@@ -56,59 +54,47 @@ class VATAuditReport:
frappe.throw(_("Please set VAT Accounts in {0}").format(link_to_settings))
def get_invoice_data(self, doctype):
conditions = self.get_conditions()
self.invoices = frappe._dict()
invoice_doctype = frappe.qb.DocType(doctype)
party_field = invoice_doctype.supplier if doctype == "Purchase Invoice" else invoice_doctype.customer
account_field = (
invoice_doctype.credit_to if doctype == "Purchase Invoice" else invoice_doctype.debit_to
invoice_data = frappe.db.sql(
f"""
SELECT
{self.select_columns}
FROM
`tab{doctype}`
WHERE
docstatus = 1 {conditions}
and is_opening = 'No'
ORDER BY
posting_date DESC
""",
self.filters,
as_dict=1,
)
query = (
frappe.qb.from_(invoice_doctype)
.select(
invoice_doctype.name.as_("voucher_no"),
invoice_doctype.posting_date,
invoice_doctype.remarks,
party_field.as_("party"),
account_field.as_("account"),
)
.where(invoice_doctype.docstatus == 1)
.where(invoice_doctype.is_opening == "No")
.orderby(invoice_doctype.posting_date, order=frappe.qb.desc)
)
if self.filters.get("company"):
query = query.where(invoice_doctype.company == self.filters.company)
if self.filters.get("from_date"):
query = query.where(invoice_doctype.posting_date >= self.filters.from_date)
if self.filters.get("to_date"):
query = query.where(invoice_doctype.posting_date <= self.filters.to_date)
invoice_data = query.run(as_dict=True)
for row in invoice_data:
self.invoices.setdefault(row.voucher_no, row)
for d in invoice_data:
self.invoices.setdefault(d.voucher_no, d)
def get_invoice_items(self, doctype):
self.invoice_items = frappe._dict()
item_doctype = frappe.qb.DocType(doctype + " Item")
items = (
frappe.qb.from_(item_doctype)
.select(
Coalesce(NullIf(item_doctype.item_code, ""), item_doctype.item_name).as_("item"),
item_doctype.parent,
item_doctype.base_net_amount,
item_doctype.is_zero_rated,
)
.where(item_doctype.parent.isin(list(self.invoices.keys())))
.run(as_dict=True)
items = frappe.db.sql(
"""
SELECT
item_code, parent, base_net_amount, is_zero_rated
FROM
`tab{} Item`
WHERE
parent in ({})
""".format(doctype, ", ".join(["%s"] * len(self.invoices))),
tuple(self.invoices),
as_dict=1,
)
for row in items:
self.invoice_items.setdefault(row.parent, {}).setdefault(row.item, {"net_amount": 0.0})
self.invoice_items[row.parent][row.item]["net_amount"] += row.get("base_net_amount", 0)
self.invoice_items[row.parent][row.item]["is_zero_rated"] = row.is_zero_rated
for d in items:
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
def get_items_based_on_tax_rate(self, doctype):
self.items_based_on_tax_rate = frappe._dict()
@@ -117,54 +103,52 @@ class VATAuditReport:
"Purchase Taxes and Charges" if doctype == "Purchase Invoice" else "Sales Taxes and Charges"
)
tax_doctype = frappe.qb.DocType(self.tax_doctype)
self.tax_details = (
frappe.qb.from_(tax_doctype)
.select(tax_doctype.parent, tax_doctype.account_head, tax_doctype.item_wise_tax_detail)
.where(tax_doctype.parenttype == doctype)
.where(tax_doctype.docstatus == 1)
.where(tax_doctype.parent.isin(list(self.invoices.keys())))
.where(tax_doctype.account_head.isin(self.sa_vat_accounts))
.orderby(tax_doctype.account_head)
.run(as_dict=True)
self.tax_details = frappe.db.sql(
"""
SELECT
parent, account_head, item_wise_tax_detail
FROM
`tab{}`
WHERE
parenttype = {} and docstatus = 1
and parent in ({})
ORDER BY
account_head
""".format(self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))),
tuple([doctype, *list(self.invoices.keys())]),
)
for tax_detail in self.tax_details:
if not tax_detail.item_wise_tax_detail:
continue
for parent, account, item_wise_tax_detail in self.tax_details:
if item_wise_tax_detail:
try:
if account in self.sa_vat_accounts:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
else:
continue
for item_code, taxes in item_wise_tax_detail.items():
is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated")
# to skip items with non-zero tax rate in multiple rows
if taxes[0] == 0 and not is_zero_rated:
continue
tax_rate = self.get_item_amount_map(parent, item_code, taxes)
try:
item_wise_tax_detail = json.loads(tax_detail.item_wise_tax_detail)
except ValueError:
continue
parent_items = self.invoice_items.get(tax_detail.parent, {})
parent_tax_rates = self.items_based_on_tax_rate.setdefault(tax_detail.parent, {})
for item, taxes in item_wise_tax_detail.items():
is_zero_rated = parent_items.get(item, {}).get("is_zero_rated")
# to skip items with non-zero tax rate in multiple rows
if taxes[0] == 0 and not is_zero_rated:
if tax_rate is not None:
rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault(
tax_rate, []
)
if item_code not in rate_based_dict:
rate_based_dict.append(item_code)
except ValueError:
continue
tax_rate = self.get_item_amount_map(tax_detail.parent, item, taxes)
if tax_rate is not None:
rate_based_dict = parent_tax_rates.setdefault(tax_rate, [])
if item not in rate_based_dict:
rate_based_dict.append(item)
def get_item_amount_map(self, parent, item, taxes):
item_details = self.invoice_items.get(parent, {}).get(item)
if not item_details:
return None
net_amount = item_details.get("net_amount", 0)
def get_item_amount_map(self, parent, item_code, taxes):
net_amount = self.invoice_items.get(parent).get(item_code).get("net_amount")
tax_rate = taxes[0]
tax_amount = taxes[1]
gross_amount = net_amount + tax_amount
self.item_tax_rate.setdefault(parent, {}).setdefault(
item,
item_code,
{
"tax_rate": tax_rate,
"gross_amount": 0.0,
@@ -173,12 +157,24 @@ class VATAuditReport:
},
)
self.item_tax_rate[parent][item]["net_amount"] += net_amount
self.item_tax_rate[parent][item]["tax_amount"] += tax_amount
self.item_tax_rate[parent][item]["gross_amount"] += gross_amount
self.item_tax_rate[parent][item_code]["net_amount"] += net_amount
self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount
self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount
return tax_rate
def get_conditions(self):
conditions = ""
for opts in (
("company", " and company=%(company)s"),
("from_date", " and posting_date>=%(from_date)s"),
("to_date", " and posting_date<=%(to_date)s"),
):
if self.filters.get(opts[0]):
conditions += opts[1]
return conditions
def get_data(self, doctype):
consolidated_data = self.get_consolidated_data(doctype)
section_name = _("Purchases") if doctype == "Purchase Invoice" else _("Sales")

View File

@@ -123,7 +123,6 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
) {
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
this.frm.add_custom_button(__("Update Items"), () => {
erpnext.utils.update_child_items({
frm: this.frm,
@@ -138,6 +137,8 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
this.frm.trigger("set_as_lost_dialog");
});
}
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {

View File

@@ -188,7 +188,7 @@ class Quotation(SellingController):
)
for row in self._items:
if row.name not in ordered_items or row.stock_qty > ordered_items[row.name]:
if row.name not in ordered_items or row.qty > ordered_items[row.name]:
return "Partially Ordered"
return "Ordered"
@@ -409,9 +409,9 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
target.run_method("calculate_taxes_and_totals")
def update_item(obj, target, source_parent):
balance_stock_qty = obj.stock_qty - ordered_items.get(obj.name, 0.0)
target.stock_qty = balance_stock_qty if balance_stock_qty > 0 else 0
target.qty = flt(target.stock_qty) / flt(obj.conversion_factor)
balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.name, 0.0)
target.qty = balance_qty if balance_qty > 0 else 0
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
if obj.against_blanket_order:
target.against_blanket_order = obj.against_blanket_order
@@ -425,7 +425,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False, ar
2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty
3. If no selections: Simple row: Map if adequate qty
"""
if not ((item.stock_qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
if not ((item.qty > ordered_items.get(item.name, 0.0)) or is_unit_price_row(item)):
return False
if not selected_rows:
@@ -560,9 +560,7 @@ def _make_customer(source_name, ignore_permissions=False):
if quotation.quotation_to == "Customer":
return frappe.get_doc("Customer", quotation.party_name)
elif quotation.quotation_to == "CRM Deal":
customer_name = frappe.get_value("Customer", {"crm_deal": quotation.party_name})
if customer_name:
return frappe.get_doc("Customer", customer_name)
return frappe.get_doc("Customer", {"crm_deal": quotation.party_name})
# Check if a Customer already exists for the Lead or Prospect.
existing_customer = None

View File

@@ -175,61 +175,6 @@ class TestQuotation(FrappeTestCase):
self.assertTrue(quotation.payment_schedule)
def test_terms_attachments_are_copied_to_quotation(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
first_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="First terms attachment",
)
quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()
self.assertEqual(get_attachment_urls("Quotation", quotation.name), {first_attachment.file_url})
second_attachment = make_file_attachment(
"Terms and Conditions",
terms.name,
content="Second terms attachment",
)
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()
quotation_attachments = get_attachment_urls("Quotation", quotation.name)
self.assertEqual(quotation_attachments, {first_attachment.file_url})
self.assertNotIn(second_attachment.file_url, quotation_attachments)
new_terms = make_terms_and_conditions(copy_attachments_to_transaction=True)
new_terms_attachment = make_file_attachment(
"Terms and Conditions",
new_terms.name,
content="Attachment from updated terms",
)
quotation.tc_name = new_terms.name
quotation.valid_till = add_days(getdate(quotation.valid_till), 1)
quotation.save()
self.assertEqual(
get_attachment_urls("Quotation", quotation.name),
{first_attachment.file_url, new_terms_attachment.file_url},
)
def test_terms_attachments_are_not_copied_when_disabled(self):
terms = make_terms_and_conditions(copy_attachments_to_transaction=False)
make_file_attachment(
"Terms and Conditions",
terms.name,
content="Terms attachment should stay on the template",
)
quotation = make_quotation(do_not_save=1)
quotation.tc_name = terms.name
quotation.insert()
self.assertFalse(get_attachment_urls("Quotation", quotation.name))
@change_settings(
"Accounts Settings",
{"automatically_fetch_payment_terms": 1},
@@ -1197,42 +1142,6 @@ def get_quotation_dict(party_name=None, item_code=None):
}
def make_terms_and_conditions(copy_attachments_to_transaction=False):
return frappe.get_doc(
{
"doctype": "Terms and Conditions",
"title": f"_Test Terms and Conditions {frappe.generate_hash(length=8)}",
"selling": 1,
"terms": "Test terms",
"copy_attachments_to_transaction": 1 if copy_attachments_to_transaction else 0,
}
).insert()
def make_file_attachment(doctype, docname, content):
return frappe.get_doc(
{
"doctype": "File",
"file_name": f"terms-attachment-{frappe.generate_hash(length=8)}.txt",
"attached_to_doctype": doctype,
"attached_to_name": docname,
"content": content,
}
).insert()
def get_attachment_urls(doctype, docname):
return {
file.file_url
for file in frappe.get_all(
"File",
filters={"attached_to_doctype": doctype, "attached_to_name": docname},
fields=["file_url"],
)
if file.file_url
}
def make_quotation(**args):
qo = frappe.new_doc("Quotation")
args = frappe._dict(args)

View File

@@ -773,13 +773,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
// payment request
if (flt(doc.per_billed) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request_with_schedule(),
__("Create")
);
}
this.frm.add_custom_button(
__("Payment Request"),
() => this.make_payment_request(),
__("Create")
);
if (frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(
@@ -835,24 +833,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
this.order_type(doc);
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
if (doc.delivery_date) {
frappe.model.set_value(cdt, cdn, "delivery_date", doc.delivery_date);
} else {
field_copy.push("delivery_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
create_pick_list() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.create_pick_list",

View File

@@ -533,7 +533,6 @@ class SalesOrder(SellingController):
self.update_reserved_qty()
self.notify_update()
clear_doctype_notifications(self)
self.update_blanket_order()
def update_reserved_qty(self, so_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""

View File

@@ -6,7 +6,6 @@ import frappe
from frappe import _, msgprint
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from frappe.model.document import Document
from frappe.query_builder import functions as fn
from frappe.utils import cstr
@@ -42,117 +41,73 @@ class SMSCenter(Document):
@frappe.whitelist()
def create_receiver_list(self):
query = None
if self.send_to == "":
return
rec, where_clause = "", ""
if self.send_to == "All Customer Contact":
where_clause = " and dl.link_doctype = 'Customer'"
if self.customer:
where_clause += (
" and dl.link_name = '%s'" % self.customer.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to == "All Supplier Contact":
where_clause = " and dl.link_doctype = 'Supplier'"
if self.supplier:
where_clause += (
" and dl.link_name = '%s'" % self.supplier.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to == "All Sales Partner Contact":
where_clause = " and dl.link_doctype = 'Sales Partner'"
if self.sales_partner:
where_clause += (
"and dl.link_name = '%s'" % self.sales_partner.replace("'", "'")
or " and ifnull(dl.link_name, '') != ''"
)
if self.send_to in [
"All Contact",
"All Customer Contact",
"All Supplier Contact",
"All Sales Partner Contact",
]:
query = self.get_contact_query_for_all_contacts()
rec = frappe.db.sql(
"""select CONCAT(ifnull(c.first_name,''), ' ', ifnull(c.last_name,'')),
c.mobile_no from `tabContact` c, `tabDynamic Link` dl where ifnull(c.mobile_no,'')!='' and
c.docstatus != 2 and dl.parent = c.name%s"""
% where_clause
)
elif self.send_to == "All Lead (Open)":
query = self.get_contact_query_for_all_open_leads()
rec = frappe.db.sql(
"""select lead_name, mobile_no from `tabLead` where
ifnull(mobile_no,'')!='' and docstatus != 2 and status='Open'"""
)
elif self.send_to == "All Employee (Active)":
query = self.get_contact_query_for_all_active_employee()
where_clause = (
self.department and " and department = '%s'" % self.department.replace("'", "'") or ""
)
where_clause += self.branch and " and branch = '%s'" % self.branch.replace("'", "'") or ""
rec = frappe.db.sql(
"""select employee_name, cell_number from
`tabEmployee` where status = 'Active' and docstatus < 2 and
ifnull(cell_number,'')!='' %s"""
% where_clause
)
elif self.send_to == "All Sales Person":
query = self.get_contact_query_for_all_sales_person()
rec = query.run(as_list=1)
rec = frappe.db.sql(
"""select sales_person_name,
tabEmployee.cell_number from `tabSales Person` left join tabEmployee
on `tabSales Person`.employee = tabEmployee.name
where ifnull(tabEmployee.cell_number,'')!=''"""
)
rec_list = ""
for d in rec:
rec_list += d[0] + " - " + d[1] + "\n"
self.receiver_list = rec_list
def get_contact_query_for_all_contacts(self):
Contact = frappe.qb.DocType("Contact")
DynamicLink = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(Contact)
.join(DynamicLink)
.on(DynamicLink.parent == Contact.name)
.select(
fn.Concat(fn.IfNull(Contact.first_name, ""), " ", fn.IfNull(Contact.last_name, "")),
Contact.mobile_no,
)
.where((fn.IfNull(Contact.mobile_no, "") != "") & (Contact.docstatus != 2))
)
if self.send_to == "All Customer Contact":
query = query.where(DynamicLink.link_doctype == "Customer")
query = (
query.where(DynamicLink.link_name == self.customer)
if self.customer
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
elif self.send_to == "All Supplier Contact":
query = query.where(DynamicLink.link_doctype == "Supplier")
query = (
query.where(DynamicLink.link_name == self.supplier)
if self.supplier
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
elif self.send_to == "All Sales Partner Contact":
query = query.where(DynamicLink.link_doctype == "Sales Partner")
query = (
query.where(DynamicLink.link_name == self.sales_partner)
if self.sales_partner
else query.where(fn.IfNull(DynamicLink.link_name, "") != "")
)
return query
def get_contact_query_for_all_open_leads(self):
Lead = frappe.qb.DocType("Lead")
query = (
frappe.qb.from_(Lead)
.select(Lead.lead_name, Lead.mobile)
.where((fn.IfNull(Lead.mobile_no, "") != "") & (Lead.docstatus != 2) & (Lead.status == "Open"))
)
return query
def get_contact_query_for_all_active_employee(self):
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(Employee)
.select(Employee.employee_name, Employee.cell_number)
.where(
(Employee.status == "Active")
& (Employee.docstatus != 2)
& (fn.IfNull(Employee.cell_number, "") != "")
)
)
if self.department:
query = query.where(Employee.department == self.department)
if self.branch:
query = query.where(Employee.branch == self.branch)
return query
def get_contact_query_for_all_sales_person(self):
SalesPerson = frappe.qb.DocType("Sales Person")
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(SalesPerson)
.left_join(Employee)
.on(SalesPerson.employee == Employee.name)
.select(SalesPerson.sales_person_name, Employee.cell_number)
.where(fn.IfNull(Employee.cell_number, "") != "")
)
return query
def get_receiver_nos(self):
receiver_nos = []
if self.receiver_list:

View File

@@ -1,176 +1,122 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.query_builder import DocType, Field, Order
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.utils import QueryBuilder
from frappe.utils.data import comma_or
SALES_TRANSACTION_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note", "POS Invoice"]
import frappe
from frappe import _, msgprint
def execute(filters=None):
if not filters:
filters = {}
return SalesPartnerCommissionSummaryReport(filters).run()
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
class SalesPartnerSummaryReport:
"""
Base class to generate Sales Partner Summary related Reports.
"""
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
dt: DocType
date_field: str
date_label: str
columns: list
data: list
query: QueryBuilder
filters: dict
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Total Commission"),
"fieldname": "total_commission",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
]
def __init__(self, filters: dict):
self.filters = filters
self.columns = []
return columns
def run(self):
self.validate_filters()
self.prepare_columns()
self.get_data()
return self.columns, self.data
def validate_filters(self):
if not self.filters.get("doctype"):
frappe.throw(_("Please select the document type first."))
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
if not self.filters.get("company"):
frappe.throw(_("Please select a company."))
if (
self.filters.get("from_date")
and self.filters.get("to_date")
and self.filters.get("from_date") > self.filters.get("to_date")
):
frappe.throw(_("From Date cannot be greater than To Date."))
self._set_date_field_and_label()
def _set_date_field_and_label(self):
self.date_field = (
"transaction_date" if self.filters.get("doctype") == "Sales Order" else "posting_date"
)
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
def prepare_columns(self):
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
Extend this method to add columns on the report. Use `make_column` to add more columns.
"""
raise NotImplementedError
SELECT
name, customer, territory, {} as posting_date, base_net_total as amount,
sales_partner, commission_rate, total_commission, '{}' as currency
FROM
`tab{}`
WHERE
{} and docstatus = 1 and sales_partner is not null
and sales_partner != '' order by name desc, sales_partner
""".format(date_field, company_currency, filters.get("doctype"), conditions),
filters,
as_dict=1,
)
def get_data(self):
self.build_report_query()
self.data = self.query.run(as_dict=1)
def build_report_query(self):
self._build_report_base_query()
self.extend_report_query()
self._apply_common_filters()
self.apply_filters()
def _build_report_base_query(self):
self.dt = DocType(self.filters.get("doctype"))
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
self.query = (
frappe.qb.from_(self.dt)
.select(
self.dt.name,
self.dt.customer,
self.dt.territory,
Field(self.date_field, "posting_date", table=self.dt),
self.dt.sales_partner,
self.dt.commission_rate,
ConstantColumn(company_currency).as_("currency"),
)
.where(
(self.dt.docstatus == 1) & (self.dt.sales_partner.notnull()) & (self.dt.sales_partner != "")
)
.orderby(self.dt.name, order=Order.desc)
.orderby(self.dt.sales_partner)
)
def extend_report_query(self):
"""
Extend this method to select more columns on the query.
"""
pass
def _apply_common_filters(self):
for field in ["company", "customer", "territory", "sales_partner"]:
if self.filters.get(field):
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
if self.filters.get("from_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
)
if self.filters.get("to_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
)
def apply_filters(self):
"""
Extend this method to add more conditions on the query.
"""
pass
def make_column(
self, label: str, fieldname: str, fieldtype: str, width: int = 140, options: str = "", hidden: int = 0
):
self.columns.append(
dict(
label=label,
fieldname=fieldname,
fieldtype=fieldtype,
options=options,
width=width,
hidden=hidden,
)
)
return entries
class SalesPartnerCommissionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
def get_conditions(filters, date_field):
conditions = "1=1"
self.make_column(_("Customer"), "customer", "Link", options="Customer")
for field in ["company", "customer", "territory"]:
if filters.get(field):
conditions += f" and {field} = %({field})s"
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
if filters.get("sales_partner"):
conditions += " and sales_partner = %(sales_partner)s"
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
if filters.get("from_date"):
conditions += f" and {date_field} >= %(from_date)s"
self.make_column(self.date_label, "posting_date", "Date")
if filters.get("to_date"):
conditions += f" and {date_field} <= %(to_date)s"
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
def extend_report_query(self):
self.query = self.query.select(
self.dt.base_net_total.as_("amount"),
self.dt.total_commission,
)
return conditions

View File

@@ -3,14 +3,6 @@
frappe.query_reports["Sales Partner Transaction Summary"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "sales_partner",
label: __("Sales Partner"),
@@ -36,6 +28,14 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "item_group",
label: __("Item Group"),

View File

@@ -3,84 +3,144 @@
import frappe
from frappe import _
from frappe.query_builder import Case
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
SalesPartnerSummaryReport,
)
from frappe import _, msgprint
def execute(filters=None):
if not filters:
filters = {}
return SalesPartnerTransactionSummaryReport(filters=filters).run()
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
class SalesPartnerTransactionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
self.make_column(_("Customer"), "customer", "Link", options="Customer")
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 100,
},
{
"label": _("Item Group"),
"fieldname": "item_group",
"fieldtype": "Link",
"options": "Item Group",
"width": 100,
},
{
"label": _("Brand"),
"fieldname": "brand",
"fieldtype": "Link",
"options": "Brand",
"width": 100,
},
{"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
{"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120},
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"width": 120,
},
]
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
return columns
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
self.make_column(self.date_label, "posting_date", "Date")
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
SELECT
dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency,
dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount,
((dt_item.base_net_amount * dt.commission_rate) / 100) as commission,
dt_item.brand, dt.sales_partner, dt.commission_rate, dt_item.item_group, dt_item.item_code
FROM
`tab{doctype}` dt, `tab{doctype} Item` dt_item
WHERE
{cond} and dt.name = dt_item.parent and dt.docstatus = 1
and dt.sales_partner is not null and dt.sales_partner != ''
order by dt.name desc, dt.sales_partner
""".format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions),
filters,
as_dict=1,
)
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
return entries
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
self.make_column(_("Quantity"), "qty", "Float", 120)
def get_conditions(filters, date_field):
conditions = "1=1"
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
for field in ["company", "customer", "territory", "sales_partner"]:
if filters.get(field):
conditions += f" and dt.{field} = %({field})s"
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
if filters.get("from_date"):
conditions += f" and dt.{date_field} >= %(from_date)s"
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
if filters.get("to_date"):
conditions += f" and dt.{date_field} <= %(to_date)s"
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
if not filters.get("show_return_entries"):
conditions += " and dt_item.qty > 0.0"
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
if filters.get("brand"):
conditions += " and dt_item.brand = %(brand)s"
def extend_report_query(self):
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
if filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"])
self.query = (
self.query.join(self.dt_item)
.on(self.dt.name == self.dt_item.parent)
.select(
self.dt_item.base_net_rate.as_("rate"),
self.dt_item.qty,
self.dt_item.base_net_amount.as_("amount"),
Case()
.when(
self.dt_item.grant_commission.eq(1),
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
)
.else_(0)
.as_("commission"),
self.dt_item.brand,
self.dt_item.item_group,
self.dt_item.item_code,
)
)
conditions += f""" and dt_item.item_group in (select name from
`tabItem Group` where lft >= {lft} and rgt <= {rgt})"""
def apply_filters(self):
if not self.filters.get("show_return_entries"):
self.query = self.query.where(self.dt_item.qty > 0.0)
if self.filters.get("brand"):
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
if self.filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
if item_groups := frappe.get_all(
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
):
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))
return conditions

View File

@@ -11,8 +11,6 @@
"field_order": [
"title",
"disabled",
"column_break_ofhb",
"copy_attachments_to_transaction",
"applicable_modules_section",
"selling",
"buying",
@@ -74,22 +72,12 @@
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_ofhb",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "copy_attachments_to_transaction",
"fieldtype": "Check",
"label": "Copy Attachments to Transaction"
}
],
"icon": "icon-legal",
"idx": 1,
"links": [],
"modified": "2026-04-29 22:51:49.285298",
"modified": "2024-01-30 12:47:52.325531",
"modified_by": "Administrator",
"module": "Setup",
"name": "Terms and Conditions",

View File

@@ -21,7 +21,6 @@ class TermsandConditions(Document):
from frappe.types import DF
buying: DF.Check
copy_attachments_to_transaction: DF.Check
disabled: DF.Check
selling: DF.Check
terms: DF.TextEditor | None

View File

@@ -372,15 +372,6 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
});
}
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["project"]);
}
}
make_sales_invoice() {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",

View File

@@ -759,6 +759,7 @@
"label": "Incoming Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"read_only": 1
},
@@ -951,7 +952,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-07 15:44:20.892151",
"modified": "2025-05-31 18:51:32.651562",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -8,8 +8,9 @@
"field_order": [
"dimension_details_tab",
"dimension_name",
"column_break_4",
"reference_document",
"column_break_4",
"disabled",
"field_mapping_section",
"source_fieldname",
"column_break_9",
@@ -92,6 +93,12 @@
"fieldtype": "Check",
"label": "Apply to All Inventory Documents"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "target_fieldname",
"fieldtype": "Data",
@@ -152,7 +159,6 @@
"label": "Conditional Rule Examples"
},
{
"depends_on": "eval:!doc.apply_to_all_doctypes",
"description": "To apply condition on parent field use parent.field_name and to apply condition on child table use doc.field_name. Here field_name could be based on the actual column name of the respective field.",
"fieldname": "mandatory_depends_on",
"fieldtype": "Small Text",
@@ -182,7 +188,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-08 10:10:16.884388",
"modified": "2025-07-07 15:51:29.329064",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",

View File

@@ -31,6 +31,7 @@ class InventoryDimension(Document):
apply_to_all_doctypes: DF.Check
condition: DF.Code | None
dimension_name: DF.Data
disabled: DF.Check
document_type: DF.Link | None
fetch_from_parent: DF.Literal[None]
istable: DF.Check
@@ -74,6 +75,7 @@ class InventoryDimension(Document):
old_doc = self._doc_before_save
allow_to_edit_fields = [
"disabled",
"fetch_from_parent",
"type_of_transaction",
"condition",
@@ -117,7 +119,6 @@ class InventoryDimension(Document):
def reset_value(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = ""
self.mandatory_depends_on = ""
self.istable = 0
for field in ["document_type", "condition"]:
@@ -182,12 +183,8 @@ class InventoryDimension(Document):
label=_(label),
depends_on="eval:doc.s_warehouse" if doctype == "Stock Entry Detail" else "",
search_index=1,
reqd=1
if self.reqd and not self.mandatory_depends_on and doctype != "Stock Entry Detail"
else 0,
mandatory_depends_on="eval:doc.s_warehouse"
if self.reqd and doctype == "Stock Entry Detail"
else self.mandatory_depends_on,
reqd=self.reqd,
mandatory_depends_on=self.mandatory_depends_on,
),
]
@@ -299,13 +296,12 @@ class InventoryDimension(Document):
options=self.reference_document,
label=label,
depends_on=display_depends_on,
mandatory_depends_on=display_depends_on if self.reqd else self.mandatory_depends_on,
),
]
)
def field_exists(doctype, fieldname) -> str | None:
def field_exists(doctype, fieldname) -> str or None:
return frappe.db.get_value("DocField", {"parent": doctype, "fieldname": fieldname}, "name")
@@ -378,6 +374,7 @@ def get_document_wise_inventory_dimensions(doctype) -> dict:
"type_of_transaction",
"fetch_from_parent",
],
filters={"disabled": 0},
or_filters={"document_type": doctype, "apply_to_all_doctypes": 1},
)
@@ -400,6 +397,7 @@ def get_inventory_dimensions():
"reference_document as doctype",
"validate_negative_stock",
],
filters={"disabled": 0},
)
frappe.local.inventory_dimensions = dimensions

View File

@@ -220,9 +220,9 @@ class TestInventoryDimension(FrappeTestCase):
doc = create_inventory_dimension(
reference_document="Pallet",
type_of_transaction="Outward",
dimension_name="Pallet 75",
dimension_name="Pallet",
apply_to_all_doctypes=0,
document_type="Delivery Note Item",
document_type="Stock Entry Detail",
)
doc.reqd = 1
@@ -230,7 +230,7 @@ class TestInventoryDimension(FrappeTestCase):
self.assertTrue(
frappe.db.get_value(
"Custom Field", {"fieldname": "pallet_75", "dt": "Delivery Note Item", "reqd": 1}, "name"
"Custom Field", {"fieldname": "pallet", "dt": "Stock Entry Detail", "reqd": 1}, "name"
)
)

View File

@@ -791,20 +791,6 @@ class Item(Document):
{"company": defaults.get("company"), "default_warehouse": defaults.default_warehouse},
)
item_group = frappe.get_cached_doc("Item Group", self.item_group)
if not self.taxes and item_group.taxes:
for tax in item_group.taxes:
self.append(
"taxes",
{
"item_tax_template": tax.item_tax_template,
"tax_category": tax.tax_category,
"valid_from": tax.valid_from,
"minimum_net_rate": tax.minimum_net_rate,
"maximum_net_rate": tax.maximum_net_rate,
},
)
def update_variants(self):
if self.flags.dont_update_variants or frappe.db.get_single_value(
"Item Variant Settings", "do_not_update_variants"

View File

@@ -368,13 +368,11 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
items_add(doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"cost_center",
"project",
]);
}
};

View File

@@ -510,14 +510,7 @@ class PurchaseReceipt(BuyingController):
else flt(item.net_amount, item.precision("net_amount"))
)
outgoing_amount = (
flt((item.base_net_amount / item.received_qty) * item.qty, item.precision("base_net_amount"))
if item.received_qty
and frappe.get_single_value(
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
)
else item.base_net_amount
)
outgoing_amount = item.qty * item.base_net_rate
if self.is_internal_transfer() and item.valuation_rate:
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount
@@ -673,9 +666,6 @@ class PurchaseReceipt(BuyingController):
or stock_asset_rbnb
)
if self.is_return and item.expense_account:
loss_account = item.expense_account
cost_center = item.cost_center or frappe.get_cached_value(
"Company", self.company, "cost_center"
)

View File

@@ -734,6 +734,7 @@
"oldfieldname": "valuation_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1,
"print_width": "80px",
"read_only": 1,
@@ -1042,7 +1043,7 @@
"search_index": 1
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_for_rejected_qty",
"fieldtype": "Button",
"label": "Add Serial / Batch No (Rejected Qty)"
@@ -1057,7 +1058,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.use_serial_batch_fields === 0 && parent.docstatus == 0",
"depends_on": "eval:doc.use_serial_batch_fields === 0",
"fieldname": "add_serial_batch_bundle",
"fieldtype": "Button",
"label": "Add Serial / Batch No"
@@ -1148,7 +1149,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-04-29 16:01:34.154697",
"modified": "2025-10-14 12:59:20.384056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -58,7 +58,6 @@ frappe.ui.form.on("Quality Inspection", {
if (doc.reference_type && doc.reference_name) {
let filters = {
from: doctype,
parent_doctype: doc.reference_type,
inspection_type: doc.inspection_type,
};

View File

@@ -364,11 +364,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
from frappe.desk.reportview import get_match_cond
from_doctype = cstr(filters.get("from"))
parent_doctype = cstr(filters.get("parent_doctype"))
if not from_doctype or not frappe.db.exists("DocType", from_doctype):
return []
mcond = get_match_cond(parent_doctype or from_doctype)
mcond = get_match_cond(from_doctype)
cond, qi_condition = "", "and (quality_inspection is null or quality_inspection = '')"
if filters.get("parent"):
@@ -392,10 +391,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
f"""
SELECT distinct `tab{from_doctype}`.item_code, `tab{from_doctype}`.item_name
SELECT distinct item_code, item_name
FROM `tab{from_doctype}`
JOIN `tab{parent_doctype}` ON `tab{parent_doctype}`.name = `tab{from_doctype}`.parent
WHERE `tab{from_doctype}`.parent=%(parent)s and `tab{parent_doctype}`.docstatus < 2 and `tab{from_doctype}`.item_code like %(txt)s
WHERE parent=%(parent)s and docstatus < 2 and item_code like %(txt)s
{qi_condition} {cond} {mcond}
ORDER BY item_code limit {cint(page_len)} offset {cint(start)}
""",

View File

@@ -356,15 +356,8 @@ def repost(doc):
message = message.get("message")
status = "Failed"
# If failed because of a recoverable error (timeout, deadlock), set status to In Progress
# so the scheduler automatically retries instead of leaving it permanently failed.
# NOTE: isinstance check comes first because the traceback string matching is unreliable
# when SIGALRM kills the process mid-C-extension (JobTimeoutException may not appear
# in the traceback if the exception handler itself was interrupted).
traceback_lower = traceback.lower() if traceback else ""
if isinstance(e, RecoverableErrors) or (
traceback_lower and ("timeout" in traceback_lower or "deadlock found" in traceback_lower)
):
# If failed because of timeout, set status to In Progress
if traceback and ("timeout" in traceback.lower() or "Deadlock found" in traceback):
status = "In Progress"
if traceback:

View File

@@ -925,7 +925,6 @@ class SerialandBatchBundle(Document):
parent.voucher_type,
parent.voucher_no,
)
.distinct()
.where(
(child.parent != self.name)
& (parent.item_code == self.item_code)

View File

@@ -346,9 +346,6 @@ class StockEntry(StockController):
def _set_serial_batch_for_disassembly_from_available_materials(self):
available_materials = get_available_materials(self.work_order, self)
for row in self.items:
if row.serial_no or row.batch_no or row.serial_and_batch_bundle:
continue
warehouse = row.s_warehouse or row.t_warehouse
materials = available_materials.get((row.item_code, warehouse))
if not materials:

View File

@@ -270,7 +270,8 @@
"oldfieldname": "transfer_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"reqd": 1
},
{
"default": "0",
@@ -616,7 +617,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-04-27 11:40:38.294196",
"modified": "2026-03-02 14:05:23.116017",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -1793,47 +1793,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
elif s.id_plant == plant_b.name:
self.assertEqual(s.actual_qty, 3)
def test_serial_no_status_with_backdated_stock_reco(self):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
item_code = self.make_item(
"Test Item",
{
"is_stock_item": 1,
"has_serial_no": 1,
"serial_no_series": "SERIAL.###",
},
).name
warehouse = "_Test Warehouse - _TC"
reco = create_stock_reconciliation(
item_code=item_code,
posting_date=add_days(nowdate(), -2),
warehouse=warehouse,
qty=1,
rate=80,
purpose="Opening Stock",
)
serial_no = get_serial_nos_from_bundle(reco.items[0].serial_and_batch_bundle)[0]
create_delivery_note(
item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate()
)
self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered")
reco = create_stock_reconciliation(
item_code=item_code,
posting_date=add_days(nowdate(), -1),
warehouse=warehouse,
qty=1,
rate=90,
)
self.assertEqual(frappe.get_value("Serial No", serial_no, "status"), "Delivered")
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)

View File

@@ -101,23 +101,49 @@ class Warehouse(NestedSet):
def warn_about_multiple_warehouse_account(self):
"If Warehouse value is split across multiple accounts, warn."
if not frappe.db.count("Stock Ledger Entry", {"warehouse": self.name}):
def get_accounts_where_value_is_booked(name):
sle = frappe.qb.DocType("Stock Ledger Entry")
gle = frappe.qb.DocType("GL Entry")
ac = frappe.qb.DocType("Account")
return (
frappe.qb.from_(sle)
.join(gle)
.on(sle.voucher_no == gle.voucher_no)
.join(ac)
.on(ac.name == gle.account)
.select(gle.account)
.distinct()
.where((sle.warehouse == name) & (ac.account_type == "Stock"))
.orderby(sle.creation)
.run(as_dict=True)
)
if self.is_new():
return
doc_before_save = self.get_doc_before_save()
old_wh_account = doc_before_save.account if doc_before_save else None
old_wh_account = frappe.db.get_value("Warehouse", self.name, "account")
if self.is_new() or (self.account and old_wh_account == self.account):
return
# WH account is being changed or set get all accounts against which wh value is booked
if self.account != old_wh_account:
accounts = get_accounts_where_value_is_booked(self.name)
accounts = [d.account for d in accounts]
frappe.msgprint(
title=_("Warning: Account changed for warehouse"),
indicator="orange",
msg=_(
"Stock entries exist with the old account. Changing the account may lead to a mismatch between the warehouse closing balance and the account closing balance. The overall closing balance will still match, but not for the specific account."
),
alert=True,
)
if not accounts or (len(accounts) == 1 and self.account in accounts):
# if same singular account has stock value booked ignore
return
warning = _("Warehouse's Stock Value has already been booked in the following accounts:")
account_str = "<br>" + ", ".join(frappe.bold(ac) for ac in accounts)
reason = "<br><br>" + _(
"Booking stock value across multiple accounts will make it harder to track stock and account value."
)
frappe.msgprint(
warning + account_str + reason,
title=_("Multiple Warehouse Accounts"),
indicator="orange",
)
def check_if_sle_exists(self):
return frappe.db.exists("Stock Ledger Entry", {"warehouse": self.name})

View File

@@ -7,7 +7,6 @@ import json
import frappe
from frappe import _, throw
from frappe.model import child_table_fields, default_fields
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import IfNull, Sum
@@ -673,9 +672,7 @@ def get_item_tax_info(company, tax_category, item_codes, item_rates=None, item_t
@frappe.whitelist()
def get_item_tax_template(
args: str | dict, item: Document | None = None, out: dict | None = None
) -> str | None:
def get_item_tax_template(args, item=None, out=None):
if isinstance(args, str):
args = json.loads(args)
@@ -690,7 +687,11 @@ def get_item_tax_template(
item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template:
item_tax_template = _get_item_tax_template_from_item_group(args, item.item_group, out)
item_group = item.item_group
while item_group and not item_tax_template:
item_group_doc = frappe.get_cached_doc("Item Group", item_group)
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group
if out and args.get("child_doctype") and item_tax_template:
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
@@ -698,26 +699,7 @@ def get_item_tax_template(
return item_tax_template
def _get_item_tax_template_from_item_group(
args: dict, item_group: str, out: dict | None = None
) -> str | None:
from frappe.utils.nestedset import get_ancestors_of
ancestors = get_ancestors_of("Item Group", item_group)
for group in [item_group, *ancestors]:
group_doc = frappe.get_cached_doc("Item Group", group)
item_tax_template = _get_item_tax_template(args, group_doc.taxes, out)
if item_tax_template:
return item_tax_template
return None
def _get_item_tax_template(
args: dict,
taxes,
out: dict | None = None,
for_validate: bool = False,
) -> str | list[str] | None:
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if out is None:
out = {}
taxes_with_validity = []
@@ -1049,7 +1031,6 @@ def insert_item_price(args):
currency=args.currency,
uom=args.stock_uom,
price_list=args.price_list,
valid_from=transaction_date,
)
item_price.insert()
frappe.msgprint(
@@ -1074,7 +1055,6 @@ def insert_item_price(args):
"currency": args.currency,
"price_list_rate": price_list_rate,
"uom": args.stock_uom,
"valid_from": transaction_date,
}
)
item_price.insert()

View File

@@ -219,7 +219,7 @@ def get_item_warehouse_batch_map(filters, float_precision):
)
qty_dict.bal_qty = flt(qty_dict.bal_qty, float_precision) + flt(d.actual_qty, float_precision)
qty_dict.bal_value += flt(d.stock_value_difference)
qty_dict.bal_value += flt(d.stock_value_difference, float_precision)
return iwb_map

View File

@@ -90,62 +90,45 @@ def get_data(filters) -> list[dict]:
batch_negative_data = []
flt_precision = frappe.db.get_default("float_precision") or 2
distinct_batches = set()
for company in companies:
warehouses = get_warehouses(filters, company)
for warehouse in warehouses:
for batch in batches:
_c, data = stock_ledger_execute(
frappe._dict(
for batch in batches:
_c, data = stock_ledger_execute(
frappe._dict(
{
"company": company,
"batch_no": batch,
"from_date": add_to_date(today(), years=-12),
"to_date": today(),
"segregate_serial_batch_bundle": 1,
"warehouse": filters.get("warehouse"),
"valuation_field_type": "Currency",
}
)
)
previous_qty = 0
for row in data:
if flt(row.get("qty_after_transaction"), flt_precision) < 0:
batch_negative_data.append(
{
"company": company,
"batch_no": batch,
"from_date": add_to_date(today(), years=-12),
"to_date": today(),
"segregate_serial_batch_bundle": 1,
"warehouse": warehouse,
"valuation_field_type": "Currency",
"posting_date": row.get("date"),
"batch_no": row.get("batch_no"),
"item_code": row.get("item_code"),
"item_name": row.get("item_name"),
"warehouse": row.get("warehouse"),
"actual_qty": row.get("actual_qty"),
"qty_after_transaction": row.get("qty_after_transaction"),
"previous_qty": previous_qty,
"voucher_type": row.get("voucher_type"),
"voucher_no": row.get("voucher_no"),
}
)
)
previous_qty = 0
for row in data:
key = (row.get("warehouse"), batch)
if key in distinct_batches:
continue
if flt(row.get("qty_after_transaction"), flt_precision) < 0:
batch_negative_data.append(
{
"posting_date": row.get("date"),
"batch_no": row.get("batch_no"),
"item_code": row.get("item_code"),
"item_name": row.get("item_name"),
"warehouse": row.get("warehouse"),
"actual_qty": row.get("actual_qty"),
"qty_after_transaction": row.get("qty_after_transaction"),
"previous_qty": previous_qty,
"voucher_type": row.get("voucher_type"),
"voucher_no": row.get("voucher_no"),
}
)
distinct_batches.add(key)
previous_qty = row.get("qty_after_transaction")
previous_qty = row.get("qty_after_transaction")
return batch_negative_data
def get_warehouses(filters, company):
warehouse_filters = {"company": company, "disabled": 0}
if filters.get("warehouse"):
warehouse_filters["name"] = filters["warehouse"]
return frappe.get_all("Warehouse", pluck="name", filters=warehouse_filters)
def get_batches(filters):
batch_filters = {}
if filters.get("item_code"):

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