Compare commits

..

95 Commits

Author SHA1 Message Date
Frappe PR Bot
80f2f5cd2f chore(release): Bumped to Version 14.74.8
## [14.74.8](https://github.com/frappe/erpnext/compare/v14.74.7...v14.74.8) (2024-10-30)

### Bug Fixes

* add company filter for project ([d3e29fe](d3e29fe828))
* add parenttype clause to invoice tax query in sales_register report ([cbe704c](cbe704c73c))
* added validation for UOM must be whole number (backport [#43710](https://github.com/frappe/erpnext/issues/43710)) ([#43711](https://github.com/frappe/erpnext/issues/43711)) ([aaae81f](aaae81f7e8))
* basic rate not editable in Stock Entry Detail (backport [#43837](https://github.com/frappe/erpnext/issues/43837)) ([#43839](https://github.com/frappe/erpnext/issues/43839)) ([51c1d03](51c1d032ba))
* calculate tds with net amount when invoice exceeds single threshold amount (backport [#43869](https://github.com/frappe/erpnext/issues/43869)) ([#43919](https://github.com/frappe/erpnext/issues/43919)) ([55c5f30](55c5f30d54))
* do not check for payment terms details for return invoices. ([fd63ace](fd63ace6c7))
* do not set payment terms for return invoices ([bd34a67](bd34a67871))
* get stock accounts from the doc instead of db in validate_stock_accounts ([ad04610](ad04610c94))
* hide payment terms for return and paid purchase invoices ([78db8c0](78db8c0029))
* not able to submit stock entry ([#43890](https://github.com/frappe/erpnext/issues/43890)) ([0742e6e](0742e6e193))
* purchase return validation issue ([#43871](https://github.com/frappe/erpnext/issues/43871)) ([a7cc7b2](a7cc7b28c0))
* remarks field in payment reconciliation ([93243b1](93243b1d95))
* **RFQ:** make strings translatable (backport [#43843](https://github.com/frappe/erpnext/issues/43843)) ([#43847](https://github.com/frappe/erpnext/issues/43847)) ([895f1bf](895f1bff01))
* set bill_no before `against_voucher` gets concatenated ([2f3c3d5](2f3c3d5519))
* work order finish button not showing (backport [#43875](https://github.com/frappe/erpnext/issues/43875)) ([#43877](https://github.com/frappe/erpnext/issues/43877)) ([76530de](76530de786))
2024-10-30 09:52:54 +00:00
ruthra kumar
4e85a79e42 Merge pull request #43892 from frappe/version-14-hotfix
chore: release v14
2024-10-30 15:21:35 +05:30
Frappe PR Bot
8c7ed37afd chore(release): Bumped to Version 14.74.7
## [14.74.7](https://github.com/frappe/erpnext/compare/v14.74.6...v14.74.7) (2024-10-23)

### Bug Fixes

* "show_remarks" checkbox in Process statement of accounts ([0833823](083382399f))
* coupon code validation logic ([c18beae](c18beaea6b))
* **deferred_revenue:** Escape account in query ([3d082c9](3d082c93bc))
* Freeze Screen on load invoices on POS Closing Entry ([725b6be](725b6beff3))
* get period estimate till service end date ([35c257a](35c257a7ae))
* incorrect amount in bank clearance ([98362d9](98362d9183))
* party_balance based on company in payment entry ([5333f33](5333f33c21))
* remove extra space ([719565c](719565c897))
* use correct variable in error message (backport [#43790](https://github.com/frappe/erpnext/issues/43790)) ([#43791](https://github.com/frappe/erpnext/issues/43791)) ([5b0a8fd](5b0a8fd2ce))
2024-10-23 04:45:56 +00:00
ruthra kumar
9da08df3ca Merge pull request #43773 from frappe/version-14-hotfix
chore: release v14
2024-10-23 10:14:40 +05:30
Frappe PR Bot
e401ad5b8f chore(release): Bumped to Version 14.74.6
## [14.74.6](https://github.com/frappe/erpnext/compare/v14.74.5...v14.74.6) (2024-10-16)

### Bug Fixes

* added parentheses for correct query formation for logical OR condition ([0f738de](0f738de5e5))
* added string for translation in bank reconciliation statement ([32f6eb7](32f6eb7ee7))
* conversion factor issue (backport [#43645](https://github.com/frappe/erpnext/issues/43645)) ([#43675](https://github.com/frappe/erpnext/issues/43675)) ([1262c32](1262c322d1))
* delete invalid pricing rule on change of applicable_for ([c7c2bdd](c7c2bdd159))
* ignore free item when qty is zero ([ae47fb9](ae47fb9617))
* missing child company accounts in consolidated balance sheet ([bb774ce](bb774cec58))
* removed unused query ([dcbfd4a](dcbfd4ae1d))
* run gl_entries and closing voucher processes in same function ([59c314c](59c314c98a))
* update formatings ([a8ad608](a8ad608fd8))
* update formatings ([699b098](699b09846a))
* update item details with actual quantity. ([c520a7c](c520a7c6c1))
* Use `ref_doc.get()` for `party_account_currency` ([01d6c10](01d6c1081b))
* validation for corrective job card (backport [#43555](https://github.com/frappe/erpnext/issues/43555)) (backport [#43558](https://github.com/frappe/erpnext/issues/43558)) ([#43628](https://github.com/frappe/erpnext/issues/43628)) ([1f09975](1f099752d2))
* zero incoming rate for delivery note return ([#43642](https://github.com/frappe/erpnext/issues/43642)) ([7eac9cc](7eac9cc1db))
2024-10-16 05:02:55 +00:00
ruthra kumar
9106c2fdd8 Merge pull request #43668 from frappe/version-14-hotfix
chore: release v14
2024-10-16 10:31:35 +05:30
Frappe PR Bot
7404911940 chore(release): Bumped to Version 14.74.5
## [14.74.5](https://github.com/frappe/erpnext/compare/v14.74.4...v14.74.5) (2024-10-12)

### Bug Fixes

* validation for corrective job card (backport [#43555](https://github.com/frappe/erpnext/issues/43555)) (backport [#43558](https://github.com/frappe/erpnext/issues/43558)) ([#43628](https://github.com/frappe/erpnext/issues/43628)) ([ea0f044](ea0f044c48))
2024-10-12 15:51:49 +00:00
rohitwaghchaure
136adcb793 Merge pull request #43629 from frappe/mergify/bp/version-14/pr-43628
fix: validation for corrective job card (backport #43555) (backport #43558) (backport #43628)
2024-10-12 21:20:29 +05:30
mergify[bot]
ea0f044c48 fix: validation for corrective job card (backport #43555) (backport #43558) (#43628)
fix: validation for corrective job card (backport #43555) (#43558)

* fix: validation for corrective job card (#43555)

(cherry picked from commit 7a0a893d08)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.py

* chore: fix conflicts

* chore: fix linters issue

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit cf0fa0db7b)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 1f099752d2)
2024-10-12 13:04:35 +00:00
Frappe PR Bot
e9dbbbe324 chore(release): Bumped to Version 14.74.4
## [14.74.4](https://github.com/frappe/erpnext/compare/v14.74.3...v14.74.4) (2024-10-10)

### Bug Fixes

* add parenttype condition for item table in Purchase Register Report ([4586f03](4586f03bdc))
* Add removed test code `b41f10c1b9` ([8f5c191](8f5c191136))
* create Account Closing Balance even though there are no transaction in period ([2fc776d](2fc776d0f5))
* deduct advances adjusted for threshold check for tcs ([de15728](de15728741))
* do not include advances for tds vouchers ([9d950a9](9d950a9b77))
* get company from ref doc as it's not available in payment request ([078eb46](078eb46eb8))
* multiple issues in Payment Request ([#42427](https://github.com/frappe/erpnext/issues/42427)) ([ef52be2](ef52be2f17))
* Remove `advance_payment_status` uses ([36b4f68](36b4f68566))
* Remove unreference method ([d4421da](d4421dade1))
* Remove unused  field ([3a51cf4](3a51cf4e1a))
* Remove unused function `get_paid_amount_against_order` ([677aadd](677aadde7c))
* Separate `on_submit` and `before_submit` of PR ([d828ea6](d828ea6a1a))
2024-10-10 00:48:02 +00:00
ruthra kumar
4b1e3c96c2 Merge pull request #43562 from frappe/version-14-hotfix
chore: release v14
2024-10-10 06:16:42 +05:30
Frappe PR Bot
96cb08dba8 chore(release): Bumped to Version 14.74.3
## [14.74.3](https://github.com/frappe/erpnext/compare/v14.74.2...v14.74.3) (2024-10-04)

### Bug Fixes

* add company filter in Warehouse wise Item Balance Age and Value ([3cb8502](3cb85028d0))
* adjustmen entry for stock reco ([57925a1](57925a1315))
* conflicts ([02ce528](02ce528501))
* **Dunning:** logic for fetching text (backport [#43160](https://github.com/frappe/erpnext/issues/43160)) ([#43489](https://github.com/frappe/erpnext/issues/43489)) ([31ecdb2](31ecdb2104))
* Fix API endpoint for Frankfurter ([1c0843c](1c0843caee))
* patch to update Currency Exchange Settings for `frankfurter.app` ([b4706db](b4706db816))
* set margin fields for purchase documents when updating items ([97edadf](97edadfc4b))
* Stock Ledger Invariant Check report ([4426384](44263842f5))
2024-10-04 03:04:09 +00:00
ruthra kumar
b18ad94071 Merge pull request #43468 from frappe/version-14-hotfix
chore: release v14
2024-10-04 08:32:45 +05:30
Frappe PR Bot
20f9549247 chore(release): Bumped to Version 14.74.2
## [14.74.2](https://github.com/frappe/erpnext/compare/v14.74.1...v14.74.2) (2024-09-25)

### Bug Fixes

* add currency in financial statement ([6becaba](6becaba672))
* AR / AP report to ignore 0.0 outstanding ([79f95ba](79f95ba507))
* **Bank Account:** dashboard connections (backport [#43365](https://github.com/frappe/erpnext/issues/43365)) ([#43366](https://github.com/frappe/erpnext/issues/43366)) ([4146bdf](4146bdf324))
* change dynamic link doctype fieldtype to data ([c0dfda6](c0dfda6ccc))
* incorrect outstanding on non-pos invoice with write_off_account ([79235cd](79235cd688))
* incorrect stock balance for inventory dimension (backport [#43284](https://github.com/frappe/erpnext/issues/43284)) ([#43289](https://github.com/frappe/erpnext/issues/43289)) ([1bdbcc6](1bdbcc6703))
* partial return on POS invoice ([50e0f56](50e0f56d18))
* partial return on POS invoice ([ef7d4fe](ef7d4fe84a))
* Payment Ledger Report currency fieldtype fix ([7ac39dc](7ac39dc233))
* **Payment Reconciliation:** German translations ([32d4f96](32d4f96e02))
* Serial No reserved against historical sales order - unable to sell again ([#43294](https://github.com/frappe/erpnext/issues/43294)) ([8abc1f4](8abc1f4da9))
* show chart tool tip in report currency ([92438f8](92438f8dba))
* translate in js ([e43c357](e43c357c08))
* Translation for button SO to PO ([a8b5bcf](a8b5bcfe02))
* Translation for button SO to PO --conflicts ([b6ce922](b6ce9222aa))
2024-09-25 04:38:46 +00:00
ruthra kumar
644ada8225 Merge pull request #43362 from frappe/version-14-hotfix
chore: release v14
2024-09-25 10:07:32 +05:30
Frappe PR Bot
481610d400 chore(release): Bumped to Version 14.74.1
## [14.74.1](https://github.com/frappe/erpnext/compare/v14.74.0...v14.74.1) (2024-09-18)

### Bug Fixes

* add currency in options for rate field in pricing rule ([c9467f4](c9467f4c1f))
* cancel cost center allocation and journal entry after test ([4646f11](4646f11829))
* delete exchange gain loss journal entry while deleting payment entry ([62f1286](62f12861f1))
* fetch cost center allocation percentage only from the applicable allocation ([a26f0e9](a26f0e9973))
* hide and reset discount control on new POS order ([5434432](5434432cfc))
* invalid gp calculation ([9af2df3](9af2df3810))
2024-09-18 07:32:40 +00:00
ruthra kumar
829a42d679 Merge pull request #43255 from frappe/version-14-hotfix
chore: release v14
2024-09-18 13:01:22 +05:30
Frappe PR Bot
8f02b88119 chore(release): Bumped to Version 14.74.0
# [14.74.0](https://github.com/frappe/erpnext/compare/v14.73.2...v14.74.0) (2024-09-11)

### Bug Fixes

* `default_advance_account` field in Process Payment Reconciliation ([3691a50](3691a500eb))
* bom cost update is not working (backport [#43155](https://github.com/frappe/erpnext/issues/43155)) ([#43156](https://github.com/frappe/erpnext/issues/43156)) ([9110534](91105344bb))
* cancel common party advance jv while canceling the invoice ([f506952](f5069524f3))
* check multi-currency on jv for common party accounting with foreign currency ([a3a40fe](a3a40febf3))
* **Delivery Note:** translatability of validation errors ([5e33961](5e33961448))
* incorrect actual cost in Procurement Tracker report (backport [#43109](https://github.com/frappe/erpnext/issues/43109)) ([#43137](https://github.com/frappe/erpnext/issues/43137)) ([a4b9dda](a4b9dda4b2))
* incorrect qty after transaction in SLE (backport [#43103](https://github.com/frappe/erpnext/issues/43103)) ([#43104](https://github.com/frappe/erpnext/issues/43104)) ([77892f4](77892f4e24))
* return type of `get_party_details` (backport [#43131](https://github.com/frappe/erpnext/issues/43131)) ([#43135](https://github.com/frappe/erpnext/issues/43135)) ([e8aae50](e8aae5018a))
* set today in 'On This Date' in Available Batch Report ([36f65fc](36f65fc592))
* uncomment internal parties ([4dd06b6](4dd06b69a1))
* unreconcile allocation child table redirect url voucher no issue ([53496ed](53496ed79f))

### Features

* utility report to identify invalid ledger entries ([37b8715](37b8715096))
2024-09-11 05:11:21 +00:00
ruthra kumar
a91fb260e1 Merge pull request #43150 from frappe/version-14-hotfix
chore: release v14
2024-09-11 10:39:50 +05:30
Frappe PR Bot
7c37d4c71a chore(release): Bumped to Version 14.73.2
## [14.73.2](https://github.com/frappe/erpnext/compare/v14.73.1...v14.73.2) (2024-09-07)

### Bug Fixes

* incorrect qty after transaction in SLE (backport [#43103](https://github.com/frappe/erpnext/issues/43103)) ([#43104](https://github.com/frappe/erpnext/issues/43104)) ([f054261](f0542613e0))
2024-09-07 12:40:10 +00:00
rohitwaghchaure
873962a109 Merge pull request #43108 from frappe/mergify/bp/version-14/pr-43104
fix: incorrect qty after transaction in SLE (backport #43103) (backport #43104)
2024-09-07 18:08:45 +05:30
mergify[bot]
f0542613e0 fix: incorrect qty after transaction in SLE (backport #43103) (#43104)
fix: incorrect qty after transaction in SLE (#43103)

(cherry picked from commit 5ff87edc85)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 77892f4e24)
2024-09-07 12:05:10 +00:00
Frappe PR Bot
610f74ca62 chore(release): Bumped to Version 14.73.1
## [14.73.1](https://github.com/frappe/erpnext/compare/v14.73.0...v14.73.1) (2024-09-04)

### Bug Fixes

* adjust price insertion logic for internal suppliers/customers ([#42988](https://github.com/frappe/erpnext/issues/42988)) ([3912c68](3912c688ac))
* default fiscal year in report v14 ([#42960](https://github.com/frappe/erpnext/issues/42960)) ([9db7f7b](9db7f7b033))
* Discount and taxes in return document should follow the reference document (backport [#41911](https://github.com/frappe/erpnext/issues/41911)) ([#42573](https://github.com/frappe/erpnext/issues/42573)) ([c55f8e3](c55f8e3388))
* indentation ([e8cfb4d](e8cfb4d602))
* removed extra filter condition ([db319f9](db319f95f1))
* resolve gl entries duplication in asset purchase workflow ([#41845](https://github.com/frappe/erpnext/issues/41845)) ([b3c9697](b3c9697b7c))
* retain date filter when redirecting in Profit and Loss report ([c87f2d2](c87f2d2f7c))
* set up filters for dimensions ([450b3f3](450b3f338e))
* typeerror on Payment Entry ([dda4002](dda4002da4))
* typerror on default_currency ([22b283d](22b283d4a8))
2024-09-04 14:59:27 +00:00
ruthra kumar
012248bd12 Merge pull request #43025 from frappe/version-14-hotfix
chore: release v14
2024-09-04 20:28:07 +05:30
Frappe PR Bot
7648d8db80 chore(release): Bumped to Version 14.73.0
# [14.73.0](https://github.com/frappe/erpnext/compare/v14.72.2...v14.73.0) (2024-08-28)

### Bug Fixes

* call 'process' directly instead of creating 'process_subscripti ([8412938](8412938442))
* currency symbol on `amount` field suppose to show as `Company`'s currency not as default company's currency ([333c757](333c7577eb))
* do not copy date fields in opportunity doctype ([5186872](5186872b9b))
* german translations for incoterms ([003653c](003653c572))
* get amount with taxes and charges from payment entry ([094411f](094411f5f0))
* same posting date and time, creation causing incorrect balance qty (backport [#42904](https://github.com/frappe/erpnext/issues/42904)) ([#42919](https://github.com/frappe/erpnext/issues/42919)) ([c0b5f7c](c0b5f7c8eb))
* specify precision for net_amount ([#39481](https://github.com/frappe/erpnext/issues/39481)) ([cf34219](cf34219c71))
* stock reco with incorrect serial numbers ([#42865](https://github.com/frappe/erpnext/issues/42865)) ([54842ea](54842ea0b3))
* timeout while submitting stock entry (backport [#42929](https://github.com/frappe/erpnext/issues/42929)) ([#42930](https://github.com/frappe/erpnext/issues/42930)) ([73eab91](73eab91631))
* unsupported operand type(s) for *: 'float' and 'NoneType' (backport [#42916](https://github.com/frappe/erpnext/issues/42916)) ([#42917](https://github.com/frappe/erpnext/issues/42917)) ([2f4f2a8](2f4f2a8eec))
* update dimesions in exchange_gain_loss jv based on base document ([820dcd7](820dcd79bb))
* Update get_amount to return currency precision grand total ([0c2d7f2](0c2d7f2d9a))
* use of incorrect attribute ([ff14d72](ff14d72a46))

### Features

* report to identify incorrectly cleared cheques ([c29b95e](c29b95e306))

### Performance Improvements

* data import for stock entries (backport [#42711](https://github.com/frappe/erpnext/issues/42711)) ([#42903](https://github.com/frappe/erpnext/issues/42903)) ([12d5e24](12d5e247c5))
2024-08-28 05:02:39 +00:00
ruthra kumar
578f9b61c6 Merge pull request #42938 from frappe/version-14-hotfix
chore: release v14
2024-08-28 10:31:18 +05:30
Frappe PR Bot
c79088f46a chore(release): Bumped to Version 14.72.2
## [14.72.2](https://github.com/frappe/erpnext/compare/v14.72.1...v14.72.2) (2024-08-21)

### Bug Fixes

* disable rename from warehouse ([ed568fb](ed568fb01d))
* disable rename from warehouse ([a520d06](a520d06c3f))
* **patch:** replace repost with direct sql to update 'against_voucher ([c98ab15](c98ab15a44))
2024-08-21 05:21:14 +00:00
ruthra kumar
d5896cd8a0 Merge pull request #42833 from frappe/version-14-hotfix
chore: release v14
2024-08-21 10:49:56 +05:30
Frappe PR Bot
79edcb2a94 chore(release): Bumped to Version 14.72.1
## [14.72.1](https://github.com/frappe/erpnext/compare/v14.72.0...v14.72.1) (2024-08-16)

### Bug Fixes

* **patch:** replace repost with direct sql to update 'against_voucher ([f6f341a](f6f341ad58))
2024-08-16 11:09:57 +00:00
ruthra kumar
bd2f408f00 Merge pull request #42795 from frappe/mergify/bp/version-14/pr-42791
fix(patch): Use sql to update 'against_voucher' rather than reposting (backport #42791)
2024-08-16 16:38:39 +05:30
ruthra kumar
3b02f80a4e chore: resolve conflict 2024-08-16 16:04:52 +05:30
ruthra kumar
bebd1a0426 chore: update patches.txt
(cherry picked from commit 1721175a20)

# Conflicts:
#	erpnext/patches.txt
2024-08-16 10:26:22 +00:00
ruthra kumar
f6f341ad58 fix(patch): replace repost with direct sql to update 'against_voucher
(cherry picked from commit 13bb48434f)
2024-08-16 10:26:22 +00:00
Frappe PR Bot
2796aa8b57 chore(release): Bumped to Version 14.72.0
# [14.72.0](https://github.com/frappe/erpnext/compare/v14.71.0...v14.72.0) (2024-08-14)

### Bug Fixes

* dimensions in common party journal entry ([b73df8f](b73df8f5f0))
* expense causing p&l test case to fail ([a9fa9e8](a9fa9e86ea))
* force fetch updates for subcription ([f35bc43](f35bc43242))
* german translation of exit ([db27dd8](db27dd8702))
* ledger entries for pos return with update outstanding for self ([714a432](714a432c1c))
* Maintain same rate on qty change on Quotation to Sales Order ([29449cb](29449cbaf9))
* missing def expense if no exp in first month ([7cd6deb](7cd6debd0e))
* patch to fix incorrect against_voucher references in ledger ([5473546](54735469c1))
* pre-commit for better code formatting ([8f19832](8f19832aa5))
* Sort lists before calling itertools.groupby ([29cfb7c](29cfb7cf25))
* test case for zero deferred expense ([186701b](186701b54f))
* text color in sales funnel report based on theme ([2509618](25096185c9))
* typeerror on payment entry ([c4c001b](c4c001b6f9))
* update 'Paid Amount' on forex payment request ([365ef6b](365ef6b88a))
* warning message for negative stock (backport [#42683](https://github.com/frappe/erpnext/issues/42683)) ([#42709](https://github.com/frappe/erpnext/issues/42709)) ([e44daf7](e44daf73fb))

### Features

* changes in opportunity.py to show contacts and addresses from referenced and opportunities ([8d7e150](8d7e150afb))
2024-08-14 08:00:58 +00:00
ruthra kumar
a5ca5b5081 Merge pull request #42744 from frappe/version-14-hotfix
chore: release v14
2024-08-14 13:29:41 +05:30
Frappe PR Bot
a8a8a033a1 chore(release): Bumped to Version 14.71.0
# [14.71.0](https://github.com/frappe/erpnext/compare/v14.70.14...v14.71.0) (2024-08-07)

### Bug Fixes

* 'undefined' in PL and BS report summary on Consolidated report ([a5e1432](a5e14324da))
* company filter for filtring tax withheld vouchers ([b7731c8](b7731c8fd7))
* do not update item price and last purchase rate for inter transf… (backport [#42616](https://github.com/frappe/erpnext/issues/42616)) ([#42632](https://github.com/frappe/erpnext/issues/42632)) ([5c5349e](5c5349ed16))
* filter condition in item-wise purchase register ([bc9b461](bc9b46126b))
* fixed depreciation calculation as per income tax act ([8df1079](8df1079618))
* item_code filter in item-wise sales register ([f0f8a2f](f0f8a2f01b))
* min height for rows in sales funnel ([7086a96](7086a96462))
* set query filters for sales / purchase tax template on PE ([c8d8536](c8d85364b9))
* slowness in reposting dependent vouchers. (backport [#42282](https://github.com/frappe/erpnext/issues/42282)) ([#42569](https://github.com/frappe/erpnext/issues/42569)) ([3fb6f97](3fb6f97f66))

### Features

* expiry date column in Available Batch Report (backport [#42628](https://github.com/frappe/erpnext/issues/42628)) ([#42641](https://github.com/frappe/erpnext/issues/42641)) ([83928bb](83928bbf78))
2024-08-07 09:27:57 +00:00
ruthra kumar
f0230b2dfd Merge pull request #42648 from frappe/version-14-hotfix
chore: release v14
2024-08-07 14:56:41 +05:30
Frappe PR Bot
4089af5a7b chore(release): Bumped to Version 14.70.14
## [14.70.14](https://github.com/frappe/erpnext/compare/v14.70.13...v14.70.14) (2024-08-01)

### Bug Fixes

* slowness in reposting dependent vouchers. (backport [#42282](https://github.com/frappe/erpnext/issues/42282)) ([#42569](https://github.com/frappe/erpnext/issues/42569)) ([cccfbf1](cccfbf193e))
2024-08-01 09:12:20 +00:00
rohitwaghchaure
06c73ef2da Merge pull request #42570 from frappe/mergify/bp/version-14/pr-42569
fix: slowness in reposting dependent vouchers. (backport #42282) (backport #42569)
2024-08-01 14:40:58 +05:30
mergify[bot]
cccfbf193e fix: slowness in reposting dependent vouchers. (backport #42282) (#42569)
fix: slowness in reposting dependent vouchers. (#42282)

(cherry picked from commit b17696a8ae)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 3fb6f97f66)
2024-08-01 07:52:46 +00:00
Frappe PR Bot
37a7da3371 chore(release): Bumped to Version 14.70.13
## [14.70.13](https://github.com/frappe/erpnext/compare/v14.70.12...v14.70.13) (2024-07-31)

### Bug Fixes

* builtins.KeyError: ('ABC', 'Store - CP') (backport [#42505](https://github.com/frappe/erpnext/issues/42505)) ([#42508](https://github.com/frappe/erpnext/issues/42508)) ([0256c64](0256c64634))
* consider payment entries for checking if tds is deducted ([e7432fc](e7432fc60d))
* dynamic condition in the pricing rule not working (backport [#42467](https://github.com/frappe/erpnext/issues/42467)) ([#42543](https://github.com/frappe/erpnext/issues/42543)) ([53034c3](53034c332b))
* **gross profit:** incorrect valuation rate on different warehouses ([1a7efbb](1a7efbb654))
* ignore duplicates while creating default templates ([99bc8e8](99bc8e849c))
* incorrect cost_center on AR/AP report ([0e2abbd](0e2abbd08e))
* keep status as In Progress for RIV for Timeout Error (backport [#42274](https://github.com/frappe/erpnext/issues/42274)) ([#42504](https://github.com/frappe/erpnext/issues/42504)) ([8e8d0c7](8e8d0c7bd0))
* parenttype in item wise purchase and sales register ([97f2e88](97f2e88f4c))
* performance issue for the report Purchase Order Analysis report (backport [#42503](https://github.com/frappe/erpnext/issues/42503)) ([#42506](https://github.com/frappe/erpnext/issues/42506)) ([f42f1bb](f42f1bb35f))
* price_list_currency not found error (backport [#42534](https://github.com/frappe/erpnext/issues/42534)) ([#42538](https://github.com/frappe/erpnext/issues/42538)) ([3ba6f40](3ba6f40063))
* warehouse filter in Product Bundle Balance (backport [#42532](https://github.com/frappe/erpnext/issues/42532)) ([#42536](https://github.com/frappe/erpnext/issues/42536)) ([0441984](0441984405))
2024-07-31 06:00:37 +00:00
ruthra kumar
81c362dbe4 Merge pull request #42542 from frappe/version-14-hotfix
chore: release v14
2024-07-31 11:29:23 +05:30
Frappe PR Bot
fe4b2e36cc chore(release): Bumped to Version 14.70.12
## [14.70.12](https://github.com/frappe/erpnext/compare/v14.70.11...v14.70.12) (2024-07-24)

### Bug Fixes

* missing cr/dr notes on payment reconciliation ([90ee21f](90ee21f868))
* remove proprietorship and update it with individual (backport [#42307](https://github.com/frappe/erpnext/issues/42307)) ([8c1f619](8c1f6196b8))
* set pos data if not return doc ([6ecb064](6ecb064264))
* Show the rows in AR/AP report where outstanding equals to 0.01 ([2936988](2936988cc6))
2024-07-24 07:26:52 +00:00
ruthra kumar
6759b90f85 Merge pull request #42448 from frappe/version-14-hotfix
chore: release v14
2024-07-24 12:55:36 +05:30
Frappe PR Bot
12a31de25a chore(release): Bumped to Version 14.70.11
## [14.70.11](https://github.com/frappe/erpnext/compare/v14.70.10...v14.70.11) (2024-07-17)

### Bug Fixes

* missing discount on POS Credit Notes ([ac48c3d](ac48c3d4e7))
2024-07-17 05:11:39 +00:00
ruthra kumar
3b9400755e Merge pull request #42353 from frappe/version-14-hotfix
chore: release v14
2024-07-17 10:40:24 +05:30
Frappe PR Bot
1d66b7e5a3 chore(release): Bumped to Version 14.70.10
## [14.70.10](https://github.com/frappe/erpnext/compare/v14.70.9...v14.70.10) (2024-07-10)

### Bug Fixes

* add missing german translations ([d5c1c62](d5c1c62622))
* empty item-wise sales/purchase register reports on initial load ([13895fa](13895fa060))
* fetch expence account from asset category ([4d6a71a](4d6a71ab4b))
* group by in item-wise purchase register ([62ad466](62ad466a3b))
* **Holiday List:** sort holidays on save to avoid disorienting the user (backport [#42236](https://github.com/frappe/erpnext/issues/42236)) ([#42251](https://github.com/frappe/erpnext/issues/42251)) ([fcf6500](fcf6500144))
* manual pick allow to pick more than available stock (backport [#42155](https://github.com/frappe/erpnext/issues/42155)) ([#42158](https://github.com/frappe/erpnext/issues/42158)) ([454e147](454e147592))
* multiple free items on same Item Group ([701dd9e](701dd9e19b))
* removed max discount validation for sales return ([4195c50](4195c50f02))
* stock qty validation in SCR (backport [#42124](https://github.com/frappe/erpnext/issues/42124)) ([#42224](https://github.com/frappe/erpnext/issues/42224)) ([e2f8e02](e2f8e02c73))
* tax on stock_rbnb on repost of Purchase Receipt ([106c154](106c154a16))
* **tds:** use doctype reference when mapping keys across multiple doctypes ([51cbbee](51cbbee4ca))
* updated logic for calculating tax_withholding_net_total in payment entry ([49e5066](49e50662b6))
* use standard method to get `_doc_before_save` ([9fde733](9fde7330e0))
2024-07-10 10:43:56 +00:00
ruthra kumar
ab9bde86f9 Merge pull request #42254 from frappe/version-14-hotfix
chore: release v14
2024-07-10 16:12:40 +05:30
Frappe PR Bot
30cba7ee2c chore(release): Bumped to Version 14.70.9
## [14.70.9](https://github.com/frappe/erpnext/compare/v14.70.8...v14.70.9) (2024-07-05)

### Bug Fixes

* blank item-wise sales/purchase register reports on first load ([0f275a9](0f275a9ff0))
* group by in item-wise purchase register ([1840267](18402677da))
2024-07-05 02:47:14 +00:00
ruthra kumar
21a60c9927 Merge pull request #42190 from frappe/mergify/bp/version-14/pr-41975
fix: group by in item-wise purchase register (backport #41975)
2024-07-05 08:15:53 +05:30
ruthra kumar
0f275a9ff0 fix: blank item-wise sales/purchase register reports on first load 2024-07-05 07:50:37 +05:30
Nihantra C. Patel
18402677da fix: group by in item-wise purchase register
(cherry picked from commit 3fab00135b)
2024-07-05 02:05:26 +00:00
Frappe PR Bot
f2d113dcf2 chore(release): Bumped to Version 14.70.8
## [14.70.8](https://github.com/frappe/erpnext/compare/v14.70.7...v14.70.8) (2024-07-03)

### Bug Fixes

* always post to tax account heads if LCV is booked ([650b25f](650b25fc23))
* asset value correction in fixed asset register ([#41943](https://github.com/frappe/erpnext/issues/41943)) ([a2ee463](a2ee4631cc))
* consistent query field name in item wise purchase register with item wise sales register ([9665212](9665212607))
* **Delivery Note:** only show permitted actions ([0aa246c](0aa246c39e))
* dynamic_link for party in customer and supplier dashboard ([6de7a8d](6de7a8de10))
* incorrect against_account upon reposting ([6028a19](6028a19e6b))
* Re-open allows SO's to be over credit limit ([952a7b4](952a7b46d5))
* **Sales Order:** only show permitted actions ([5468a3b](5468a3b0b7))
* timeout while cancelling LCV ([#42030](https://github.com/frappe/erpnext/issues/42030)) ([21bf7fd](21bf7fd1f8))
2024-07-03 05:02:59 +00:00
ruthra kumar
97e4495f1f Merge pull request #42141 from frappe/version-14-hotfix
chore: release v14
2024-07-03 10:31:45 +05:30
Frappe PR Bot
b593f57637 chore: release v14 (#42023)
* fix(Sales Order): only show permitted actions

(cherry picked from commit c29d955371)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.js

* fix(Delivery Note): only show permitted actions

(cherry picked from commit 418bdc1dcc)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/delivery_note.js

* fix: consistent query field name in item wise purchase register with item wise sales register

* chore: resolve conflicts

* refactor: remove use of can_create for Payment Request (#41647)

(cherry picked from commit 47bc5691a1)

* fix: incorrect against_account upon reposting

(cherry picked from commit 20c4098399)

* fix: asset value correction in fixed asset register (#41943)

* fix: timeout while cancelling LCV (#42030)

fix: timeout while canelling LCV

---------

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
Co-authored-by: ljain112 <ljain112@gmail.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
2024-06-26 18:02:23 +05:30
Frappe PR Bot
5ae9ec2657 chore(release): Bumped to Version 14.70.7
## [14.70.7](https://github.com/frappe/erpnext/compare/v14.70.6...v14.70.7) (2024-06-19)

### Bug Fixes

* add LCV flag to determine negative expenses ([20b8ee1](20b8ee1e90))
* allow Auditor to read a Fiscal Year ([b1a911a](b1a911aa9c))
* allow Auditor to select a company ([18b6d50](18b6d50a31))
* allow Employee role to select Department (backport [#41877](https://github.com/frappe/erpnext/issues/41877)) ([#41880](https://github.com/frappe/erpnext/issues/41880)) ([245c6d8](245c6d8672))
* migrate lead notes ([8f5278e](8f5278e3d4))
* only show user and date if available ([598c581](598c581623))
* parameters for PI references ([d484553](d48455393e))
* regional overide for updating item valution ([6441bc7](6441bc7862))
* reposting validation related PCV ([bcfc83d](bcfc83d8d5))
* reset perpetual inventory flag after test ([eae5f27](eae5f27ec8))
2024-06-19 06:25:45 +00:00
ruthra kumar
890289563a Merge pull request #41942 from frappe/version-14-hotfix
chore: release v14
2024-06-19 11:54:25 +05:30
Frappe PR Bot
32fba94b2a chore(release): Bumped to Version 14.70.6
## [14.70.6](https://github.com/frappe/erpnext/compare/v14.70.5...v14.70.6) (2024-06-11)

### Bug Fixes

* Add additional condition application ([e79e9e9](e79e9e94d0))
* fixing Item-wise sales register ([5d97c7c](5d97c7cff9))
* fixing Item-wise sales register [#41373](https://github.com/frappe/erpnext/issues/41373) ([ab90b81](ab90b815e3))
* fixing Item-wise sales register and purchase register [#41373](https://github.com/frappe/erpnext/issues/41373) ([ee47440](ee47440063))
* Item-wise Sales and Purchase register with no item codes [#41373](https://github.com/frappe/erpnext/issues/41373) ([bc24e75](bc24e75c24))
* terms and conditions for material request (backport [#41834](https://github.com/frappe/erpnext/issues/41834)) ([#41836](https://github.com/frappe/erpnext/issues/41836)) ([7234625](7234625d65))
2024-06-11 13:07:24 +00:00
rohitwaghchaure
a6ed10b712 Merge pull request #41853 from frappe/version-14-hotfix
chore: release v14
2024-06-11 18:36:07 +05:30
Frappe PR Bot
0c4a2af9ab chore(release): Bumped to Version 14.70.5
## [14.70.5](https://github.com/frappe/erpnext/compare/v14.70.4...v14.70.5) (2024-06-10)

### Bug Fixes

* Add additional condition application ([810378c](810378c899))
* fixing Item-wise sales register ([3823e0e](3823e0e494))
* fixing Item-wise sales register [#41373](https://github.com/frappe/erpnext/issues/41373) ([e8286d7](e8286d7b3c))
* fixing Item-wise sales register and purchase register [#41373](https://github.com/frappe/erpnext/issues/41373) ([fd73a8a](fd73a8a348))
* Item-wise Sales and Purchase register with no item codes [#41373](https://github.com/frappe/erpnext/issues/41373) ([91de469](91de46922d))
2024-06-10 04:55:25 +00:00
Deepesh Garg
5ba0082bc7 Merge pull request #41832 from frappe/mergify/bp/version-14/pr-41829
fix: Item-wise Sales and Purchase register with no item codes (#41424)
2024-06-10 10:24:04 +05:30
Deepesh Garg
47b3e96a37 chore: resolve conflicts
(cherry picked from commit e9b7d00afb)
2024-06-10 04:53:29 +00:00
Deepesh Garg
810378c899 fix: Add additional condition application
(cherry picked from commit 8ec364df6f)

# Conflicts:
#	erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
(cherry picked from commit e79e9e94d0)
2024-06-10 04:53:29 +00:00
Deepesh Garg
bdf198c94f chore: update condition queries in qb
(cherry picked from commit d2af36e1eb)

# Conflicts:
#	erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
#	erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
(cherry picked from commit e94e15259b)
2024-06-10 04:53:29 +00:00
Poorvi-R-Bhat
e8286d7b3c fix: fixing Item-wise sales register #41373
(cherry picked from commit eafa88b8e9)

# Conflicts:
#	erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
(cherry picked from commit ab90b815e3)
2024-06-10 04:53:29 +00:00
Poorvi-R-Bhat
fd73a8a348 fix: fixing Item-wise sales register and purchase register #41373
(cherry picked from commit 76073ae228)

# Conflicts:
#	erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
(cherry picked from commit ee47440063)
2024-06-10 04:53:29 +00:00
Poorvi-R-Bhat
91de46922d fix: Item-wise Sales and Purchase register with no item codes #41373
(cherry picked from commit 1b45ecfcae)

# Conflicts:
#	erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
#	erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
(cherry picked from commit bc24e75c24)
2024-06-10 04:53:29 +00:00
Poorvi
3823e0e494 fix: fixing Item-wise sales register
(cherry picked from commit c90185f533)
(cherry picked from commit 5d97c7cff9)
2024-06-10 04:53:28 +00:00
Frappe PR Bot
b6648eebfa chore(release): Bumped to Version 14.70.4
## [14.70.4](https://github.com/frappe/erpnext/compare/v14.70.3...v14.70.4) (2024-06-05)

### Bug Fixes

* Filters in account balance report ([570985f](570985f40e))
* get assets received but not billed account only if any asset item is received ([#41734](https://github.com/frappe/erpnext/issues/41734)) ([39885b2](39885b2b01))
* Ignore disabling default currency field while creating new company (backport [#41699](https://github.com/frappe/erpnext/issues/41699)) ([#41760](https://github.com/frappe/erpnext/issues/41760)) ([5ab5bd1](5ab5bd138f))
* payment term when creating PO from SO (backport [#41376](https://github.com/frappe/erpnext/issues/41376)) ([#41743](https://github.com/frappe/erpnext/issues/41743)) ([dc32657](dc3265751c))
2024-06-05 01:17:52 +00:00
ruthra kumar
faa3c7c3a4 Merge pull request #41774 from frappe/version-14-hotfix
chore: release v14
2024-06-05 06:46:34 +05:30
Frappe PR Bot
13dfbe3d80 chore(release): Bumped to Version 14.70.3
## [14.70.3](https://github.com/frappe/erpnext/compare/v14.70.2...v14.70.3) (2024-05-29)

### Bug Fixes

* add in some indices to speed up Purchase Order deletion ([c16c41e](c16c41ee59))
* Add to Cart button width for Mobile/Tablet ([819ced4](819ced4cb3))
* cost center filter according to the company in project ([7035969](7035969db7))
* cost center filter according to the company in project ([0cf97f2](0cf97f2559))
* cost center filter according to the company in project ([459d136](459d136368))
* dict can not be used as parameter ([#41598](https://github.com/frappe/erpnext/issues/41598)) ([649c192](649c192abe))
* Fetch outstanding and total amount for reference journal entry ([50f6afd](50f6afd588))
* set expense account as Assets RBNB only if it is booked in linked PR ([#41368](https://github.com/frappe/erpnext/issues/41368)) ([014486d](014486de39))
* Update depreciation schedule via asset repair ([#41344](https://github.com/frappe/erpnext/issues/41344)) ([eb418e8](eb418e8659))
2024-05-29 07:52:36 +00:00
ruthra kumar
7e3c15e0b6 Merge pull request #41665 from frappe/version-14-hotfix
chore: release v14
2024-05-29 13:21:18 +05:30
Frappe PR Bot
c910b8ab03 chore(release): Bumped to Version 14.70.2
## [14.70.2](https://github.com/frappe/erpnext/compare/v14.70.1...v14.70.2) (2024-05-23)

### Bug Fixes

* Fetch outstanding and total amount for reference journal entry ([94c3ee6](94c3ee645d))
2024-05-23 11:01:54 +00:00
ruthra kumar
b9ebb50a02 Merge pull request #41600 from frappe/mergify/bp/version-14/pr-40957
fix: Fetch correct outstanding and total amount for reference journal entry (backport #40920) (backport #40957)
2024-05-23 16:30:31 +05:30
ruthra kumar
38cc28a4c3 chore: resolve conflicts
(cherry picked from commit 5230d411bf)
2024-05-23 10:39:16 +00:00
ruthra kumar
bbb9b9e3b6 chore: remove unused imports
(cherry picked from commit a6bf7c1ebd)
2024-05-23 10:39:15 +00:00
Nabin Hait
94c3ee645d fix: Fetch outstanding and total amount for reference journal entry
(cherry picked from commit f331f9b15c)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.js
#	erpnext/accounts/doctype/payment_entry/payment_entry.py
(cherry picked from commit 50f6afd588)
2024-05-23 10:39:15 +00:00
Frappe PR Bot
b7d6a54bed chore(release): Bumped to Version 14.70.1
## [14.70.1](https://github.com/frappe/erpnext/compare/v14.70.0...v14.70.1) (2024-05-22)

### Bug Fixes

* minor Dr and Cr between Purchase Receipt and Purchase Invoice ([82d206b](82d206b709))
* possible sql error on General Ledger ([dfb4c47](dfb4c47089))
* print format bold for field "total" ([89d507e](89d507e07e))
* priority not working for multiple pricing rules (backport [#41516](https://github.com/frappe/erpnext/issues/41516)) ([#41524](https://github.com/frappe/erpnext/issues/41524)) ([97fdda8](97fdda8a7c))
* typerror on hide_fields ([331a743](331a743d69))
* validate reorder group warehouse (backport [#41478](https://github.com/frappe/erpnext/issues/41478)) ([#41479](https://github.com/frappe/erpnext/issues/41479)) ([2659535](26595351cc))
2024-05-22 08:46:30 +00:00
ruthra kumar
05e4dae1b8 Merge pull request #41575 from frappe/version-14-hotfix
chore: release v14
2024-05-22 14:15:11 +05:30
Frappe PR Bot
dfaca93292 chore(release): Bumped to Version 14.70.0
# [14.70.0](https://github.com/frappe/erpnext/compare/v14.69.1...v14.70.0) (2024-05-15)

### Bug Fixes

* "Based on" field always has the value "Not applicable" ([1078a98](1078a98cce))
* address filter and quotation to for prospect ([c9e7f45](c9e7f450c5))
* address filter and quotation to for prospect ([6902780](690278042d))
* address filter and quotation to for prospect ([754e193](754e193c76))
* consistent use of "Address & Contact" (backport [#41386](https://github.com/frappe/erpnext/issues/41386)) ([#41387](https://github.com/frappe/erpnext/issues/41387)) ([256d6a4](256d6a47ac))
* data getting override in delivery trip (backport [#41431](https://github.com/frappe/erpnext/issues/41431)) ([#41432](https://github.com/frappe/erpnext/issues/41432)) ([5366356](5366356400))
* Default dates in report ([c3244f0](c3244f009b))
* default fiscal year ([7a380f5](7a380f584d))
* Duplicate party name column in AR/AP report ([a8be5f0](a8be5f0789))
* PSOA ageing ([e69e540](e69e5404d3))

### Features

* allow to pick manually qty / batches / serial nos (backport [#40723](https://github.com/frappe/erpnext/issues/40723)) ([#41435](https://github.com/frappe/erpnext/issues/41435)) ([7b28d7d](7b28d7d2b8))
2024-05-15 05:12:14 +00:00
Deepesh Garg
40de3f3481 Merge pull request #41458 from frappe/version-14-hotfix
chore: release v14
2024-05-15 10:40:58 +05:30
Frappe PR Bot
9d5e4b3b3a chore(release): Bumped to Version 14.69.1
## [14.69.1](https://github.com/frappe/erpnext/compare/v14.69.0...v14.69.1) (2024-05-09)

### Bug Fixes

* added brand column in Warehouse wise Item Balance Age and Value … (backport [#41280](https://github.com/frappe/erpnext/issues/41280)) ([#41281](https://github.com/frappe/erpnext/issues/41281)) ([d727c52](d727c52421))
* Cost center not getting saved in PSOA ([e82ea12](e82ea12cbc))
* filter validation for batch-wise balance history report (backport [#41356](https://github.com/frappe/erpnext/issues/41356)) ([#41360](https://github.com/frappe/erpnext/issues/41360)) ([339256b](339256bc71))
* incorrect query for Purchase Invoice rate in GP ([93b30d9](93b30d9f11))
* missing Item Name on Save for Quotation created from Item (backport [#41233](https://github.com/frappe/erpnext/issues/41233)) ([#41303](https://github.com/frappe/erpnext/issues/41303)) ([a26ae64](a26ae64385))
* pricing rule rounding ([d2ce927](d2ce927891))
* reset rate for serial batch supplied items ([#41293](https://github.com/frappe/erpnext/issues/41293)) ([cd33199](cd33199da2))

### Performance Improvements

* index on item code for the Pick List Item doctype (backport [#41357](https://github.com/frappe/erpnext/issues/41357)) ([#41362](https://github.com/frappe/erpnext/issues/41362)) ([4647ec8](4647ec8892))
2024-05-09 05:30:06 +00:00
ruthra kumar
d7709cf4e4 Merge pull request #41354 from frappe/version-14-hotfix
chore: release v14
2024-05-09 10:58:39 +05:30
Frappe PR Bot
5ae29655f9 chore(release): Bumped to Version 14.69.0
# [14.69.0](https://github.com/frappe/erpnext/compare/v14.68.2...v14.69.0) (2024-05-02)

### Bug Fixes

* 'NoneType' object has no attribute '_read_rowdata_packet_unbuffered' ([949aa93](949aa9346c))
* args when get the delivery note in delivery trip ([abe64aa](abe64aa1ab))
* args when get the delivery note in delivery trip ([1a7b3c4](1a7b3c437d))
* display term name for single term invoices ([58b68b7](58b68b7597))
* duplicate column in the stock ledger report ([3fcdcef](3fcdcef178))
* handle stock balance unbuffered_cursor error (backport [#41186](https://github.com/frappe/erpnext/issues/41186)) ([#41187](https://github.com/frappe/erpnext/issues/41187)) ([59010c9](59010c9a61))
* incorrectly applying TDS when Advance is in previous FY ([eb22fb9](eb22fb9326))
* mode of payment has precedance ([33d38ba](33d38ba3a7))
* multiple pricing rules with discount amount and discount percentage not working ([#41211](https://github.com/frappe/erpnext/issues/41211)) ([54313b5](54313b5db9))
* negative stock qty error for stock reconciliation ([#41283](https://github.com/frappe/erpnext/issues/41283)) ([9aa054c](9aa054c400))
* permission issue when user permission restricts on company ([45c4167](45c4167c86))
* validation to prevent overallocation ([8318286](8318286865))

### Features

* allow to do reposting for all stock transactions (audit) (backport [#41165](https://github.com/frappe/erpnext/issues/41165)) ([#41205](https://github.com/frappe/erpnext/issues/41205)) ([1e13193](1e1319351d))

### Performance Improvements

* timeout issue while submitting purchase receipt (v14) ([113351e](113351e850))
2024-05-02 04:31:43 +00:00
ruthra kumar
81a99309d8 Merge pull request #41264 from frappe/version-14-hotfix
chore: release v14
2024-05-02 09:59:30 +05:30
Frappe PR Bot
d160f5b61a chore(release): Bumped to Version 14.68.2
## [14.68.2](https://github.com/frappe/erpnext/compare/v14.68.1...v14.68.2) (2024-04-26)

### Performance Improvements

* timeout issue while submitting purchase receipt (v14) ([c93840e](c93840eb56))
2024-04-26 06:53:19 +00:00
rohitwaghchaure
40ece3f5da Merge pull request #41193 from frappe/mergify/bp/version-14/pr-41174
perf: timeout issue while submitting purchase receipt (v14) (backport #41174)
2024-04-26 12:21:59 +05:30
Rohit Waghchaure
c93840eb56 perf: timeout issue while submitting purchase receipt (v14)
(cherry picked from commit 113351e850)
2024-04-26 05:27:47 +00:00
Frappe PR Bot
2026c986ba chore(release): Bumped to Version 14.68.1
## [14.68.1](https://github.com/frappe/erpnext/compare/v14.68.0...v14.68.1) (2024-04-25)

### Bug Fixes

* mode of payment has precedance ([7e52f72](7e52f72bed))
2024-04-25 03:43:28 +00:00
ruthra kumar
91c202f172 Merge pull request #41179 from frappe/mergify/bp/version-14/pr-41142
fix: mode of payment has precedance in Payment Entry (backport #41142)
2024-04-25 09:12:14 +05:30
ruthra kumar
7e52f72bed fix: mode of payment has precedance
Mode of Payment is given precedence over company/party bank account

(cherry picked from commit 4aef969879)
2024-04-25 09:06:04 +05:30
Frappe PR Bot
30b2cac423 chore(release): Bumped to Version 14.68.0
# [14.68.0](https://github.com/frappe/erpnext/compare/v14.67.2...v14.68.0) (2024-04-24)

### Bug Fixes

* account and stock manager read perm ([03ce9ee](03ce9ee321))
* allow Employee role to select cost center & project (accounting dimensions) (backport [#41160](https://github.com/frappe/erpnext/issues/41160)) ([#41161](https://github.com/frappe/erpnext/issues/41161)) ([bb48440](bb48440591))
* do not add actual expense twice for validating budget ([ec4f07f](ec4f07fd60))
* don't attempt to set gender from salutation (backport [#40997](https://github.com/frappe/erpnext/issues/40997)) ([#41072](https://github.com/frappe/erpnext/issues/41072)) ([5d05bf8](5d05bf8d4e))
* Missing args while fetching items from delivery note ([0df80ad](0df80ad923))
* Multiple partial payment requests against Purchase Invoice ([f1b75e8](f1b75e8c54))
* Party type in Payment Order ([a2e1d13](a2e1d132df))
* Permission for lower dedcution certificate ([a22be6f](a22be6f9b9))
* Test case ([5aef9d2](5aef9d2ef2))
* validate uom is integer for PR item (backport [#41074](https://github.com/frappe/erpnext/issues/41074)) ([#41076](https://github.com/frappe/erpnext/issues/41076)) ([9d0c1dc](9d0c1dc46f))

### Features

* Available batches report as on specific date ([8868cb1](8868cb147d))
* show expense breakup ([f087ec8](f087ec8df5))

### Performance Improvements

* stock ageing and batch-wise balance history report ([6017e7a](6017e7ac3e))
2024-04-24 07:32:43 +00:00
Deepesh Garg
67be2ba9dc Merge pull request #41151 from frappe/version-14-hotfix
chore: release v14
2024-04-24 13:01:19 +05:30
Frappe PR Bot
84789b7407 chore(release): Bumped to Version 14.67.2
## [14.67.2](https://github.com/frappe/erpnext/compare/v14.67.1...v14.67.2) (2024-04-17)

### Bug Fixes

* barcode not fetched on selection of item (backport [#40814](https://github.com/frappe/erpnext/issues/40814)) ([#41027](https://github.com/frappe/erpnext/issues/41027)) ([0bee921](0bee921d40))
* Delayed Order Report not working (backport [#41037](https://github.com/frappe/erpnext/issues/41037)) ([#41038](https://github.com/frappe/erpnext/issues/41038)) ([e956dbb](e956dbbf68))
* Don't call get_fiscal_year if setup is not done yet ([59cea9f](59cea9f4dd))
* Don't set delivery date as today while making SO from Quotation ([b47e224](b47e224a1c))
* exclude some query builder lines from ruff rules ([2425119](2425119b5e))
* expense account set as COGS for stock entry Material Issue (backport [#41026](https://github.com/frappe/erpnext/issues/41026)) ([#41028](https://github.com/frappe/erpnext/issues/41028)) ([153e0ba](153e0ba81b))
* get address if multiple companies ([c2b6b64](c2b6b64e2e))
* get address if multiple companies ([0493872](04938726d9))
* **gp:** SLEs not fetched for correct warehouse ([97e7b3f](97e7b3f3d3))
* incorrect exc gain/loss for PE against JE for payable accounts ([50a74ee](50a74ee7fe))
* Resolve merge conflicts ([cc925ae](cc925ae938))
* Subcontracting Receipt GL Entries (backport [#40773](https://github.com/frappe/erpnext/issues/40773)) ([#40978](https://github.com/frappe/erpnext/issues/40978)) ([c2c4548](c2c4548cc0))
* test cases ([79e23da](79e23dad2c))
* **treewide:** manual ruff fixes ([b087fb3](b087fb3d54))
* use 'eq' and isnull() on qb conditions ([b4ed2d2](b4ed2d2c16))
2024-04-17 06:12:30 +00:00
rohitwaghchaure
cd70c6c1b2 Merge pull request #41032 from frappe/version-14-hotfix
chore: release v14
2024-04-17 11:41:15 +05:30
359 changed files with 3567 additions and 10516 deletions

View File

@@ -10,7 +10,6 @@ WEBSITE_REPOS = [
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"docs.frappe.io",
"frappeframework.com",
]

4
.github/release.yml vendored
View File

@@ -1,4 +0,0 @@
changelog:
exclude:
labels:
- skip-release-notes

View File

@@ -1,30 +0,0 @@
name: "Auto-label PRs based on title"
on:
pull_request_target:
types: [opened, reopened]
jobs:
add-label-if-prefix-matches:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Check PR title and add label if it matches prefixes
uses: actions/github-script@v7
continue-on-error: true
with:
script: |
const title = context.payload.pull_request.title.toLowerCase();
const prefixes = ['chore', 'ci', 'style', 'test', 'refactor'];
// Check if the PR title starts with any of the prefixes
if (prefixes.some(prefix => title.startsWith(prefix))) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['skip-release-notes']
});
}

View File

@@ -9,16 +9,15 @@ jobs:
name: linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: '3.10'
cache: pip
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0
uses: pre-commit/action@v2.0.3
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules

View File

@@ -16,7 +16,7 @@ concurrency:
jobs:
test:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
timeout-minutes: 60
name: Patch Test
@@ -59,7 +59,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -68,7 +68,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
@@ -83,7 +83,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -79,7 +79,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -88,7 +88,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
@@ -103,7 +103,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -66,7 +66,7 @@ jobs:
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -75,7 +75,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
@@ -90,7 +90,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -1,5 +1,5 @@
exclude: 'node_modules|.git'
default_stages: [pre-commit]
default_stages: [commit]
fail_fast: false

View File

@@ -4,22 +4,22 @@
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @khushi8112 @deepeshgarg007
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/buying/ @rohitwaghchaure
erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure
erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure
erpnext/subcontracting @rohitwaghchaure
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
.github/ @deepeshgarg007
pyproject.toml @akhilnarang
pyproject.toml @ankush

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "14.67.1"
__version__ = "14.74.8"
def get_default_company(user=None):

View File

@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
)
if account:
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
conditions += f"AND {deferred_account}='{frappe.db.escape(account)}'"
elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}"

View File

@@ -94,8 +94,8 @@ frappe.ui.form.on("Account", {
function () {
frappe.route_options = {
account: frm.doc.name,
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
from_date: frappe.sys_defaults.year_start_date,
to_date: frappe.sys_defaults.year_end_date,
company: frm.doc.company,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _, throw
from frappe.utils import add_to_date, cint, cstr, pretty_date
from frappe.utils import cint, cstr
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
@@ -400,7 +400,6 @@ def validate_account_number(name, account_number, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.db.get_value("Account", name, "company", as_dict=True)
if not account:
return
@@ -421,7 +420,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
"name",
)
if old_name and not from_descendant:
if old_name:
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
@@ -462,7 +461,6 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
@@ -516,27 +514,3 @@ def sync_update_account_number_in_child(
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)
def _ensure_idle_system():
# Don't allow renaming if accounting entries are actively being updated, there are two main reasons:
# 1. Correctness: It's next to impossible to ensure that renamed account is not being used *right now*.
# 2. Performance: Renaming requires locking out many tables entirely and severely degrades performance.
if frappe.flags.in_test:
return
try:
# We also lock inserts to GL entry table with for_update here.
last_gl_update = frappe.db.get_value("GL Entry", {}, "modified", for_update=True, wait=False)
except frappe.QueryTimeoutError:
# wait=False fails immediately if there's an active transaction.
last_gl_update = add_to_date(None, seconds=-1)
if last_gl_update > add_to_date(None, minutes=-5):
frappe.throw(
_(
"Last GL Entry update was done {}. This operation is not allowed while system is actively being used. Please wait for 5 minutes before retrying."
).format(pretty_date(last_gl_update)),
title=_("System In Use"),
)

View File

@@ -279,8 +279,8 @@ frappe.treeview_settings["Account"] = {
click: function (node, btn) {
frappe.route_options = {
account: node.label,
from_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
to_date: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
from_date: frappe.sys_defaults.year_start_date,
to_date: frappe.sys_defaults.year_end_date,
company:
frappe.treeview_settings["Account"].treeview.page.fields_dict.company.get_value(),
};

View File

@@ -98,7 +98,7 @@
"Office Maintenance Expenses": {},
"Office Rent": {},
"Postal Expenses": {},
"Print and Stationery": {},
"Print and Stationary": {},
"Rounded Off": {
"account_type": "Round Off"
},

View File

@@ -31,8 +31,7 @@
"label": "Reference Document Type",
"options": "DocType",
"read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"default": "0",

View File

@@ -25,7 +25,6 @@ class AccountingDimension(Document):
"Accounting Dimension Detail",
"Company",
"Account",
"Finance Book",
):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)

View File

@@ -3,14 +3,4 @@
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {},
drop_ar_procedures: function (frm) {
frm.call({
doc: frm.doc,
method: "drop_ar_sql_procedures",
callback: function (r) {
frappe.show_alert(__("Procedures dropped"), 5);
},
});
},
});

View File

@@ -44,7 +44,6 @@
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days",
"exchange_gain_loss_posting_date",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
@@ -74,13 +73,7 @@
"remarks_section",
"general_ledger_remarks_length",
"column_break_lvjk",
"receivable_payable_remarks_length",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"column_break_ntmi",
"drop_ar_procedures",
"legacy_section",
"ignore_is_opening_check_for_reporting"
"receivable_payable_remarks_length"
],
"fields": [
{
@@ -390,7 +383,7 @@
{
"fieldname": "section_break_jpd0",
"fieldtype": "Section Break",
"label": "Payment Reconciliation Settings"
"label": "Payment Reconciliations"
},
{
"default": "0",
@@ -469,49 +462,6 @@
"fieldname": "remarks_section",
"fieldtype": "Section Break",
"label": "Remarks Column Length"
},
{
"default": "Payment",
"description": "Only applies for Normal Payments",
"fieldname": "exchange_gain_loss_posting_date",
"fieldtype": "Select",
"label": "Posting Date Inheritance for Exchange Gain / Loss",
"options": "Invoice\nPayment\nReconciliation Date"
},
{
"default": "0",
"description": "Ignores legacy Is Opening field in GL Entry that allows adding opening balance post the system is in use while generating reports",
"fieldname": "ignore_is_opening_check_for_reporting",
"fieldtype": "Check",
"label": "Ignore Is Opening check for reporting"
},
{
"default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"options": "Buffered Cursor\nUnBuffered Cursor\nRaw SQL"
},
{
"fieldname": "accounts_receivable_payable_tuning_section",
"fieldtype": "Section Break",
"label": "Accounts Receivable / Payable Tuning"
},
{
"fieldname": "legacy_section",
"fieldtype": "Section Break",
"label": "Legacy Fields"
},
{
"fieldname": "column_break_ntmi",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.receivable_payable_fetch_method == \"Raw SQL\"",
"description": "Drops existing SQL Procedures and Function setup by Accounts Receivable report",
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
}
],
"icon": "icon-cog",
@@ -519,7 +469,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-05 12:29:38.302027",
"modified": "2024-01-22 12:10:10.151819",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -65,11 +65,3 @@ class AccountsSettings(Document):
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
frappe.db.sql(f"drop function if exists {InitSQLProceduresForAR.genkey_function_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.init_procedure_name}")
frappe.db.sql(f"drop procedure if exists {InitSQLProceduresForAR.allocate_procedure_name}")

View File

@@ -224,6 +224,11 @@
"link_doctype": "Bank Guarantee",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Payroll Entry",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Bank Transaction",
@@ -250,7 +255,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2024-10-30 09:41:14.113414",
"modified": "2024-09-24 06:57:41.292970",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",

View File

@@ -21,7 +21,7 @@ class BankAccount(Document):
self.name = self.account_name + " - " + self.bank
def on_trash(self):
delete_contact_and_address("Bank Account", self.name)
delete_contact_and_address("BankAccount", self.name)
def validate(self):
self.validate_company()

View File

@@ -46,6 +46,9 @@ class BankClearance(Document):
as_dict=1,
)
if self.bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
f"""
select
@@ -67,6 +70,7 @@ class BankClearance(Document):
"account": self.account,
"from": self.from_date,
"to": self.to_date,
"bank_account": self.bank_account,
},
as_dict=1,
)
@@ -89,7 +93,7 @@ class BankClearance(Document):
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.disbursement_account == self.account)
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
@@ -117,7 +121,7 @@ class BankClearance(Document):
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account == self.account)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
if not self.include_reconciled_entries:

View File

@@ -9,6 +9,13 @@ cur_frm.add_fetch("bank", "swift_number", "swift_number");
frappe.ui.form.on("Bank Guarantee", {
setup: function (frm) {
frm.set_query("bank", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("bank_account", function () {
return {
filters: {

View File

@@ -19,15 +19,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
onload: function (frm) {
if (!frm.doc.company) {
frm.set_value("company", frappe.defaults.get_default("company"));
}
// Set default filter dates
let today = frappe.datetime.get_today();
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
frm.doc.bank_statement_to_date = today;
frm.trigger("bank_account");
},
@@ -99,7 +94,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
make_reconciliation_tool(frm) {
frm.get_field("reconciliation_tool_cards").$wrapper.empty();
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
frm.trigger("get_cleared_balance").then(() => {
if (
frm.doc.bank_account &&
@@ -115,7 +110,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
get_account_opening_balance(frm) {
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_from_date) {
if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: {
@@ -130,7 +125,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
get_cleared_balance(frm) {
if (frm.doc.company && frm.doc.bank_account && frm.doc.bank_statement_to_date) {
if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
return frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
args: {

View File

@@ -12,7 +12,6 @@ from frappe.utils import cint, flt
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
get_entries,
@@ -285,56 +284,54 @@ def create_payment_entry_bts(
bank_transaction = frappe.db.get_values(
"Bank Transaction",
bank_transaction_name,
fieldname=["name", "unallocated_amount", "deposit", "bank_account", "currency"],
fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_cached_value("Account", bank_account, "company")
party_account = get_party_account(party_type, party, company)
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = {
"company": company,
"payment_type": payment_type,
"reference_no": reference_number,
"reference_date": reference_date,
"party_type": party_type,
"party": party,
"posting_date": posting_date,
"paid_amount": paid_amount,
"received_amount": paid_amount,
}
payment_entry = frappe.new_doc("Payment Entry")
bank_currency = bank_transaction.currency
party_currency = frappe.get_cached_value("Account", party_account, "account_currency")
payment_entry.update(payment_entry_dict)
exc_rate = get_exchange_rate(bank_currency, party_currency, posting_date)
if mode_of_payment:
payment_entry.mode_of_payment = mode_of_payment
if project:
payment_entry.project = project
if cost_center:
payment_entry.cost_center = cost_center
if payment_type == "Receive":
payment_entry.paid_to = company_account
else:
payment_entry.paid_from = company_account
amt_in_bank_acc_currency = bank_transaction.unallocated_amount
amount_in_party_currency = bank_transaction.unallocated_amount * exc_rate
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
pe.company = company
pe.reference_no = reference_number
pe.reference_date = reference_date
pe.party_type = party_type
pe.party = party
pe.posting_date = posting_date
pe.paid_from = party_account if payment_type == "Receive" else bank_account
pe.paid_to = party_account if payment_type == "Pay" else bank_account
pe.paid_from_account_currency = party_currency if payment_type == "Receive" else bank_currency
pe.paid_to_account_currency = party_currency if payment_type == "Pay" else bank_currency
pe.paid_amount = amount_in_party_currency if payment_type == "Receive" else amt_in_bank_acc_currency
pe.received_amount = amount_in_party_currency if payment_type == "Pay" else amt_in_bank_acc_currency
pe.mode_of_payment = mode_of_payment
pe.project = project
pe.cost_center = cost_center
pe.validate()
payment_entry.validate()
if allow_edit:
return pe
return payment_entry
pe.insert()
pe.submit()
payment_entry.insert()
payment_entry.submit()
vouchers = json.dumps(
[
{
"payment_doctype": "Payment Entry",
"payment_name": pe.name,
"amount": amt_in_bank_acc_currency,
"payment_name": payment_entry.name,
"amount": paid_amount,
}
]
)
@@ -458,12 +455,8 @@ def get_linked_payments(
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
voucher_docs = [(voucher[1], voucher[2]) for voucher in vouchers]
voucher_allocated_amounts = get_total_allocated_amount(voucher_docs)
for voucher in vouchers:
rows = voucher_allocated_amounts.get((voucher[1], voucher[2])) or []
rows = get_total_allocated_amount(voucher[1], voucher[2])
amount = None
for row in rows:
if row["gl_account"] == gl_account:

View File

@@ -45,41 +45,42 @@ class AutoMatchbyAccountIBAN:
if not (self.bank_party_account_number or self.bank_party_iban):
return None
return self.match_account_in_party()
result = self.match_account_in_party()
return result
def match_account_in_party(self) -> tuple | None:
"""
Returns (Party Type, Party) if a matching account is found in Bank Account or Employee:
1. Get party from a matching (iban/account no) Bank Account
2. If not found, get party from Employee with matching bank account details (iban/account no)
"""
if not (self.bank_party_account_number or self.bank_party_iban):
# Nothing to match
return None
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
result = None
parties = get_parties_in_order(self.deposit)
or_filters = self.get_or_filters()
# Search for a matching Bank Account that has party set
party_result = frappe.db.get_all(
"Bank Account",
or_filters=self.get_or_filters(),
filters={"party_type": ("is", "set"), "party": ("is", "set")},
fields=["party", "party_type"],
limit_page_length=1,
)
if result := party_result[0] if party_result else None:
return (result["party_type"], result["party"])
for party in parties:
party_result = frappe.db.get_all(
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
)
# If no party is found, search in Employee (since it has bank account details)
if employee_result := frappe.db.get_all(
"Employee", or_filters=self.get_or_filters("Employee"), pluck="name", limit_page_length=1
):
return ("Employee", employee_result[0])
if party == "Employee" and not party_result:
# Search in Bank Accounts first for Employee, and then Employee record
if "bank_account_no" in or_filters:
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
def get_or_filters(self, party: str | None = None) -> dict:
"""Return OR filters for Bank Account and IBAN"""
party_result = frappe.db.get_all(
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
if party_result:
result = (
party,
party_result[0],
)
break
return result
def get_or_filters(self) -> dict:
or_filters = {}
if self.bank_party_account_number:
bank_ac_field = "bank_ac_no" if party == "Employee" else "bank_account_no"
or_filters[bank_ac_field] = self.bank_party_account_number
or_filters["bank_account_no"] = self.bank_party_account_number
if self.bank_party_iban:
or_filters["iban"] = self.bank_party_iban
@@ -99,7 +100,8 @@ class AutoMatchbyPartyNameDescription:
if not (self.bank_party_name or self.description):
return None
return self.match_party_name_desc_in_party()
result = self.match_party_name_desc_in_party()
return result
def match_party_name_desc_in_party(self) -> tuple | None:
"""Fuzzy search party name and/or description against parties in the system"""
@@ -108,7 +110,7 @@ class AutoMatchbyPartyNameDescription:
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
field = f"{party.lower()}_name"
field = party.lower() + "_name"
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
for field in ["bank_party_name", "description"]:
@@ -135,7 +137,13 @@ class AutoMatchbyPartyNameDescription:
)
party_name, skip = self.process_fuzzy_result(result)
return ((party, party_name), skip) if party_name else (None, skip)
if not party_name:
return None, skip
return (
party,
party_name,
), skip
def process_fuzzy_result(self, result: list | None):
"""
@@ -153,8 +161,8 @@ class AutoMatchbyPartyNameDescription:
if len(result) == 1:
return (first_result[PARTY_ID] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
second_result = result[1]
# If multiple matches with the same score, return None but discontinue matching
# Matches were found but were too close to distinguish between
if first_result[SCORE] == second_result[SCORE]:
@@ -166,8 +174,8 @@ class AutoMatchbyPartyNameDescription:
def get_parties_in_order(deposit: float) -> list:
return (
["Customer", "Supplier", "Employee"] # most -> least likely to pay us
if flt(deposit) > 0
else ["Supplier", "Employee", "Customer"] # most -> least likely to receive from us
)
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
if flt(deposit) > 0:
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
return parties

View File

@@ -2,7 +2,6 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.docstatus import DocStatus
from frappe.utils import flt
@@ -93,16 +92,10 @@ class BankTransaction(StatusUpdater):
- clear means: set the latest transaction date as clearance date
"""
remaining_amount = self.unallocated_amount
payment_entry_docs = [(pe.payment_document, pe.payment_entry) for pe in self.payment_entries]
pe_bt_allocations = get_total_allocated_amount(payment_entry_docs)
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
self,
payment_entry,
pe_bt_allocations.get((payment_entry.payment_document, payment_entry.payment_entry))
or [],
self, payment_entry
)
if 0.0 == unallocated_amount:
@@ -163,17 +156,13 @@ class BankTransaction(StatusUpdater):
if self.party_type and self.party:
return
result = None
try:
result = AutoMatchParty(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
bank_party_name=self.bank_party_name,
description=self.description,
deposit=self.deposit,
).match()
except Exception:
frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name))
result = AutoMatchParty(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
bank_party_name=self.bank_party_name,
description=self.description,
deposit=self.deposit,
).match()
if result:
party_type, party = result
@@ -188,7 +177,7 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_clearance_details(transaction, payment_entry, bt_allocations):
def get_clearance_details(transaction, payment_entry):
"""
There should only be one bank gle for a voucher.
Could be none for a Bank Transaction.
@@ -197,6 +186,7 @@ def get_clearance_details(transaction, payment_entry, bt_allocations):
"""
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
unallocated_amount = min(
transaction.unallocated_amount,
@@ -252,52 +242,44 @@ def get_related_bank_gl_entries(doctype, docname):
return result
def get_total_allocated_amount(docs):
def get_total_allocated_amount(doctype, docname):
"""
Gets the sum of allocations for a voucher on each bank GL account
along with the latest bank transaction name & date
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
"""
if not docs:
return {}
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT total, latest_name, latest_date, gl_account, payment_document, payment_entry FROM (
SELECT total, latest_name, latest_date, gl_account FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account, btp.payment_document, btp.payment_entry) AS total,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
FIRST_VALUE(bt.name) OVER w AS latest_name,
FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account,
btp.payment_document,
btp.payment_entry
ba.account AS gl_account
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
(btp.payment_document, btp.payment_entry) IN %(docs)s
btp.payment_document = %(doctype)s
AND btp.payment_entry = %(docname)s
AND bt.docstatus = 1
WINDOW w AS (PARTITION BY ba.account, btp.payment_document, btp.payment_entry ORDER BY bt.date DESC)
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
) temp
WHERE
rownum = 1
""",
dict(docs=docs),
dict(doctype=doctype, docname=docname),
as_dict=True,
)
payment_allocation_details = {}
for row in result:
# Why is this *sometimes* a byte string?
if isinstance(row["latest_name"], bytes):
row["latest_name"] = row["latest_name"].decode()
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
payment_allocation_details.setdefault((row["payment_document"], row["payment_entry"]), []).append(row)
return payment_allocation_details
return result
def get_paid_amount(payment_entry, currency, gl_bank_account):

View File

@@ -460,20 +460,13 @@ def get_actual_expense(args):
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
if monthly_distribution:
mdp = frappe.qb.DocType("Monthly Distribution Percentage")
md = frappe.qb.DocType("Monthly Distribution")
res = (
frappe.qb.from_(mdp)
.join(md)
.on(mdp.parent == md.name)
.select(mdp.month, mdp.percentage_allocation)
.where(md.fiscal_year == fiscal_year)
.where(md.name == monthly_distribution)
.run(as_dict=True)
)
for d in res:
for d in frappe.db.sql(
"""select mdp.month, mdp.percentage_allocation
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
where mdp.parent=md.name and md.fiscal_year=%s""",
fiscal_year,
as_dict=1,
):
distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")

View File

@@ -6,11 +6,7 @@ import unittest
import frappe
from frappe.utils import now_datetime, nowdate
from erpnext.accounts.doctype.budget.budget import (
BudgetError,
get_accumulated_monthly_budget,
get_actual_expense,
)
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -100,10 +96,6 @@ class TestBudget(unittest.TestCase):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
mr = frappe.get_doc(
{
"doctype": "Material Request",
@@ -117,7 +109,7 @@ class TestBudget(unittest.TestCase):
"uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC",
"schedule_date": nowdate(),
"rate": accumulated_limit + 1,
"rate": 100000,
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
}

View File

@@ -448,8 +448,9 @@ def unset_existing_data(company):
"Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template",
]:
dt = frappe.qb.DocType(doctype)
frappe.qb.from_(dt).where(dt.company == company).delete().run()
frappe.db.sql(
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
)
def set_default_accounts(company):

View File

@@ -4,8 +4,6 @@
import unittest
import frappe
from frappe.query_builder.functions import Sum
from frappe.tests.utils import change_settings
from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
@@ -192,31 +190,6 @@ class TestCostCenterAllocation(unittest.TestCase):
coa2.cancel()
jv.cancel()
@change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
cca = create_cost_center_allocation(
"_Test Company",
"Main Cost Center 1 - _TC",
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
)
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
gl_entry = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gl_entry)
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
.where(gl_entry.voucher_type == "Sales Invoice")
.where(gl_entry.voucher_no == si.name)
).run(as_dict=1)
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
si.cancel()
cca.cancel()
def create_cost_center_allocation(
company,

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
} else if (frm.doc.service_provider == "frankfurter.app") {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-23 21:16:41.896424",
"modified": "2024-03-18 08:32:26.895076",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -141,8 +141,8 @@
"write": 1
}
],
"sort_field": "creation",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -35,7 +35,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
elif self.service_provider == "frankfurter.app":
self.set("result_key", [])
self.set("req_params", [])
@@ -80,13 +80,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -55,46 +55,46 @@ class Dunning(AccountsController):
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
debit = {
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}
credit = {
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"credit_in_account_currency": self.dunning_amount,
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}
for dimension in accounting_dimensions:
if val := inv.get(dimension):
debit[dimension] = credit[dimension] = val
gl_entries = [
self.get_gl_dict(debit, inv.party_account_currency, item=inv),
self.get_gl_dict(credit, item=inv),
]
gl_entries.append(
self.get_gl_dict(
{
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
},
inv.party_account_currency,
item=inv,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"cost_center": inv.cost_center or default_cost_center,
"credit_in_account_currency": self.dunning_amount,
"project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)

View File

@@ -52,21 +52,6 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def before_submit(self):
self.remove_accounts_without_gain_loss()
def remove_accounts_without_gain_loss(self):
self.accounts = [account for account in self.accounts if account.gain_loss]
if not self.accounts:
frappe.throw(_("At least one account with exchange gain or loss is required"))
frappe.msgprint(
_("Removing rows without exchange gain or loss"),
alert=True,
indicator="yellow",
)
def on_cancel(self):
self.ignore_linked_doctypes = "GL Entry"
@@ -241,23 +226,23 @@ class ExchangeRateRevaluation(Document):
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
}
)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
}
)
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
@@ -281,22 +266,23 @@ class ExchangeRateRevaluation(Document):
current_exchange_rate * d.balance_in_account_currency
)
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
return accounts

View File

@@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.model.naming import set_name_from_naming_options
from frappe.utils import flt, fmt_money, now
from frappe.utils import flt, fmt_money
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -83,7 +83,7 @@ class GLEntry(Document):
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
if not self.is_cancelled and not (self.party_type and self.party):
if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
frappe.throw(
@@ -261,7 +261,7 @@ def validate_balance_type(account, adv_adj=False):
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
from `tabGL Entry` where account = %s""",
account,
)[0][0]
@@ -405,7 +405,7 @@ def rename_temporarily_named_docs(doctype):
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name
frappe.db.sql(
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname),
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
(newname, oldname),
auto_commit=True,
)

View File

@@ -197,7 +197,7 @@ frappe.ui.form.on("Invoice Discounting", {
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "Categorize by Voucher (Consolidated)",
group_by: "Group by Voucher (Consolidated)",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -34,7 +34,7 @@ frappe.ui.form.on("Journal Entry", {
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
finance_book: frm.doc.finance_book,
categorize_by: "",
group_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -433,22 +433,8 @@ class JournalEntry(AccountsController):
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
customer_details = frappe._dict(
frappe.db.get_all(
"Customer Credit Limit",
filters={
"parent": ["in", customers],
"parenttype": ["=", "Customer"],
"company": ["=", self.company],
},
fields=["parent", "bypass_credit_limit_check"],
as_list=True,
)
)
for customer in customers:
ignore_outstanding_sales_order = bool(customer_details.get(customer))
check_credit_limit(customer, self.company, ignore_outstanding_sales_order)
check_credit_limit(customer, self.company)
def validate_cheque_info(self):
if self.voucher_type in ["Bank Entry"]:

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, today
@@ -19,29 +18,21 @@ def get_loyalty_details(
if not expiry_date:
expiry_date = today()
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
query = (
frappe.qb.from_(LoyaltyPointEntry)
.select(
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
)
.where(
(LoyaltyPointEntry.customer == customer)
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
& (LoyaltyPointEntry.posting_date <= expiry_date)
)
.groupby(LoyaltyPointEntry.customer)
)
condition = ""
if company:
query = query.where(LoyaltyPointEntry.company == company)
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
condition += " and expiry_date>='%s' " % expiry_date
loyalty_point_details = query.run(as_dict=True)
loyalty_point_details = frappe.db.sql(
f"""select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
group by customer""",
(customer, loyalty_program, expiry_date),
as_dict=1,
)
if loyalty_point_details:
return loyalty_point_details[0]

View File

@@ -60,6 +60,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
party_link.save()
party_link.save(ignore_permissions=True)
return party_link

View File

@@ -15,10 +15,6 @@ frappe.ui.form.on('Payment Entry', {
}
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
if (frm.is_new()) {
set_default_party_type(frm);
}
},
setup: function(frm) {
@@ -165,10 +161,6 @@ frappe.ui.form.on('Payment Entry', {
filters: {
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
company: doc.company,
status: ["!=", "Paid"],
outstanding_amount: [">", 0], // for compatibility with old data
docstatus: 1,
},
};
});
@@ -319,7 +311,7 @@ frappe.ui.form.on('Payment Entry', {
"from_date": frm.doc.posting_date,
"to_date": moment(frm.doc.modified).format('YYYY-MM-DD'),
"company": frm.doc.company,
"categorize_by": "",
"group_by": "",
"show_cancelled_entries": frm.doc.docstatus === 2
};
frappe.set_route("query-report", "General Ledger");
@@ -328,9 +320,8 @@ frappe.ui.form.on('Payment Entry', {
},
payment_type: function(frm) {
set_default_party_type(frm);
if(frm.doc.payment_type == "Internal Transfer") {
$.each(["party", "party_type", "party_balance", "paid_from", "paid_to",
$.each(["party", "party_balance", "paid_from", "paid_to",
"references", "total_allocated_amount"], function(i, field) {
frm.set_value(field, null);
});
@@ -1088,24 +1079,6 @@ frappe.ui.form.on('Payment Entry', {
if (r.message) {
if (!frm.doc.mode_of_payment) {
frm.set_value(field, r.message.account);
} else {
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Mode of Payment Account",
filters: {
parent: frm.doc.mode_of_payment,
company: frm.doc.company,
},
fieldname: "default_account",
parent: "Mode of Payment",
},
callback: function (res) {
if (!res.message.default_account) {
frm.set_value(field, r.message.account);
}
},
});
}
frm.set_value('bank', r.message.bank);
frm.set_value('bank_account_no', r.message.bank_account_no);
@@ -1538,16 +1511,3 @@ frappe.ui.form.on('Payment Entry Deduction', {
frm.events.set_unallocated_amount(frm);
},
});
function set_default_party_type(frm) {
if (frm.doc.party) return;
let party_type;
if (frm.doc.payment_type == "Receive") {
party_type = "Customer";
} else if (frm.doc.payment_type == "Pay") {
party_type = "Supplier";
}
if (party_type) frm.set_value("party_type", party_type);
}

View File

@@ -350,25 +350,15 @@ class PaymentEntry(AccountsController):
self.set(self.party_account_field, party_account)
self.party_account = party_account
if self.paid_from and (
not self.paid_from_account_currency
or not self.paid_from_account_balance
or not self.paid_from_account_type
):
if self.paid_from and not (self.paid_from_account_currency or self.paid_from_account_balance):
acc = get_account_details(self.paid_from, self.posting_date, self.cost_center)
self.paid_from_account_currency = acc.account_currency
self.paid_from_account_balance = acc.account_balance
self.paid_from_account_type = acc.account_type
if self.paid_to and (
not self.paid_to_account_currency
or not self.paid_to_account_balance
or not self.paid_to_account_type
):
if self.paid_to and not (self.paid_to_account_currency or self.paid_to_account_balance):
acc = get_account_details(self.paid_to, self.posting_date, self.cost_center)
self.paid_to_account_currency = acc.account_currency
self.paid_to_account_balance = acc.account_balance
self.paid_to_account_type = acc.account_type
self.party_account_currency = (
self.paid_from_account_currency
@@ -475,7 +465,7 @@ class PaymentEntry(AccountsController):
if d.reference_doctype not in valid_reference_doctypes:
frappe.throw(
_("Reference Doctype must be one of {0}").format(
comma_or([_(d) for d in valid_reference_doctypes])
comma_or(_(d) for d in valid_reference_doctypes)
)
)
@@ -1529,11 +1519,11 @@ class PaymentEntry(AccountsController):
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Customer"):
elif self.party_type in ("Supplier", "Employee"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
_("Cannot {0} from {1} without any negative outstanding invoice").format(
_("Cannot {0} from {2} without any negative outstanding invoice").format(
self.payment_type,
self.party_type,
)
@@ -1590,7 +1580,7 @@ class PaymentEntry(AccountsController):
# Re allocate amount to those references which have PR set (Higher priority)
for ref in self.references:
if not (ref.reference_doctype and ref.reference_name and ref.payment_request):
if not ref.payment_request:
continue
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
@@ -1641,7 +1631,7 @@ class PaymentEntry(AccountsController):
)
# Re allocate amount to those references which have no PR (Lower priority)
for ref in self.references:
if ref.payment_request or not (ref.reference_doctype and ref.reference_name):
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
@@ -2491,7 +2481,6 @@ def get_payment_entry(
pe.paid_amount = paid_amount
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
pe.bank_account = frappe.db.get_value("Bank Account", {"is_company_account": 1, "is_default": 1}, "name")
if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
pe.project = doc.get("project") or reduce(
@@ -2616,7 +2605,6 @@ def get_open_payment_requests_for_references(references=None):
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
).run(as_dict=True)

View File

@@ -144,14 +144,12 @@ class PaymentReconciliation(Document):
if self.get("cost_center"):
conditions.append(jea.cost_center == self.cost_center)
account_type = erpnext.get_party_account_type(self.party_type)
if account_type == "Receivable":
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
elif account_type == "Payable":
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
conditions.append(dr_or_cr.gt(0))
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
conditions.append(jea[dr_or_cr].gt(0))
if self.bank_cash_account:
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
@@ -166,7 +164,7 @@ class PaymentReconciliation(Document):
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
dr_or_cr.as_("amount"),
jea[dr_or_cr].as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),
@@ -270,7 +268,6 @@ class PaymentReconciliation(Document):
for payment in non_reconciled_payments:
row = self.append("payments", {})
row.update(payment)
row.is_advance = payment.book_advance_payments_in_separate_party_account
def get_invoice_entries(self):
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
@@ -302,10 +299,6 @@ class PaymentReconciliation(Document):
if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
non_reconciled_invoices = sorted(
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
)
self.add_invoice_entries(non_reconciled_invoices)
def add_invoice_entries(self, non_reconciled_invoices):
@@ -355,9 +348,6 @@ class PaymentReconciliation(Document):
def allocate_entries(self, args):
self.validate_entries()
exc_gain_loss_posting_date = frappe.db.get_single_value(
"Accounts Settings", "exchange_gain_loss_posting_date", cache=True
)
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
@@ -384,11 +374,6 @@ class PaymentReconciliation(Document):
res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate")
res.update({"gain_loss_posting_date": pay.get("posting_date")})
if not pay.get("is_advance"):
if exc_gain_loss_posting_date == "Invoice":
res.update({"gain_loss_posting_date": inv.get("invoice_date")})
elif exc_gain_loss_posting_date == "Reconciliation Date":
res.update({"gain_loss_posting_date": nowdate()})
if pay.get("amount") == 0:
entries.append(res)

View File

@@ -615,42 +615,6 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_negative_debit_or_credit_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
# credit debtors account to record a payment
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = self.customer
je.accounts[1].credit_in_account_currency = 0
je.accounts[1].debit_in_account_currency = -1 * amount
je.save()
je.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
@@ -973,100 +937,6 @@ class TestPaymentReconciliation(FrappeTestCase):
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = -8000
je1.accounts[0].credit = -8000
je1.accounts[0].debit_in_account_currency = 0
je1.accounts[0].debit = 0
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = -16000
je2.accounts[0].credit = -16000
je2.accounts[0].debit_in_account_currency = 0
je2.accounts[0].debit = 0
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
# Test exact payment allocation
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Test partial payment allocation (with excess payment entry)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount",
group_by="reference_name",
)[0].amount
# total credit includes the exchange gain/loss amount
self.assertEqual(flt(total_credit_amount, 2), 8500)
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(

View File

@@ -246,7 +246,6 @@ class PaymentRequest(Document):
"payer_name": data.customer_name,
"order_id": self.name,
"currency": self.currency,
"payment_gateway": self.payment_gateway,
}
)
@@ -839,17 +838,21 @@ def validate_payment(doc, method=None):
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
# permission checks in `get_list()`
filters = frappe._dict(filters)
reference_doctype = filters.get("reference_doctype")
reference_name = filters.get("reference_doctype")
if not filters.reference_doctype or not filters.reference_name:
if not reference_doctype or not reference_name:
return []
if txt:
filters.name = ["like", f"%{txt}%"]
open_payment_requests = frappe.get_list(
"Payment Request",
filters=filters,
filters={
"reference_doctype": filters["reference_doctype"],
"reference_name": filters["reference_name"],
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
},
fields=["name", "grand_total", "outstanding_amount"],
order_by="transaction_date ASC,creation ASC",
)

View File

@@ -1,14 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "payment_request",
"internal_links": {
"Payment Entry": ["references", "payment_request"],
"Payment Order": ["references", "payment_order"],
},
"transactions": [
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
],
}

View File

@@ -1,18 +1,19 @@
const INDICATORS = {
"Partially Paid": "orange",
Cancelled: "red",
Draft: "gray",
Failed: "red",
Initiated: "green",
Paid: "blue",
Requested: "green",
};
frappe.listview_settings["Payment Request"] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (!doc.status || !INDICATORS[doc.status]) return;
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
if (doc.status == "Draft") {
return [__("Draft"), "gray", "status,=,Draft"];
}
if (doc.status == "Requested") {
return [__("Requested"), "green", "status,=,Requested"];
} else if (doc.status == "Initiated") {
return [__("Initiated"), "green", "status,=,Initiated"];
} else if (doc.status == "Partially Paid") {
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
} else if (doc.status == "Paid") {
return [__("Paid"), "blue", "status,=,Paid"];
} else if (doc.status == "Cancelled") {
return [__("Cancelled"), "red", "status,=,Cancelled"];
}
},
};

View File

@@ -10,19 +10,14 @@
"description",
"section_break_4",
"due_date",
"invoice_portion",
"mode_of_payment",
"column_break_5",
"due_date_based_on",
"credit_days",
"credit_months",
"invoice_portion",
"section_break_6",
"discount_date",
"discount",
"discount_type",
"discount_date",
"column_break_9",
"discount_validity_based_on",
"discount_validity",
"discount",
"section_break_9",
"payment_amount",
"outstanding",
@@ -160,50 +155,12 @@
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fieldname": "due_date_based_on",
"fieldtype": "Select",
"label": "Due Date Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
"fieldname": "credit_days",
"fieldtype": "Int",
"label": "Credit Days",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
"fieldname": "credit_months",
"fieldtype": "Int",
"label": "Credit Months",
"non_negative": 1,
"read_only": 1
},
{
"depends_on": "discount",
"fieldname": "discount_validity_based_on",
"fieldtype": "Select",
"label": "Discount Validity Based On",
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
"read_only": 1
},
{
"depends_on": "discount_validity_based_on",
"fieldname": "discount_validity",
"fieldtype": "Int",
"label": "Discount Validity",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-07-31 08:38:25.820701",
"modified": "2022-09-16 13:57:06.382859",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@@ -214,4 +171,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -161,4 +161,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -11,9 +11,9 @@ frappe.ui.form.on("Period Closing Voucher", {
return {
filters: [
["Account", "company", "=", frm.doc.company],
["Account", "is_group", "=", 0],
["Account", "is_group", "=", "0"],
["Account", "freeze_account", "=", "No"],
["Account", "root_type", "in", ["Liability", "Equity"]],
["Account", "root_type", "in", "Liability, Equity"],
],
};
});
@@ -29,7 +29,7 @@ frappe.ui.form.on("Period Closing Voucher", {
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
group_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");

View File

@@ -32,13 +32,8 @@ class PeriodClosingVoucher(AccountsController):
def on_cancel(self):
self.validate_future_closing_vouchers()
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Account Closing Balance",
)
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
@@ -206,9 +201,6 @@ class PeriodClosingVoucher(AccountsController):
return gl_entry
def get_gle_for_closing_account(self, acc):
debit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0
credit = abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0
gl_entry = self.get_gl_dict(
{
"company": self.company,
@@ -217,10 +209,16 @@ class PeriodClosingVoucher(AccountsController):
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": debit,
"debit": debit,
"credit_in_account_currency": credit,
"credit": credit,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
"is_period_closing_voucher_entry": 1,
},
item=acc,

View File

@@ -48,7 +48,6 @@
"shipping_address",
"company_address",
"company_address_display",
"company_contact_person",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -1558,19 +1557,12 @@
"fieldname": "update_billed_amount_in_delivery_note",
"fieldtype": "Check",
"label": "Update Billed Amount in Delivery Note"
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2024-11-26 13:10:50.309570",
"modified": "2024-03-20 16:00:34.268756",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1623,5 +1615,6 @@
"states": [],
"timeline_field": "customer",
"title_field": "title",
"track_changes": 1
}
"track_changes": 1,
"track_seen": 1
}

View File

@@ -560,13 +560,7 @@ class POSInvoice(SalesInvoice):
"Account", self.debit_to, "account_currency", cache=True
)
if not self.due_date and self.customer:
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super(SalesInvoice, self).set_missing_values(for_validate)

View File

@@ -389,9 +389,7 @@ def split_invoices(invoices):
if not item.serial_no:
continue
return_against_is_added = any(
d for d in _invoices if d and d[0].pos_invoice == pos_invoice.return_against
)
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
if return_against_is_added:
break

View File

@@ -53,7 +53,6 @@
"column_break_42",
"free_item_uom",
"round_free_qty",
"dont_enforce_free_item_qty",
"is_recursive",
"recurse_for",
"apply_recursion_over",
@@ -644,19 +643,12 @@
"fieldname": "has_priority",
"fieldtype": "Check",
"label": "Has Priority"
},
{
"default": "0",
"depends_on": "eval:doc.price_or_product_discount == 'Product'",
"fieldname": "dont_enforce_free_item_qty",
"fieldtype": "Check",
"label": "Don't Enforce Free Item Qty"
}
],
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2025-02-17 18:15:39.824639",
"modified": "2024-09-16 18:14:51.314765",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@@ -359,20 +359,7 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
if isinstance(pricing_rule, str):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
update_pricing_rule_uom(pricing_rule, args)
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
pricing_rule.apply_rule_on_other_items = (
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
)
if pricing_rule.coupon_code_based == 1:
if not args.coupon_code:
return item_details
coupon_code = frappe.db.get_value(
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
)
if args.coupon_code != coupon_code:
continue
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
if pricing_rule.get("suggestion"):
continue
@@ -399,6 +386,9 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
pricing_rule.apply_rule_on_other_items
)
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
return item_details
if not pricing_rule.validate_applied_rule:
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)
@@ -553,7 +543,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
if pricing_rule.margin_type in ["Percentage", "Amount"]:
item_details.margin_rate_or_amount = 0.0
item_details.margin_type = None
elif pricing_rule.get("free_item") and not pricing_rule.get("dont_enforce_free_item_qty"):
elif pricing_rule.get("free_item"):
item_details.remove_free_item = (
item_code if pricing_rule.get("same_item") else pricing_rule.get("free_item")
)

View File

@@ -428,54 +428,6 @@ class TestPricingRule(FrappeTestCase):
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
def test_dont_enforce_free_item_qty(self):
# this test is only for testing non-enforcement as all other tests in this file already test with enforcement
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"min_qty": 0,
"max_qty": 7,
"discount_percentage": 17.5,
"price_or_product_discount": "Product",
"same_item": 0,
"free_item": "_Test Item 2",
"free_qty": 1,
"company": "_Test Company",
}
pricing_rule = frappe.get_doc(test_record.copy()).insert()
# With enforcement
so = make_sales_order(item_code="_Test Item", qty=1, do_not_submit=True)
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2")
# Test 1 : Saving a document with an item with pricing list without it's corresponding free item will cause it the free item to be refetched on save
so.items.pop(1)
so.save()
so.reload()
self.assertEqual(len(so.items), 2)
# Without enforcement
pricing_rule.dont_enforce_free_item_qty = 1
pricing_rule.save()
# Test 2 : Deleted free item will not be fetched again on save without enforcement
so.items.pop(1)
so.save()
so.reload()
self.assertEqual(len(so.items), 1)
def test_cumulative_pricing_rule(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Cumulative Pricing Rule")
test_record = {
@@ -1035,45 +987,6 @@ class TestPricingRule(FrappeTestCase):
so.save()
self.assertEqual(len(so.items), 1)
def test_pricing_rule_for_product_free_item_round_free_qty(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"currency": "USD",
"items": [
{
"item_code": "_Test Item",
}
],
"selling": 1,
"rate": 0,
"min_qty": 100,
"max_qty": 0,
"price_or_product_discount": "Product",
"same_item": 1,
"free_qty": 10,
"round_free_qty": 1,
"is_recursive": 1,
"recurse_for": 100,
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
# With pricing rule
so = make_sales_order(item_code="_Test Item", qty=100)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
so = make_sales_order(item_code="_Test Item", qty=150)
so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 10)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
@@ -1287,7 +1200,6 @@ def make_pricing_rule(**args):
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0,
"has_priority": args.has_priority or 0,
"enforce_free_item_qty": args.dont_enforce_free_item_qty or 0,
}
)

View File

@@ -642,7 +642,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if transaction_qty:
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
if pricing_rule.round_free_qty:
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
qty = math.floor(qty)
if not qty:
return
@@ -691,10 +691,7 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
args.pop((item.item_code, item.pricing_rules))
for free_item in args.values():
if doc.is_new() or not frappe.get_value(
"Pricing Rule", free_item["pricing_rules"], "dont_enforce_free_item_qty"
):
doc.append("items", free_item)
doc.append("items", free_item)
def get_pricing_rule_items(pr_doc, other_items=False) -> list:

View File

@@ -20,7 +20,6 @@
"is_advance",
"section_break_5",
"difference_amount",
"gain_loss_posting_date",
"column_break_7",
"difference_account",
"exchange_rate",
@@ -154,16 +153,11 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Reconciled"
},
{
"fieldname": "gain_loss_posting_date",
"fieldtype": "Date",
"label": "Difference Posting Date"
}
],
"istable": 1,
"links": [],
"modified": "2025-01-23 16:09:01.058574",
"modified": "2023-03-20 21:05:43.121945",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log Allocations",

View File

@@ -12,7 +12,7 @@
"posting_date",
"company",
"account",
"categorize_by",
"group_by",
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
@@ -172,6 +172,14 @@
"fieldtype": "Date",
"label": "Start Date"
},
{
"default": "Group by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "group_by",
"fieldtype": "Select",
"label": "Group By",
"options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
},
{
"depends_on": "eval: (doc.report == 'General Ledger');",
"fieldname": "currency",
@@ -387,18 +395,10 @@
"fieldname": "show_remarks",
"fieldtype": "Check",
"label": "Show Remarks"
},
{
"default": "Categorize by Voucher (Consolidated)",
"depends_on": "eval:(doc.report == 'General Ledger');",
"fieldname": "categorize_by",
"fieldtype": "Select",
"label": "Categorize By",
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
}
],
"links": [],
"modified": "2025-04-30 14:43:23.643006",
"modified": "2024-10-18 17:51:39.108481",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
@@ -433,4 +433,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -145,7 +145,7 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
"party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency,
"categorize_by": doc.categorize_by,
"group_by": doc.group_by,
"currency": doc.currency,
"project": [p.project_name for p in doc.project],
"show_opening_entries": 0,
@@ -177,21 +177,17 @@ def get_ar_filters(doc, entry):
def get_html(doc, filters, entry, col, res, ageing):
base_template_path = "frappe/www/printview.html"
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
if doc.report == "General Ledger":
template_path = (
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
)
process_soa_html = frappe.get_hooks("process_soa_html")
# fetching custom print format for Process Statement of Accounts
if process_soa_html and process_soa_html.get(doc.report):
template_path = process_soa_html[doc.report][-1]
template_path = (
"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
if doc.report == "General Ledger"
else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
)
if doc.letter_head:
from frappe.www.printview import get_letter_head
letter_head = get_letter_head(doc, 0)
html = frappe.render_template(
template_path,
{
@@ -207,6 +203,7 @@ def get_html(doc, filters, entry, col, res, ageing):
else None,
},
)
html = frappe.render_template(
base_template_path,
{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
@@ -265,12 +262,9 @@ def get_recipients_and_cc(customer, doc):
recipients = []
for clist in doc.customers:
if clist.customer == customer:
if clist.billing_email:
for email in clist.billing_email.split(","):
recipients.append(email.strip())
recipients.append(clist.billing_email)
if doc.primary_mandatory and clist.primary_email:
for email in clist.primary_email.split(","):
recipients.append(email.strip())
recipients.append(clist.primary_email)
cc = []
if doc.cc_to != "":
try:
@@ -449,7 +443,7 @@ def send_auto_email():
selected = frappe.get_list(
"Process Statement Of Accounts",
filters={"enable_auto_email": 1},
or_filters={"to_date": today(), "posting_date": today()},
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
)
for entry in selected:
send_emails(entry.name, from_scheduler=True)

View File

@@ -29,10 +29,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.frm.set_query("expense_account", "items", function () {
return {
query: "erpnext.controllers.queries.get_expense_account",
filters: {
company: doc.company,
disabled: 0,
},
filters: { company: doc.company },
};
});
}
@@ -305,11 +302,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return;
let payment_terms_template = this.frm.doc.payment_terms_template;
erpnext.utils.get_party_details(
this.frm,
"erpnext.accounts.party.get_party_details",
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{
posting_date: this.frm.doc.posting_date,
bill_date: this.frm.doc.bill_date,
@@ -327,14 +320,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1);
me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1);
// while duplicating, don't change payment terms
if (me.frm.doc.__run_link_triggers === false) {
me.frm.set_value("payment_terms_template", payment_terms_template);
me.frm.refresh_field("payment_terms_template");
}
}
);
})
}
apply_tds(frm) {
@@ -380,8 +366,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
hide_fields(this.frm.doc);
if(cint(this.frm.doc.is_paid)) {
this.frm.set_value("allocate_advances_automatically", 0);
this.frm.set_value("payment_terms_template", "");
this.frm.set_value("payment_schedule", []);
if(!this.frm.doc.company) {
this.frm.set_value("is_paid", 0)
frappe.msgprint(__("Please specify Company to proceed"));

View File

@@ -10,6 +10,7 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
validate_docs_for_voucher_types,
@@ -32,7 +33,7 @@ from erpnext.accounts.general_ledger import (
merge_similar_entries,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
@@ -173,12 +174,7 @@ class PurchaseInvoice(BuyingController):
)
if not self.due_date:
self.due_date = get_due_date(
self.posting_date,
"Supplier",
self.supplier,
self.company,
self.bill_date,
template_name=self.payment_terms_template,
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
@@ -665,12 +661,12 @@ class PurchaseInvoice(BuyingController):
def update_supplier_outstanding(self, update_outstanding):
if update_outstanding == "No":
update_voucher_outstanding(
voucher_type=self.doctype,
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
account=self.credit_to,
party_type="Supplier",
party=self.supplier,
update_outstanding_amt(
self.credit_to,
"Supplier",
self.supplier,
self.doctype,
self.return_against if cint(self.is_return) and self.return_against else self.name,
)
def get_gl_entries(self, warehouse_account=None):
@@ -786,7 +782,7 @@ class PurchaseInvoice(BuyingController):
self.get_provisional_accounts()
for item in self.get("items"):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
if item.item_code:
frappe.get_cached_value("Item", item.item_code, "asset_category")
@@ -1021,9 +1017,6 @@ class PurchaseInvoice(BuyingController):
def get_provisional_accounts(self):
self.provisional_accounts = frappe._dict()
linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt])
if not linked_purchase_receipts:
return
pr_items = frappe.get_all(
"Purchase Receipt Item",
filters={"parent": ("in", linked_purchase_receipts)},
@@ -1132,30 +1125,6 @@ class PurchaseInvoice(BuyingController):
warehouse_debit_amount = stock_amount
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
net_rate = item.base_net_amount
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
stock_adjustment_amt = stock_amount - warehouse_debit_amount
gl_entries.append(
self.get_gl_dict(
{
"account": cost_of_goods_sold_account,
"against": item.expense_account,
"debit": stock_adjustment_amt,
"debit_in_transaction_currency": stock_adjustment_amt / self.conversion_rate,
"remarks": self.get("remarks") or _("Stock Adjustment"),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
account_currency,
item=item,
)
)
return warehouse_debit_amount
def make_tax_gl_entries(self, gl_entries):
@@ -1474,12 +1443,7 @@ class PurchaseInvoice(BuyingController):
if pi:
pi = pi[0][0]
frappe.throw(
_("Supplier Invoice No exists in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", pi)
)
)
frappe.throw(_("Supplier Invoice No exists in Purchase Invoice {0}").format(pi))
def update_billing_status_in_pr(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_purchase_receipt:

View File

@@ -45,16 +45,12 @@ frappe.listview_settings["Purchase Invoice"] = {
},
onload: function (listview) {
if (frappe.model.can_create("Purchase Receipt")) {
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
});
}
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
});
}
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment Entry");
});
},
};

View File

@@ -1643,30 +1643,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
# Cost of Item is zero in Purchase Receipt
pr = make_purchase_receipt(qty=1, rate=0)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 0)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 150
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 150)
# Increase the cost of the item
pr = make_purchase_receipt(qty=1, rate=100)
@@ -1816,16 +1792,19 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
self.assertAlmostEqual(rate, 500)
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
def test_payment_allocation_for_payment_terms(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_pr_against_po,
create_purchase_order,
)
from erpnext.selling.doctype.sales_order.test_sales_order import (
automatically_fetch_payment_terms,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
automatically_fetch_payment_terms()
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
@@ -1849,8 +1828,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
1,
)
pi = make_pi_from_pr(pr.name)
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
automatically_fetch_payment_terms(enable=0)
frappe.db.set_value(
"Payment Terms Template",
"_Test Payment Term Template",
@@ -1980,78 +1960,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
self.assertRaises(frappe.ValidationError, dr_note.save)
def test_apply_discount_on_grand_total(self):
"""
To test if after applying discount on grand total,
the grand total is calculated correctly without any rounding errors
"""
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
invoice.append(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 21.39,
},
)
invoice.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"rate": 15.5,
},
)
# the grand total here will be 255.71
invoice.disable_rounded_total = 1
# apply discount on grand total to adjust the grand total to 255
invoice.discount_amount = 0.71
invoice.save()
# check if grand total is 496 and not something like 254.99 due to rounding errors
self.assertEqual(invoice.grand_total, 255)
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
"""
To test if after applying discount on grand total,
where the tax is calculated on previous row total, the grand total is calculated correctly
"""
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True)
invoice.extend(
"taxes",
[
{
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"tax_amount": 100,
},
{
"charge_type": "On Previous Row Amount",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"row_id": 1,
"rate": 10,
},
{
"charge_type": "On Previous Row Total",
"account_head": "_Test Account VAT - _TC",
"description": "VAT",
"row_id": 1,
"rate": 10,
},
],
)
# the total here will be 340, so applying 40 discount
invoice.discount_amount = 40
invoice.save()
self.assertEqual(invoice.grand_total, 300)
def check_gl_entries(
doc,

View File

@@ -14,8 +14,7 @@
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate",
"difference_posting_date"
"ref_exchange_rate"
],
"fields": [
{
@@ -31,7 +30,7 @@
"width": "180px"
},
{
"columns": 2,
"columns": 3,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
@@ -41,7 +40,7 @@
"read_only": 1
},
{
"columns": 2,
"columns": 3,
"fieldname": "remarks",
"fieldtype": "Text",
"in_list_view": 1,
@@ -112,20 +111,13 @@
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "difference_posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Difference Posting Date"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-12-20 12:04:46.729972",
"modified": "2021-09-26 15:47:28.167371",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Advance",

View File

@@ -6,8 +6,6 @@ from frappe import _, qb
from frappe.model.document import Document
from frappe.utils.data import comma_and
from erpnext.stock import get_warehouse_account_map
class RepostAccountingLedger(Document):
def __init__(self, *args, **kwargs):
@@ -29,7 +27,7 @@ class RepostAccountingLedger(Document):
latest_pcv = (
frappe.db.get_all(
"Period Closing Voucher",
filters={"company": self.company, "docstatus": 1},
filters={"company": self.company},
order_by="posting_date desc",
pluck="posting_date",
limit=1,
@@ -79,9 +77,6 @@ class RepostAccountingLedger(Document):
doc = frappe.get_doc(x.voucher_type, x.voucher_no)
if doc.doctype in ["Payment Entry", "Journal Entry"]:
gle_map = doc.build_gl_map()
elif doc.doctype == "Purchase Receipt":
warehouse_account_map = get_warehouse_account_map(doc.company)
gle_map = doc.get_gl_entries(warehouse_account_map)
else:
gle_map = doc.get_gl_entries()
@@ -151,7 +146,7 @@ def start_repost(account_repost_doc=str) -> None:
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel(from_repost=True)
doc.make_gl_entries_on_cancel()
doc.docstatus = 1
if doc.doctype == "Sales Invoice":
@@ -160,14 +155,6 @@ def start_repost(account_repost_doc=str) -> None:
doc.force_set_against_expense_account()
doc.make_gl_entries()
elif doc.doctype == "Purchase Receipt":
if not repost_doc.delete_cancelled_entries:
doc.docstatus = 2
doc.make_gl_entries_on_cancel(from_repost=True)
doc.docstatus = 1
doc.make_gl_entries(from_repost=True)
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
if not repost_doc.delete_cancelled_entries:
doc.make_gl_entries(1)

View File

@@ -12,8 +12,6 @@ from erpnext.accounts.doctype.payment_request.payment_request import make_paymen
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries, make_purchase_receipt
class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
@@ -204,81 +202,9 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_06_repost_purchase_receipt(self):
from erpnext.accounts.doctype.account.test_account import create_account
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
)
another_provisional_account = create_account(
account_name="Another Provision Account",
parent_account="Current Liabilities - _TC",
company=self.company,
)
company = frappe.get_doc("Company", self.company)
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
test_cc = company.cost_center
default_expense_account = company.default_expense_account
item = make_item(properties={"is_stock_item": 0})
pr = make_purchase_receipt(company=self.company, item_code=item.name, rate=1000.0, qty=1.0)
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles = [
{"account": provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
]
self.assertEqual(expected_pr_gles, pr_gl_entries)
# change the provisional account
frappe.db.set_value(
"Purchase Receipt Item",
pr.items[0].name,
"provisional_expense_account",
another_provisional_account,
)
repost_doc = frappe.new_doc("Repost Accounting Ledger")
repost_doc.company = self.company
repost_doc.delete_cancelled_entries = True
repost_doc.append("vouchers", {"voucher_type": pr.doctype, "voucher_no": pr.name})
repost_doc.save().submit()
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
expected_pr_gles_after_repost = [
{"account": default_expense_account, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
{"account": another_provisional_account, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
]
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
# teardown
repost_doc.cancel()
repost_doc.delete()
pr.reload()
pr.cancel()
company.enable_provisional_accounting_for_non_stock_items = 0
company.default_provisional_account = None
company.save()
def update_repost_settings():
allowed_types = [
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Purchase Receipt",
]
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})

View File

@@ -9,10 +9,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
setup(doc) {
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {
Dunning: this.make_dunning.bind(this),
"Invoice Discounting": this.make_invoice_discounting.bind(this),
};
}
company() {
super.company();
@@ -98,35 +94,26 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
}
if (doc.outstanding_amount > 0) {
this.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),
__("Create")
);
if (doc.outstanding_amount>0) {
cur_frm.add_custom_button(__('Payment Request'), function() {
me.make_payment_request();
}, __('Create'));
const payment_is_overdue = doc.payment_schedule
.map((row) => Date.parse(row.due_date) < Date.now())
.reduce((prev, current) => prev || current, false);
cur_frm.add_custom_button(__('Invoice Discounting'), function() {
cur_frm.events.create_invoice_discounting(cur_frm);
}, __('Create'));
if (payment_is_overdue) {
this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create"));
if (doc.due_date < frappe.datetime.get_today()) {
cur_frm.add_custom_button(__('Dunning'), function() {
cur_frm.events.create_dunning(cur_frm);
}, __('Create'));
}
}
if (doc.docstatus === 1) {
this.frm.add_custom_button(
__("Maintenance Schedule"),
this.make_maintenance_schedule.bind(this),
__("Create")
);
cur_frm.add_custom_button(__('Maintenance Schedule'), function () {
cur_frm.cscript.make_maintenance_schedule();
}, __('Create'));
}
if(!doc.auto_repeat) {
@@ -159,20 +146,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
}
make_invoice_discounting() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
frm: this.frm,
});
}
make_dunning() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
frm: this.frm,
});
}
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
@@ -694,6 +667,20 @@ frappe.ui.form.on('Sales Invoice', {
}
}
frm.set_query('company_address', function(doc) {
if(!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: {
link_doctype: 'Company',
link_name: doc.company
}
};
});
frm.set_query('pos_profile', function(doc) {
if(!doc.company) {
frappe.throw(_('Please set Company'));
@@ -975,6 +962,20 @@ frappe.ui.form.on('Sales Invoice', {
frm.set_df_property('return_against', 'label', __('Adjustment Against'));
}
},
create_invoice_discounting: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting",
frm: frm
});
},
create_dunning: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
frm: frm
});
}
});
frappe.ui.form.on("Sales Invoice Timesheet", {

View File

@@ -160,9 +160,8 @@
"dispatch_address",
"company_address_section",
"company_address",
"company_address_display",
"company_addr_col_break",
"company_contact_person",
"company_address_display",
"terms_tab",
"payment_schedule_section",
"ignore_default_payment_terms_template",
@@ -2172,13 +2171,6 @@
"label": "Update Outstanding for Self",
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "company_contact_person",
"fieldtype": "Link",
"label": "Company Contact Person",
"options": "Contact",
"print_hide": 1
}
],
"icon": "fa fa-file-text",
@@ -2191,7 +2183,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-11-26 12:34:09.110690",
"modified": "2024-07-18 15:30:39.428519",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -24,11 +24,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
update_voucher_outstanding,
)
from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_disposal_account_and_cost_center,
@@ -91,8 +87,8 @@ class SalesInvoice(SellingController):
self.indicator_title = _("Paid")
def validate(self):
self.validate_auto_set_posting_time()
super().validate()
self.validate_auto_set_posting_time()
if not (self.is_pos or self.is_debit_note):
self.so_dn_required()
@@ -498,13 +494,7 @@ class SalesInvoice(SellingController):
"Account", self.debit_to, "account_currency", cache=True
)
if not self.due_date and self.customer:
self.due_date = get_due_date(
self.posting_date,
"Customer",
self.customer,
self.company,
template_name=self.payment_terms_template,
)
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
super().set_missing_values(for_validate)
@@ -1025,17 +1015,18 @@ class SalesInvoice(SellingController):
self.make_exchange_gain_loss_journal()
elif self.docstatus == 2:
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":
update_voucher_outstanding(
voucher_type=self.doctype,
voucher_no=self.return_against
if cint(self.is_return) and self.return_against
else self.name,
account=self.debit_to,
party_type="Customer",
party=self.customer,
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
update_outstanding_amt(
self.debit_to,
"Customer",
self.customer,
self.doctype,
self.return_against if cint(self.is_return) and self.return_against else self.name,
)
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
@@ -1565,12 +1556,8 @@ class SalesInvoice(SellingController):
)
def update_project(self):
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
if self.project and self.project not in unique_projects:
unique_projects.append(self.project)
for p in unique_projects:
project = frappe.get_doc("Project", p)
if self.project:
project = frappe.get_doc("Project", self.project)
project.update_billed_amount()
project.db_update()

View File

@@ -32,16 +32,12 @@ frappe.listview_settings["Sales Invoice"] = {
right_column: "grand_total",
onload: function (listview) {
if (frappe.model.can_create("Delivery Note")) {
listview.page.add_action_item(__("Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
});
}
listview.page.add_action_item(__("Delivery Note"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
});
}
listview.page.add_action_item(__("Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment Entry");
});
},
};

View File

@@ -36,7 +36,6 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.get_item_details import get_item_tax_map
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
@@ -416,9 +415,9 @@ class TestSalesInvoice(FrappeTestCase):
for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.base_grand_total, 1500)
self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.rounding_adjustment, 0)
self.assertEqual(si.base_grand_total, 1500.01)
self.assertEqual(si.grand_total, 1500.01)
self.assertEqual(si.rounding_adjustment, -0.01)
def test_discount_amount_gl_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
@@ -1773,6 +1772,17 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue(gle)
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=1,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, si.save)
def test_invalid_currency(self):
# Customer currency = USD
@@ -2807,80 +2817,13 @@ class TestSalesInvoice(FrappeTestCase):
item.save()
sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True)
item_tax_map = get_item_tax_map(
company=sales_invoice.company,
item_tax_template=sales_invoice.items[0].item_tax_template,
)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
# Apply discount
sales_invoice.apply_discount_on = "Net Total"
sales_invoice.discount_amount = 300
sales_invoice.save()
item_tax_map = get_item_tax_map(
company=sales_invoice.company,
item_tax_template=sales_invoice.items[0].item_tax_template,
)
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
def test_item_tax_template_change_with_grand_total_discount(self):
"""
Test that when item tax template changes due to discount on Grand Total,
the tax calculations are consistent.
"""
item = create_item("Test Item With Multiple Tax Templates")
item.set("taxes", [])
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500,
},
)
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000,
},
)
item.save()
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Excise Duty - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"rate": 0,
},
)
si.insert()
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
si.apply_discount_on = "Grand Total"
si.discount_amount = 300
si.save()
# Verify template changed to 10%
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
si.submit()
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
@@ -3341,7 +3284,6 @@ class TestSalesInvoice(FrappeTestCase):
si.posting_date = getdate()
si.submit()
@change_settings("Accounts Settings", {"over_billing_allowance": 0})
def test_over_billing_case_against_delivery_note(self):
"""
Test a case where duplicating the item with qty = 1 in the invoice
@@ -3349,23 +3291,24 @@ class TestSalesInvoice(FrappeTestCase):
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
dn = create_delivery_note()
dn.submit()
si = make_sales_invoice(dn.name)
# make a copy of first item and add it to invoice
item_copy = frappe.copy_doc(si.items[0])
si.save()
si.items = [] # Clear existing items
si.append("items", item_copy)
si.save()
si.append("items", item_copy)
with self.assertRaises(frappe.ValidationError) as err:
si.save()
si.submit()
self.assertTrue("cannot overbill" in str(err.exception).lower())
dn.cancel()
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
@change_settings(
"Accounts Settings",
@@ -3817,186 +3760,6 @@ class TestSalesInvoice(FrappeTestCase):
self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total)
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.setup.utils import get_exchange_rate
creditors = create_account(
account_name="Creditors INR",
parent_account="Accounts Payable - _TC",
company="_Test Company",
account_currency="INR",
account_type="Payable",
)
debtors = create_account(
account_name="Debtors USD",
parent_account="Accounts Receivable - _TC",
company="_Test Company",
account_currency="USD",
account_type="Receivable",
)
# create a customer
customer = make_customer(customer="_Test Common Party USD")
cust_doc = frappe.get_doc("Customer", customer)
cust_doc.default_currency = "USD"
test_account_details = {
"company": "_Test Company",
"account": debtors,
}
cust_doc.append("accounts", test_account_details)
cust_doc.save()
# create a supplier
supplier = create_supplier(supplier_name="_Test Common Party INR").name
supp_doc = frappe.get_doc("Supplier", supplier)
supp_doc.default_currency = "INR"
test_account_details = {
"company": "_Test Company",
"account": creditors,
}
supp_doc.append("accounts", test_account_details)
supp_doc.save()
# create a party link between customer & supplier
create_party_link("Supplier", supplier, customer)
# create a sales invoice
si = create_sales_invoice(
customer=customer,
currency="USD",
conversion_rate=get_exchange_rate("USD", "INR"),
debit_to=debtors,
do_not_save=1,
)
si.party_account_currency = "USD"
si.save()
si.submit()
# check outstanding of sales invoice
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(flt(si.outstanding_amount), 0.0)
# check creation of journal entry
jv = frappe.get_all(
"Journal Entry Account",
{
"account": si.debit_to,
"party_type": "Customer",
"party": si.customer,
"reference_type": si.doctype,
"reference_name": si.name,
},
pluck="credit_in_account_currency",
)
self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total)
def test_total_billed_amount(self):
si = create_sales_invoice(do_not_submit=True)
project = frappe.new_doc("Project")
project.company = "_Test Company"
project.project_name = "Test Total Billed Amount"
project.save()
si.project = project.name
si.save()
si.submit()
doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total)
def test_create_return_invoice_for_self_update(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.controllers.sales_and_purchase_return import make_return_doc
invoice = create_sales_invoice()
payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
payment_entry.reference_no = "test001"
payment_entry.reference_date = getdate()
payment_entry.save()
payment_entry.submit()
r_invoice = make_return_doc(invoice.doctype, invoice.name)
r_invoice.update_outstanding_for_self = 0
r_invoice.save()
self.assertEqual(r_invoice.update_outstanding_for_self, 1)
r_invoice.submit()
self.assertNotEqual(r_invoice.outstanding_amount, 0)
invoice.reload()
self.assertEqual(invoice.outstanding_amount, 0)
def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
update_repost_settings,
)
update_repost_settings()
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=80,
)
pe = get_payment_entry("Sales Invoice", si.name)
pe.reference_no = "10"
pe.reference_date = nowdate()
pe.paid_from_account_currency = si.currency
pe.paid_to_account_currency = "INR"
pe.source_exchange_rate = 85
pe.target_exchange_rate = 1
pe.paid_amount = si.outstanding_amount
pe.received_amount = 8500
pe.insert()
pe.submit()
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = si.company
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.save()
ral.submit()
je = frappe.qb.DocType("Journal Entry")
jea = frappe.qb.DocType("Journal Entry Account")
q = (
(
frappe.qb.from_(je)
.join(jea)
.on(je.name == jea.parent)
.select(je.docstatus)
.where(
(je.voucher_type == "Exchange Gain Or Loss")
& (jea.reference_name == si.name)
& (jea.reference_type == "Sales Invoice")
& (je.is_system_generated == 1)
)
)
.limit(1)
.run()
)
self.assertEqual(q[0][0], 1)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(

View File

@@ -14,8 +14,7 @@
"advance_amount",
"allocated_amount",
"exchange_gain_loss",
"ref_exchange_rate",
"difference_posting_date"
"ref_exchange_rate"
],
"fields": [
{
@@ -31,7 +30,7 @@
"width": "250px"
},
{
"columns": 2,
"columns": 3,
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
@@ -42,7 +41,7 @@
"read_only": 1
},
{
"columns": 2,
"columns": 3,
"fieldname": "remarks",
"fieldtype": "Text",
"in_list_view": 1,
@@ -113,20 +112,13 @@
"label": "Reference Exchange Rate",
"non_negative": 1,
"read_only": 1
},
{
"columns": 2,
"fieldname": "difference_posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Difference Posting Date"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-12-20 11:58:28.962370",
"modified": "2021-09-26 15:47:46.911595",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Advance",

View File

@@ -80,7 +80,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "rate",
"fieldtype": "Currency",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -102,7 +102,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -199,7 +199,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Currency",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -221,7 +221,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -324,7 +324,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2025-12-10 08:06:40.611761",
"modified": "2018-01-10 18:32:36.201124",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
@@ -339,4 +339,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View File

@@ -13,15 +13,17 @@
"fields": [
{
"fieldname": "voucher_type",
"fieldtype": "Data",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Voucher Type"
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_name",
"fieldtype": "Data",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Voucher Name"
"label": "Voucher Name",
"options": "voucher_type"
},
{
"fieldname": "taxable_amount",
@@ -34,7 +36,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-02-05 16:39:14.863698",
"modified": "2023-01-13 13:40:41.479208",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withheld Vouchers",

View File

@@ -75,7 +75,7 @@
},
{
"default": "0",
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"fieldname": "consider_party_ledger_amount",
"fieldtype": "Check",
"label": "Consider Entire Party Ledger Amount",
@@ -102,11 +102,10 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-30 07:13:51.785735",
"modified": "2021-07-27 21:47:34.396071",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -149,4 +148,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}
}

View File

@@ -124,9 +124,6 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
cost_center = get_cost_center(inv)
tax_row.update({"cost_center": cost_center})
if cint(tax_details.round_off_tax_amount):
inv.round_off_applicable_accounts_for_tax_withholding = tax_details.account_head
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
@@ -218,14 +215,14 @@ def get_tax_row_for_tds(tax_details, tax_amount):
}
def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
def get_lower_deduction_certificate(company, tax_details, pan_no):
ldc_name = frappe.db.get_value(
"Lower Deduction Certificate",
{
"pan_no": pan_no,
"tax_withholding_category": tax_details.tax_withholding_category,
"valid_from": ("<=", posting_date),
"valid_upto": (">=", posting_date),
"valid_from": (">=", tax_details.from_date),
"valid_upto": ("<=", tax_details.to_date),
"company": company,
},
"name",
@@ -236,7 +233,7 @@ def get_lower_deduction_certificate(company, posting_date, tax_details, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
vouchers, voucher_wise_amount, zero_rate_ldc_invoices = get_invoice_vouchers(
vouchers, voucher_wise_amount = get_invoice_vouchers(
parties, tax_details, inv.company, party_type=party_type
)
@@ -273,7 +270,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
if ldc:
@@ -290,8 +287,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
else:
taxable_vouchers = list(set(vouchers) - set(zero_rate_ldc_invoices))
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, taxable_vouchers)
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
elif party_type == "Customer":
if tax_deducted:
@@ -310,33 +306,12 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
field = [
"name",
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
"posting_date",
]
field = (
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total"
)
voucher_wise_amount = {}
vouchers = []
ldcs = frappe.db.get_all(
"Lower Deduction Certificate",
filters={
"valid_from": [">=", tax_details.from_date],
"valid_upto": ["<=", tax_details.to_date],
"company": company,
"supplier": ["in", parties],
},
fields=["supplier", "valid_from", "valid_upto", "rate"],
)
doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice"
field = [
"base_tax_withholding_net_total as base_net_total" if party_type == "Supplier" else "base_net_total",
"name",
"grand_total",
"posting_date",
]
filters = {
"company": company,
frappe.scrub(party_type): ["in", parties],
@@ -350,31 +325,11 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
)
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", field])
ldcs = frappe.db.get_all(
"Lower Deduction Certificate",
filters={
"valid_from": [">=", tax_details.from_date],
"valid_upto": ["<=", tax_details.to_date],
"company": company,
"supplier": ["in", parties],
"rate": 0,
},
fields=["name", "supplier", "valid_from", "valid_upto"],
)
zero_rate_ldc_invoices = []
for d in invoices_details:
vouchers.append(d.name)
_voucher_detail = {"amount": d.base_net_total, "voucher_type": doctype}
if ldc := [x for x in ldcs if d.posting_date >= x.valid_from and d.posting_date <= x.valid_upto]:
if ldc[0].supplier in parties:
_voucher_detail.update({"amount": 0})
zero_rate_ldc_invoices.append(d.name)
voucher_wise_amount.update({d.name: _voucher_detail})
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
journal_entries_details = frappe.db.sql(
"""
@@ -405,7 +360,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
return vouchers, voucher_wise_amount, zero_rate_ldc_invoices
return vouchers, voucher_wise_amount
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
@@ -552,7 +507,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
)
supp_credit_amt = supp_jv_credit_amt
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
supp_credit_amt += inv.tax_withholding_net_total
for type in payment_entry_amounts:
if type.payment_type == "Pay":
@@ -564,15 +519,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
if inv.doctype != "Payment Entry":
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
tax_withholding_net_total = inv.base_tax_withholding_net_total
else:
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
tax_withholding_net_total = inv.tax_withholding_net_total
has_cumulative_threshold_breached = (
if (threshold and tax_withholding_net_total >= threshold) or (
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
)
if (threshold and tax_withholding_net_total >= threshold) or (has_cumulative_threshold_breached):
):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = (
@@ -580,8 +533,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
)
supp_credit_amt += net_total
if has_cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
tax_details.tax_on_excess_amount
):
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
tds_amount = get_lower_deduction_amount(
@@ -622,7 +577,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
advance_amt = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0

View File

@@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, add_months, today
from frappe.utils import add_days, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.utils import get_fiscal_year
@@ -161,45 +161,6 @@ class TestTaxWithholdingCategory(FrappeTestCase):
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_with_tax_on_excess_amount(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# Invoice with tax and without exceeding single and cumulative thresholds
for _ in range(2):
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
pi.apply_tds = 1
pi.append(
"taxes",
{
"category": "Total",
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"tax_amount": 500,
"description": "Test",
"add_deduct_tax": "Add",
},
)
pi.save()
pi.submit()
invoices.append(pi)
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
pi1.apply_tds = 1
pi1.save()
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 10,000
# Threshold calculation should be only on the third invoice
self.assertTrue(len(pi1.taxes) > 0)
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
@@ -243,18 +204,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
fiscal_year = get_fiscal_year(today(), company="_Test Company")
vouchers = []
# create advance payment
pe1 = create_payment_entry(
pe = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
)
pe1.paid_from = "Debtors - _TC"
pe1.paid_to = "Cash - _TC"
pe1.submit()
vouchers.append(pe1)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
# create invoice
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
@@ -276,17 +236,6 @@ class TestTaxWithholdingCategory(FrappeTestCase):
# make another invoice
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
# TDS should be calculated
# this payment should not be considered for TCS calculation as it is outside of fiscal year
pe2 = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
)
pe2.paid_from = "Debtors - _TC"
pe2.paid_to = "Cash - _TC"
pe2.posting_date = add_days(fiscal_year[1], -10)
pe2.submit()
vouchers.append(pe2)
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
si2.submit()
vouchers.append(si2)
@@ -626,49 +575,6 @@ class TestTaxWithholdingCategory(FrappeTestCase):
pi2.cancel()
pi3.cancel()
def test_ldc_at_0_rate(self):
frappe.db.set_value(
"Supplier",
"Test LDC Supplier",
{
"tax_withholding_category": "Test Service Category",
"pan": "ABCTY1234D",
},
)
fiscal_year = get_fiscal_year(today(), company="_Test Company")
valid_from = fiscal_year[1]
valid_upto = add_months(valid_from, 1)
create_lower_deduction_certificate(
supplier="Test LDC Supplier",
certificate_no="1AE0423AAJ",
tax_withholding_category="Test Service Category",
tax_rate=0,
limit=50000,
valid_from=valid_from,
valid_upto=valid_upto,
)
pi1 = create_purchase_invoice(
supplier="Test LDC Supplier", rate=35000, posting_date=valid_from, set_posting_time=True
)
pi1.submit()
self.assertEqual(pi1.taxes, [])
pi2 = create_purchase_invoice(
supplier="Test LDC Supplier",
rate=35000,
posting_date=add_days(valid_upto, 1),
set_posting_time=True,
)
pi2.submit()
self.assertEqual(len(pi2.taxes), 1)
# pi1 net total shouldn't be included as it lies within LDC at rate of '0'
self.assertEqual(pi2.taxes[0].tax_amount, 3500)
pi1.cancel()
pi2.cancel()
def set_previous_fy_and_tax_category(self):
test_company = "_Test Company"
category = "Cumulative Threshold TDS"
@@ -826,8 +732,7 @@ def create_purchase_invoice(**args):
pi = frappe.get_doc(
{
"doctype": "Purchase Invoice",
"set_posting_time": args.set_posting_time or False,
"posting_date": args.posting_date or today(),
"posting_date": today(),
"apply_tds": 0 if args.do_not_apply_tds else 1,
"supplier": args.supplier,
"company": "_Test Company",
@@ -1155,9 +1060,7 @@ def create_tax_withholding_category(
).insert()
def create_lower_deduction_certificate(
supplier, tax_withholding_category, tax_rate, certificate_no, limit, valid_from=None, valid_upto=None
):
def create_lower_deduction_certificate(supplier, tax_withholding_category, tax_rate, certificate_no, limit):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
if not frappe.db.exists("Lower Deduction Certificate", certificate_no):
frappe.get_doc(
@@ -1168,8 +1071,8 @@ def create_lower_deduction_certificate(
"certificate_no": certificate_no,
"tax_withholding_category": tax_withholding_category,
"fiscal_year": fiscal_year[0],
"valid_from": valid_from or fiscal_year[1],
"valid_upto": valid_upto or fiscal_year[2],
"valid_from": fiscal_year[1],
"valid_upto": fiscal_year[2],
"rate": tax_rate,
"certificate_limit": limit,
}

View File

@@ -179,15 +179,6 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
round_off_account, default_currency = frappe.get_cached_value(
"Company", gl_map[0].company, ["round_off_account", "default_currency"]
)
if not precision:
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"),
currency=default_currency,
)
new_gl_map = []
for d in gl_map:
cost_center = d.get("cost_center")
@@ -201,11 +192,6 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
new_gl_map.append(d)
continue
if d.account == round_off_account:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
continue
for sub_cost_center, percentage in cost_center_allocation:
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center

View File

@@ -29,12 +29,6 @@ from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
from erpnext.utilities.regional import temporary_flag
try:
from frappe.contacts.doctype.address.address import render_address as _render_address
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render_address
PURCHASE_TRANSACTION_TYPES = {
"Supplier Quotation",
"Purchase Order",
@@ -552,13 +546,12 @@ def validate_party_accounts(doc):
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None, template_name=None):
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
"""Get due date from `Payment Terms Template`"""
due_date = None
if (bill_date or posting_date) and party:
due_date = bill_date or posting_date
if not template_name:
template_name = get_payment_terms_template(party, party_type, company)
template_name = get_payment_terms_template(party, party_type, company)
if template_name:
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
@@ -598,34 +591,35 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
def validate_due_date(
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None, doctype=None
posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None
):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
else:
validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype)
if not template_name:
return
default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime(
"%Y-%m-%d"
)
def validate_due_date_with_template(posting_date, due_date, bill_date, template_name, doctype=None):
if not template_name:
return
if not default_due_date:
return
default_due_date = format(get_due_date_from_template(template_name, posting_date, bill_date))
if not default_due_date:
return
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
if frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles():
party_type = "supplier" if doctype == "Purchase Invoice" else "customer"
msgprint(
_("Note: Due Date exceeds allowed {0} credit days by {1} day(s)").format(
party_type, date_diff(due_date, default_due_date)
)
if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date):
is_credit_controller = (
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
)
else:
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
if is_credit_controller:
msgprint(
_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format(
date_diff(due_date, default_due_date)
)
)
else:
frappe.throw(
_("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date))
)
@frappe.whitelist()
@@ -903,16 +897,12 @@ def get_party_shipping_address(doctype: str, name: str) -> str | None:
["is_shipping_address", "=", 1],
["address_type", "=", "Shipping"],
],
fields=["name", "is_shipping_address"],
pluck="name",
limit=1,
order_by="is_shipping_address DESC",
)
if shipping_addresses and shipping_addresses[0].is_shipping_address == 1:
return shipping_addresses[0].name
if len(shipping_addresses) == 1:
return shipping_addresses[0].name
else:
return None
return shipping_addresses[0] if shipping_addresses else None
def get_partywise_advanced_payment_amount(
@@ -995,4 +985,10 @@ def add_party_account(party_type, party, company, account):
def render_address(address, check_permissions=True):
return frappe.call(_render_address, address, check_permissions=check_permissions)
try:
from frappe.contacts.doctype.address.address import render_address as _render
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render
return frappe.call(_render, address, check_permissions=check_permissions)

View File

@@ -111,7 +111,6 @@ frappe.query_reports["Accounts Payable"] = {
fieldname: "party",
label: __("Party"),
fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) {
if (!frappe.query_report.filters) return;

View File

@@ -88,7 +88,6 @@ frappe.query_reports["Accounts Payable Summary"] = {
fieldname: "party",
label: __("Party"),
fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) {
if (!frappe.query_report.filters) return;

View File

@@ -282,4 +282,4 @@
{% } %}
</tbody>
</table>
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>

View File

@@ -56,7 +56,6 @@ frappe.query_reports["Accounts Receivable"] = {
fieldname: "party",
label: __("Party"),
fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) {
if (!frappe.query_report.filters) return;

View File

@@ -6,7 +6,6 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, query_builder, scrub
from frappe.database.schema import get_definition
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
@@ -15,10 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.utils import (
build_qb_match_conditions,
get_currency_precision,
)
from erpnext.accounts.utils import get_currency_precision
# This report gives a summary of all Outstanding Invoices considering the following
@@ -53,10 +49,6 @@ class ReceivablePayableReport:
self.age_as_on = (
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
)
self.ple_fetch_method = (
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
or "Buffered Cursor"
) # Fail Safe
def run(self, args):
self.filters.update(args)
@@ -93,7 +85,13 @@ class ReceivablePayableReport:
self.skip_total_row = 1
def get_data(self):
self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person()
self.voucher_balance = OrderedDict()
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
# Get invoice details like bill_no, due_date etc for all invoices
self.get_invoice_details()
@@ -107,46 +105,13 @@ class ReceivablePayableReport:
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
self.prepare_ple_query()
self.data = []
self.voucher_balance = OrderedDict()
if self.ple_fetch_method == "Buffered Cursor":
self.fetch_ple_in_buffered_cursor()
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
elif self.ple_fetch_method == "Raw SQL":
self.fetch_ple_in_sql_procedures()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
for ple in self.ple_entries:
self.update_voucher_balance(ple)
self.build_data()
def fetch_ple_in_buffered_cursor(self):
self.ple_entries = self.ple_query.run(as_dict=True)
for ple in self.ple_entries:
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
# This is unavoidable. Initialization and allocation cannot happen in same loop
for ple in self.ple_entries:
self.update_voucher_balance(ple)
delattr(self, "ple_entries")
def fetch_ple_in_unbuffered_cursor(self):
self.ple_entries = []
with frappe.db.unbuffered_cursor():
for ple in self.ple_query.run(as_dict=True, as_iterator=True):
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
self.ple_entries.append(ple)
# This is unavoidable. Initialization and allocation cannot happen in same loop
for ple in self.ple_entries:
self.update_voucher_balance(ple)
delattr(self, "ple_entries")
def build_voucher_dict(self, ple):
return frappe._dict(
voucher_type=ple.voucher_type,
@@ -164,24 +129,26 @@ class ReceivablePayableReport:
paid_in_account_currency=0.0,
credit_note_in_account_currency=0.0,
outstanding_in_account_currency=0.0,
cost_center=ple.cost_center,
)
def init_voucher_balance(self, ple):
if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party)
else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
def init_voucher_balance(self):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
if self.filters.get("ignore_accounts"):
key = (ple.voucher_type, ple.voucher_no, ple.party)
else:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
self.voucher_balance[key].cost_center = ple.cost_center
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
self.get_invoices(ple)
self.get_invoices(ple)
if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
self.init_subtotal_row("Total")
@@ -303,78 +270,8 @@ class ReceivablePayableReport:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
def fetch_ple_in_sql_procedures(self):
self.proc = InitSQLProceduresForAR()
build_balance = f"""
begin not atomic
declare done boolean default false;
declare rec1 row type of `{self.proc._row_def_table_name}`;
declare ple cursor for {self.ple_query.get_sql()};
declare continue handler for not found set done = true;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.init_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
set done = false;
open ple;
fetch ple into rec1;
while not done do
call {self.proc.allocate_procedure_name}(rec1);
fetch ple into rec1;
end while;
close ple;
end;
"""
frappe.db.sql(build_balance)
balances = frappe.db.sql(
f"""select
name,
voucher_type,
voucher_no,
party,
party_account `account`,
posting_date,
account_currency,
cost_center,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
sum(invoiced) - sum(paid) - sum(credit_note) `outstanding`,
sum(invoiced_in_account_currency) `invoiced_in_account_currency`,
sum(paid_in_account_currency) `paid_in_account_currency`,
sum(credit_note_in_account_currency) `credit_note_in_account_currency`,
sum(invoiced_in_account_currency) - sum(paid_in_account_currency) - sum(credit_note_in_account_currency) `outstanding_in_account_currency`
from `{self.proc._voucher_balance_name}` group by name order by posting_date;""",
as_dict=True,
)
for x in balances:
if self.filters.get("ignore_accounts"):
key = (x.voucher_type, x.voucher_no, x.party)
else:
key = (x.account, x.voucher_type, x.voucher_no, x.party)
_d = self.build_voucher_dict(x)
for field in [
"invoiced",
"paid",
"credit_note",
"outstanding",
"invoiced_in_account_currency",
"paid_in_account_currency",
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
]:
_d[field] = x.get(field)
self.voucher_balance[key] = _d
if not row.cost_center and ple.cost_center:
row.cost_center = str(ple.cost_center)
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
@@ -602,7 +499,7 @@ class ReceivablePayableReport:
ps.description, ps.paid_amount, ps.discounted_amount
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and ps.parenttype = '{row.voucher_type}' and
si.name = ps.parent and
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date
@@ -632,7 +529,9 @@ class ReceivablePayableReport:
self.append_payment_term(row, d, term)
def append_payment_term(self, row, d, term):
if d.currency == d.party_account_currency:
if (
self.filters.get("customer") or self.filters.get("supplier")
) and d.currency == d.party_account_currency:
invoiced = d.payment_amount
else:
invoiced = d.base_payment_amount
@@ -868,7 +767,7 @@ class ReceivablePayableReport:
index = 4
row["range" + str(index + 1)] = row.outstanding
def prepare_ple_query(self):
def get_ple_entries(self):
# get all the GL entries filtered by the given filters
self.prepare_conditions()
@@ -916,15 +815,12 @@ class ReceivablePayableReport:
else:
query = query.select(ple.remarks)
if match_conditions := build_qb_match_conditions("Payment Ledger Entry"):
query = query.where(Criterion.all(match_conditions))
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
query = query.orderby(self.ple.posting_date, self.ple.party)
self.ple_query = query
self.ple_entries = query.run(as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@@ -1108,29 +1004,22 @@ class ReceivablePayableReport:
def get_columns(self):
self.columns = []
self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
self.add_column("Posting Date", fieldtype="Date")
self.add_column(
label=_("Party Type"),
label="Party Type",
fieldname="party_type",
fieldtype="Data",
width=100,
)
self.add_column(
label=_("Party"),
label="Party",
fieldname="party",
fieldtype="Dynamic Link",
options="party_type",
width=180,
)
if self.account_type == "Receivable":
label = _("Receivable Account")
elif self.account_type == "Payable":
label = _("Payable Account")
else:
label = _("Party Account")
self.add_column(
label=label,
label=self.account_type + " Account",
fieldname="party_account",
fieldtype="Link",
options="Account",
@@ -1139,10 +1028,10 @@ class ReceivablePayableReport:
if self.party_naming_by == "Naming Series":
if self.account_type == "Payable":
label = _("Supplier Name")
label = "Supplier Name"
fieldname = "supplier_name"
else:
label = _("Customer Name")
label = "Customer Name"
fieldname = "customer_name"
self.add_column(
label=label,
@@ -1168,7 +1057,7 @@ class ReceivablePayableReport:
width=180,
)
self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date")
self.add_column(label="Due Date", fieldtype="Date")
if self.account_type == "Payable":
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
@@ -1311,134 +1200,3 @@ def get_customer_group_with_children(customer_groups):
frappe.throw(_("Customer Group: {0} does not exist").format(d))
return list(set(all_customer_groups))
class InitSQLProceduresForAR:
"""
Initialize SQL Procedures, Functions and Temporary tables to build Receivable / Payable report
"""
_varchar_type = get_definition("Data")
_currency_type = get_definition("Currency")
# Temporary Tables
_voucher_balance_name = "_ar_voucher_balance"
_voucher_balance_definition = f"""
create temporary table `{_voucher_balance_name}`(
name {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
party {_varchar_type},
party_account {_varchar_type},
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
invoiced_in_account_currency {_currency_type},
paid_in_account_currency {_currency_type},
credit_note_in_account_currency {_currency_type}) engine=memory;
"""
_row_def_table_name = "_ar_ple_row"
_row_def_table_definition = f"""
create temporary table `{_row_def_table_name}`(
name {_varchar_type},
account {_varchar_type},
voucher_type {_varchar_type},
voucher_no {_varchar_type},
against_voucher_type {_varchar_type},
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
account_currency {_varchar_type},
amount {_currency_type},
amount_in_account_currency {_currency_type}) engine=memory;
"""
# Function
genkey_function_name = "ar_genkey"
genkey_function_sql = f"""
create function `{genkey_function_name}`(rec row type of `{_row_def_table_name}`, allocate bool) returns char(40)
begin
if allocate then
return sha1(concat_ws(',', rec.account, rec.against_voucher_type, rec.against_voucher_no, rec.party));
else
return sha1(concat_ws(',', rec.account, rec.voucher_type, rec.voucher_no, rec.party));
end if;
end
"""
# Procedures
init_procedure_name = "ar_init_tmp_table"
init_procedure_sql = f"""
create procedure ar_init_tmp_table(in ple row type of `{_row_def_table_name}`)
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, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
allocate_procedure_name = "ar_allocate_to_tmp_table"
allocate_procedure_sql = f"""
create procedure ar_allocate_to_tmp_table(in ple row type of `{_row_def_table_name}`)
begin
declare invoiced {_currency_type} default 0;
declare invoiced_in_account_currency {_currency_type} default 0;
declare paid {_currency_type} default 0;
declare paid_in_account_currency {_currency_type} default 0;
declare credit_note {_currency_type} default 0;
declare credit_note_in_account_currency {_currency_type} default 0;
if ple.amount > 0 then
if (ple.voucher_type in ("Journal Entry", "Payment Entry") and (ple.voucher_no != ple.against_voucher_no)) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set invoiced = ple.amount;
set invoiced_in_account_currency = ple.amount_in_account_currency;
end if;
else
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice") then
if (ple.voucher_no = ple.against_voucher_no) then
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
else
set credit_note = -1 * ple.amount;
set credit_note_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
else
set paid = -1 * ple.amount;
set paid_in_account_currency = -1 * ple.amount_in_account_currency;
end if;
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);
end;
"""
def __init__(self):
existing_procedures = frappe.db.get_routines()
if self.genkey_function_name not in existing_procedures:
frappe.db.sql(self.genkey_function_sql)
if self.init_procedure_name not in existing_procedures:
frappe.db.sql(self.init_procedure_sql)
if self.allocate_procedure_name not in existing_procedures:
frappe.db.sql(self.allocate_procedure_sql)
frappe.db.sql(f"drop table if exists `{self._voucher_balance_name}`")
frappe.db.sql(self._voucher_balance_definition)
frappe.db.sql(f"drop table if exists `{self._row_def_table_name}`")
frappe.db.sql(self._row_def_table_definition)

View File

@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
item=self.item,
@@ -34,7 +34,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
rate=100,
price_list_rate=100,
do_not_save=1,
**args,
)
if not no_payment_schedule:
si.append(
@@ -112,7 +111,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel()
def test_accounts_receivable_with_payment(self):
def test_accounts_receivable(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
@@ -152,15 +151,11 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
# as the invoice partially paid and returning the full amount so the outstanding amount should be True
self.assertEqual(cr_note.update_outstanding_for_self, True)
report = execute(filters)
expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
row = report[1][-1]
row = report[1][0]
self.assertEqual(
expected_data_after_credit_note,
[
@@ -173,105 +168,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
],
)
def test_accounts_receivable_without_payment(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
si = self.create_sales_invoice()
report = execute(filters)
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
self.assertEqual(cr_note.update_outstanding_for_self, False)
report = execute(filters)
row = report[1]
self.assertTrue(len(row) == 0)
def test_accounts_receivable_with_partial_payment(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
si = self.create_sales_invoice(qty=2)
report = execute(filters)
expected_data = [[200, 60, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(
expected_data_after_payment[i - 1],
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
cr_note = self.create_credit_note(si.name, do_not_submit=True)
cr_note.update_outstanding_for_self = False
cr_note.save().submit()
self.assertFalse(cr_note.update_outstanding_for_self)
report = execute(filters)
expected_data_after_credit_note = [
[200, 100, 0, 80, 20, self.debit_to],
[200, 40, 0, 0, 40, self.debit_to],
]
for i in range(2):
row = report[1][i - 1]
self.assertEqual(
expected_data_after_credit_note[i - 1],
[
row.invoice_grand_total,
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
row.party_account,
],
)
def test_cr_note_flag_to_update_self(self):
filters = {
"company": self.company,

View File

@@ -88,7 +88,6 @@ frappe.query_reports["Accounts Receivable Summary"] = {
fieldname: "party",
label: __("Party"),
fieldtype: "MultiSelectList",
options: "party_type",
get_data: function (txt) {
if (!frappe.query_report.filters) return;

View File

@@ -25,26 +25,11 @@ frappe.query_reports["Asset Depreciations and Balances"] = {
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2],
reqd: 1,
},
{
fieldname: "group_by",
label: __("Group By"),
fieldtype: "Select",
options: ["Asset Category", "Asset"],
default: "Asset Category",
},
{
fieldname: "asset_category",
label: __("Asset Category"),
fieldtype: "Link",
options: "Asset Category",
depends_on: "eval: doc.group_by == 'Asset Category'",
},
{
fieldname: "asset",
label: __("Asset"),
fieldtype: "Link",
options: "Asset",
depends_on: "eval: doc.group_by == 'Asset'",
},
],
};

View File

@@ -14,28 +14,21 @@ def execute(filters=None):
def get_data(filters):
if filters.get("group_by") == "Asset Category":
return get_group_by_asset_category_data(filters)
elif filters.get("group_by") == "Asset":
return get_group_by_asset_data(filters)
def get_group_by_asset_category_data(filters):
data = []
asset_categories = get_asset_categories_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_category(filters)
asset_categories = get_asset_categories(filters)
assets = get_assets(filters)
for asset_category in asset_categories:
row = frappe._dict()
# row.asset_category = asset_category
row.update(asset_category)
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
row.cost_as_on_to_date = (
flt(row.cost_as_on_from_date)
+ flt(row.cost_of_new_purchase)
- flt(row.cost_of_sold_asset)
- flt(row.cost_of_scrapped_asset)
)
row.update(
@@ -45,19 +38,17 @@ def get_group_by_asset_category_data(filters):
if asset["asset_category"] == asset_category.get("asset_category", "")
)
)
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
@@ -66,71 +57,52 @@ def get_group_by_asset_category_data(filters):
return data
def get_asset_categories_for_grouped_by_category(filters):
def get_asset_categories(filters):
condition = ""
if filters.get("asset_category"):
condition += " and a.asset_category = %(asset_category)s"
# nosemgrep
condition += " and asset_category = %(asset_category)s"
return frappe.db.sql(
f"""
SELECT a.asset_category,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.gross_purchase_amount
SELECT asset_category,
ifnull(sum(case when purchase_date < %(from_date)s then
case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then
gross_purchase_amount
else
0
end
else
0
end), 0) as value_as_on_from_date,
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
end), 0) as cost_as_on_from_date,
ifnull(sum(case when purchase_date >= %(from_date)s then
gross_purchase_amount
else
0
end), 0) as value_of_new_purchase,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Sold" then
a.gross_purchase_amount
end), 0) as cost_of_new_purchase,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Sold" then
gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_sold_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Scrapped" then
a.gross_purchase_amount
end), 0) as cost_of_sold_asset,
ifnull(sum(case when ifnull(disposal_date, 0) != 0
and disposal_date >= %(from_date)s
and disposal_date <= %(to_date)s then
case when status = "Scrapped" then
gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Capitalized" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
where acai.asset = a.name
and ac.posting_date < %(from_date)s
and ac.docstatus=1
)
group by a.asset_category
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {condition}
group by asset_category
""",
{
"to_date": filters.to_date,
@@ -142,17 +114,14 @@ def get_asset_categories_for_grouped_by_category(filters):
)
def get_assets_for_grouped_by_category(filters):
def get_assets(filters):
condition = ""
if filters.get("asset_category"):
condition = f" and a.asset_category = '{filters.get('asset_category')}'"
# nosemgrep
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
return frappe.db.sql(
f"""
"""
SELECT results.asset_category,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.asset_category,
@@ -161,11 +130,6 @@ def get_assets_for_grouped_by_category(filters):
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
@@ -185,22 +149,15 @@ def get_assets_for_grouped_by_category(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition}
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.asset_category
union
SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_eliminated_via_reversal,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
@@ -208,272 +165,51 @@ def get_assets_for_grouped_by_category(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.asset_category) as results
group by results.asset_category
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
},
as_dict=1,
)
def get_group_by_asset_data(filters):
data = []
asset_details = get_asset_details_for_grouped_by_category(filters)
assets = get_assets_for_grouped_by_asset(filters)
for asset_detail in asset_details:
row = frappe._dict()
row.update(asset_detail)
row.value_as_on_to_date = (
flt(row.value_as_on_from_date)
+ flt(row.value_of_new_purchase)
- flt(row.value_of_sold_asset)
- flt(row.value_of_scrapped_asset)
- flt(row.value_of_capitalized_asset)
)
row.update(next(asset for asset in assets if asset["asset"] == asset_detail.get("name", "")))
row.accumulated_depreciation_as_on_to_date = (
flt(row.accumulated_depreciation_as_on_from_date)
+ flt(row.depreciation_amount_during_the_period)
- flt(row.depreciation_eliminated_during_the_period)
- flt(row.depreciation_eliminated_via_reversal)
)
row.net_asset_value_as_on_from_date = flt(row.value_as_on_from_date) - flt(
row.accumulated_depreciation_as_on_from_date
)
row.net_asset_value_as_on_to_date = flt(row.value_as_on_to_date) - flt(
row.accumulated_depreciation_as_on_to_date
)
data.append(row)
return data
def get_asset_details_for_grouped_by_category(filters):
condition = ""
if filters.get("asset"):
condition += " and a.name = %(asset)s"
# nosemgrep
return frappe.db.sql(
f"""
SELECT a.name,
ifnull(sum(case when a.purchase_date < %(from_date)s then
case when ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_as_on_from_date,
ifnull(sum(case when a.purchase_date >= %(from_date)s then
a.gross_purchase_amount
else
0
end), 0) as value_of_new_purchase,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Sold" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_sold_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Scrapped" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_scrapped_asset,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0
and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s then
case when a.status = "Capitalized" then
a.gross_purchase_amount
else
0
end
else
0
end), 0) as value_of_capitalized_asset
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
and not exists(
select 1 from `tabAsset Capitalization Asset Item` acai join `tabAsset Capitalization` ac on acai.parent=ac.name
where acai.asset = a.name
and ac.posting_date < %(from_date)s
and ac.docstatus=1
)
group by a.name
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset": filters.get("asset"),
},
as_dict=1,
)
def get_assets_for_grouped_by_asset(filters):
condition = ""
if filters.get("asset"):
condition = f" and a.name = '{filters.get('asset')}'"
# nosemgrep
return frappe.db.sql(
f"""
SELECT results.name as asset,
sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date,
sum(results.depreciation_eliminated_via_reversal) as depreciation_eliminated_via_reversal,
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
from (SELECT a.name as name,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when gle.posting_date <= %(to_date)s and ifnull(a.disposal_date, 0) = 0 then
gle.credit
else
0
end), 0) as depreciation_eliminated_via_reversal,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where
a.docstatus=1
and a.company=%(company)s
and a.purchase_date <= %(to_date)s
and gle.is_cancelled = 0
and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
{condition}
group by a.name
union
SELECT a.name as name,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date < %(from_date)s then
0
else
a.opening_accumulated_depreciation
end), 0) as accumulated_depreciation_as_on_from_date,
0 as depreciation_as_on_from_date_credit,
ifnull(sum(case when a.disposal_date >= %(from_date)s and a.disposal_date <= %(to_date)s then
a.opening_accumulated_depreciation
else
0
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {condition}
group by a.name) as results
group by results.name
""",
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
},
""".format(condition),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)
def get_columns(filters):
columns = []
if filters.get("group_by") == "Asset Category":
columns.append(
{
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
}
)
elif filters.get("group_by") == "Asset":
columns.append(
{
"label": _("Asset"),
"fieldname": "asset",
"fieldtype": "Link",
"options": "Asset",
"width": 120,
}
)
columns += [
return [
{
"label": _("Value as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "value_as_on_from_date",
"label": _("Asset Category"),
"fieldname": "asset_category",
"fieldtype": "Link",
"options": "Asset Category",
"width": 120,
},
{
"label": _("Cost as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "cost_as_on_from_date",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Value of New Purchase"),
"fieldname": "value_of_new_purchase",
"label": _("Cost of New Purchase"),
"fieldname": "cost_of_new_purchase",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Value of Sold Asset"),
"fieldname": "value_of_sold_asset",
"label": _("Cost of Sold Asset"),
"fieldname": "cost_of_sold_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Value of Scrapped Asset"),
"fieldname": "value_of_scrapped_asset",
"label": _("Cost of Scrapped Asset"),
"fieldname": "cost_of_scrapped_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Value of New Capitalized Asset"),
"fieldname": "value_of_capitalized_asset",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Value as on") + " " + formatdate(filters.to_date),
"fieldname": "value_as_on_to_date",
"label": _("Cost as on") + " " + formatdate(filters.to_date),
"fieldname": "cost_as_on_to_date",
"fieldtype": "Currency",
"width": 140,
},
@@ -501,12 +237,6 @@ def get_columns(filters):
"fieldtype": "Currency",
"width": 270,
},
{
"label": _("Depreciation eliminated via reversal"),
"fieldname": "depreciation_eliminated_via_reversal",
"fieldtype": "Currency",
"width": 270,
},
{
"label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date),
"fieldname": "net_asset_value_as_on_from_date",
@@ -520,5 +250,3 @@ def get_columns(filters):
"width": 200,
},
]
return columns

View File

@@ -7,7 +7,6 @@ from frappe import _
from frappe.utils import cint, flt
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,
get_columns,
get_data,
get_filtered_list_for_consolidated_report,
@@ -102,9 +101,6 @@ def execute(filters=None):
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
)
if filters.get("selected_view") == "Growth":
compute_growth_view_data(data, period_list)
return columns, data, message, chart, report_summary, primitive_summary

View File

@@ -1,3 +1,6 @@
<div style="margin-bottom: 7px;">
{%= frappe.boot.letter_heads[frappe.defaults.get_default("letter_head")] %}
</div>
<h2 class="text-center">{%= __("Bank Reconciliation Statement") %}</h2>
<h4 class="text-center">{%= filters.account && (filters.account + ", "+filters.report_date) || "" %} {%= filters.company %}</h4>
<hr>
@@ -43,4 +46,4 @@
{% } %}
</tbody>
</table>
<p class="text-right text-muted">{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}</p>
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>

View File

@@ -27,7 +27,6 @@ def get_report_filters(report_filters):
["Purchase Invoice", "docstatus", "=", 1],
["Purchase Invoice", "per_received", "<", 100],
["Purchase Invoice", "update_stock", "=", 0],
["Purchase Invoice", "is_opening", "!=", "Yes"],
]
if report_filters.get("purchase_invoice"):

View File

@@ -91,7 +91,6 @@ function get_filters() {
fieldname: "budget_against_filter",
label: __("Dimension Filter"),
fieldtype: "MultiSelectList",
options: "budget_against",
get_data: function (txt) {
if (!frappe.query_report.filters) return;

View File

@@ -263,7 +263,6 @@ def get_actual_details(name, filters):
and ba.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select

View File

@@ -355,7 +355,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
gl_entries_by_account,
accounts_by_name,
accounts,
ignore_closing_entries=ignore_closing_entries,
ignore_closing_entries=False,
root_type=root_type,
)

View File

@@ -67,5 +67,5 @@
</tbody>
</table>
<p class="text-right text-muted">
{%= __("Printed on {0}", [frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string())]) %}
Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}
</p>

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