Compare commits

...

275 Commits

Author SHA1 Message Date
Ankush Menat
e576a9c7c1 fix: using DN for transfer w/o internal customer (#27798)
This used to be work before though not "advertised", since a lot of
users have started using it as feature, it can't be broken now.

(cherry picked from commit df1f8fddf6)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
#	erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
2025-02-01 00:13:19 +00:00
rohitwaghchaure
75567dc9ff Merge pull request #45646 from rohitwaghchaure/fixed-support-30611
fix: not able to make manufacturing entry for alternate items
2025-01-31 23:47:43 +05:30
Rohit Waghchaure
1607aa1a44 fix: not able to make manufacturing entry for alternate items 2025-01-31 23:28:54 +05:30
rohitwaghchaure
1a83316112 Merge pull request #45642 from rohitwaghchaure/fixed-support-30559
fix: actual qty showing blank for sub-assembly items
2025-01-31 22:38:04 +05:30
Rohit Waghchaure
5be2e71a35 fix: actual qty showing blank for sub-assembly items 2025-01-31 16:49:55 +05:30
rohitwaghchaure
3e35c48cf7 Merge pull request #45626 from rohitwaghchaure/fixed-support-29212-1
fix: validation to prevent submission, if the SABB is not linked to a stock transaction
2025-01-31 14:43:12 +05:30
Rohit Waghchaure
f976115a2b fix: validation to prevent submission if the SABB is not linked to a stock transaction 2025-01-31 14:04:27 +05:30
ruthra kumar
e2db3b9cfc Merge pull request #45615 from ljain112/fix-pe-erroe
fix: correct error message in payment entry
2025-01-31 12:56:14 +05:30
rohitwaghchaure
a913c9b202 Merge pull request #45629 from mihir-kandoi/st30495
fix: attribute 'msgbox' not found in sales invoice.js
2025-01-31 12:03:47 +05:30
Diptanil Saha
fe51535392 fix: pos print receipt on submit (#45632) 2025-01-31 12:00:00 +05:30
Mihir Kandoi
5643385c22 fix: attribute 'msgbox' not found in sales invoice.js 2025-01-31 11:26:01 +05:30
Diptanil Saha
5a1851dfe3 fix: loading print receipt only at order complete (#45627) 2025-01-31 11:17:48 +05:30
ruthra kumar
437444fe04 Merge pull request #45623 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-31 10:54:40 +05:30
Frappe PR Bot
00f9a36980 fix: Swedish translations 2025-01-31 03:11:13 +05:30
ljain112
592704cfd0 fix: correct error message in payment entry 2025-01-30 17:27:44 +05:30
rohitwaghchaure
b3ffb82586 Merge pull request #45612 from rohitwaghchaure/fixed-posting-date
fix: posting_date to posting_datetime in stock related queries
2025-01-30 17:05:26 +05:30
Rohit Waghchaure
e61ab48145 fix: posting_date to posting_datetime in stock related queries 2025-01-30 16:04:06 +05:30
rohitwaghchaure
b8ee15269c Merge pull request #45609 from rohitwaghchaure/fixed-support-30336
fix: reposting issue with s3 backup
2025-01-30 15:58:14 +05:30
Rohit Waghchaure
6b454ca9a7 fix: reposting issue with s3 backup 2025-01-30 11:59:40 +05:30
Frappe PR Bot
9a34c4c1ec fix: sync translations from crowdin (#45608)
* fix: Turkish translations

* fix: Persian translations
2025-01-30 11:20:01 +05:30
rohitwaghchaure
968e235a3f Merge pull request #45486 from rohitwaghchaure/fixed-SABB-performance-issue
perf: stock entry with batch
2025-01-29 20:49:09 +05:30
rohitwaghchaure
90eefac6f6 Merge pull request #45600 from rohitwaghchaure/fixed-validation-msg
fix: validation message
2025-01-29 20:41:39 +05:30
Rohit Waghchaure
4c8dff942d fix: validation message 2025-01-29 20:36:58 +05:30
rohitwaghchaure
378afd2f65 Merge pull request #45596 from frappe/mergify/bp/develop/pr-45595
fix: do not allow to manually submit the SABB (backport #45595)
2025-01-29 16:44:12 +05:30
Sanket Shah
aaf720ab61 fix: Gross Profit Report with Correct Totals and Gross Margin (#45548)
Co-authored-by: Sanket322 <shahsanket322003.com>
2025-01-29 16:42:51 +05:30
Rohit Waghchaure
d042c841e4 fix: do not allow to manually submit the SABB
(cherry picked from commit 2b16eb5381)
2025-01-29 11:08:59 +00:00
ruthra kumar
409e512d47 Merge pull request #45569 from ljain112/fix-vch-outstanding
fix: update voucher outstanding from payment ledger
2025-01-29 16:11:02 +05:30
Safvan Huzain
3f2e93dcb6 fix(query): remove duplicate docstatus condition (#45586)
fix: remove duplicate docstatus condition in query
2025-01-29 10:27:01 +00:00
ruthra kumar
72b940e3d3 Merge pull request #45441 from ljain112/fix-tax-withhel-details
fix: show payment entries in tax withheld vouchers
2025-01-29 15:35:50 +05:30
ruthra kumar
9ae8e94e6c Merge pull request #45585 from ruthra-kumar/auto_add_taxes
refactor: auto add taxes from template
2025-01-29 15:28:12 +05:30
ruthra kumar
d1086722bf refactor: auto add taxes from template 2025-01-29 14:38:50 +05:30
Sugesh G
d748b491ee fix: use currency from opportunity while creating quotation (#45540) 2025-01-29 12:35:45 +05:30
rohitwaghchaure
5e8e7dd3d8 Merge pull request #45536 from rtdany10/precision-return
fix: return qty error due to precision
2025-01-29 12:19:54 +05:30
Ejaaz Khan
5a023dc8d4 fix: add multiple item issue in stock entry (#45544) 2025-01-29 12:19:01 +05:30
Lakshit Jain
9f20854bd9 fix: get stock balance filtered by company for validating stock value in jv (#45549)
* fix: get stock balance filtered by company for validating stock value in jv

* test: error is raised  on validate
2025-01-29 12:16:11 +05:30
ruthra kumar
a64a4f9b20 Merge pull request #45575 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-29 11:25:43 +05:30
Frappe PR Bot
ac32c554af fix: Turkish translations 2025-01-29 03:14:46 +05:30
Frappe PR Bot
ceb4f249cb fix: Swedish translations 2025-01-29 03:14:43 +05:30
rohitwaghchaure
2609f9809d Merge pull request #43270 from rohitwaghchaure/feat-stock-reservation-for-wo
feat: stock reservation for Work Order
2025-01-28 18:06:57 +05:30
ljain112
dd77070351 fix: update voucher outstanding from payment ledger 2025-01-28 17:54:16 +05:30
Rohit Waghchaure
4d050441b3 feat: stock reservation for Work Order 2025-01-28 16:42:57 +05:30
Akhil Narang
8e77b26641 Merge pull request #45554 from akhilnarang/fixup-44604
fix(book_appointment): stop trying to use pytz api with zoneinfo
2025-01-28 16:25:27 +05:30
Akhil Narang
b08da0f6bd fix(book_appointment): stop trying to use pytz api with zoneinfo
Signed-off-by: Akhil Narang <me@akhilnarang.dev>
2025-01-28 16:09:26 +05:30
ruthra kumar
b1eb604363 Merge pull request #45560 from ruthra-kumar/rename_swiss_coa
chore: rename file to standard name format
2025-01-28 16:03:19 +05:30
ruthra kumar
7bc075376b chore: rename file to standard name format 2025-01-28 14:34:49 +05:30
ruthra kumar
7e7209e448 Merge pull request #45452 from aerele/payment-entry-transaction-currency
fix(payment entry): get amount in transaction currency
2025-01-28 14:33:30 +05:30
ruthra kumar
9964ddc0e9 Merge pull request #45551 from eagleautomate/swiss_coa
feat: Add chart of accounts for Switzerland
2025-01-28 11:33:57 +05:30
Sanket Shah
9933d3c8ff fix: update fields on change of item code In Update Items of Sales Order (#45125)
* fix: update fields on change of item code

* fix: minor update

* fix: set the new values always

* Revert "fix: set the new values always"

This reverts commit 44daa0a641.

---------

Co-authored-by: Sanket322 <shahsanket322003.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2025-01-28 11:27:28 +05:30
ruthra kumar
813cfdfff6 Merge pull request #45552 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-28 11:03:49 +05:30
Rohit Waghchaure
0b1b964b77 perf: stock entry with batch 2025-01-28 09:49:37 +05:30
Frappe PR Bot
d1fbeb11cb fix: Turkish translations 2025-01-28 03:03:54 +05:30
Frappe PR Bot
dc7e7118af fix: Swedish translations 2025-01-28 03:03:50 +05:30
Frappe PR Bot
26afba142e fix: French translations 2025-01-28 03:03:33 +05:30
eagleautomate
2c644ec2ef feat: Add chart of accounts for Switzerland
240812 Schulkontenrahmen VEB - DE
2025-01-27 19:29:36 +05:30
ruthra kumar
47c2c5377c Merge pull request #45345 from Sanket322/set_discount_properly
fix:  maintain existing discounts in get_pricing_rule_for_item
2025-01-27 16:01:46 +05:30
Dany Robert
3078578692 fix: return qty error due to precision 2025-01-27 09:54:06 +00:00
ruthra kumar
49570a5544 Merge pull request #45302 from aerele/validate-dimensions-company
feat: add company level validation for accounting dimension
2025-01-27 14:53:49 +05:30
meera-greycube
e88f96b92d feat : company field required for multi company setup (#45415)
* Update vehicle.json

feat : company field required for multi company setup

* Update vehicle.json

allow_rename fixed in vehicle.json

* fix: fix permission issue
2025-01-27 14:28:11 +05:30
ruthra kumar
e55d4030ec Merge pull request #45284 from aerele/pos-return-currency
fix: set party_account_currency for pos_invoice returns
2025-01-27 14:27:41 +05:30
Raffael Meyer
11f65f20a0 feat(Sales Invoice): allow linking to project without adding timesheets (#44295)
* feat(Sales Invoice): allow linking to project without adding timesheets

* test: add timesheet data
2025-01-27 13:20:57 +05:30
Diptanil Saha
2ac8c92e7f fix: currency decimal on POS Past Order List (#45524)
* fix: currency decimal on POS

* fix: removed precision
2025-01-27 13:02:24 +05:30
ruthra kumar
fb285749dd Merge pull request #45300 from HarryPaulo/fix-prevent-commit
fix: prevent unnecessary db.commit for contact insert
2025-01-27 12:18:07 +05:30
Frappe PR Bot
887645e55f fix: sync translations from crowdin (#45522)
* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Hungarian translations

* fix: Polish translations

* fix: Russian translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Persian translations

* fix: Bosnian translations

* fix: German translations

* fix: Esperanto translations
2025-01-26 23:19:32 +01:00
venkat102
af97f42429 fix(payment entry): get amount in transaction currency 2025-01-26 19:52:06 +05:30
Frappe PR Bot
eabdd5992b chore: update POT file (#45451) 2025-01-26 13:54:13 +01:00
Frappe PR Bot
29341245b8 fix: sync translations from crowdin (#45444)
* fix: Swedish translations

* fix: Turkish translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Persian translations
2025-01-26 12:15:18 +05:30
rohitwaghchaure
b5afd901f3 Merge pull request #45443 from rohitwaghchaure/fixed-support-29487
fix: allow to fix negative stock for batch using stock reco
2025-01-25 15:03:22 +05:30
Rohit Waghchaure
2e8cde3378 fix: allow to fix negative stock for batch using stock reco 2025-01-25 14:44:25 +05:30
ljain112
d97e78e5d3 fix: variable names 2025-01-25 13:16:06 +05:30
ljain112
55733d4f18 fix: show payment entries in Tax Withheld Vouchers 2025-01-25 13:04:12 +05:30
rohitwaghchaure
53704b98b5 fix: do not check budget during reposting (#45432) 2025-01-24 12:00:45 +00:00
rohitwaghchaure
bad1ac9fbc Merge pull request #45423 from rohitwaghchaure/fixed-support-29882
fix: precision issue in stock entry
2025-01-24 17:07:16 +05:30
Raffael Meyer
4008ca5ddd feat(UX): scroll to required field (#44367) 2025-01-24 16:36:46 +05:30
Rohit Waghchaure
9f3b8520fe fix: precision issue in stock entry 2025-01-24 16:07:25 +05:30
Sanket Shah
d862e9b771 fix: validate items against selling settings (#45288)
fix: validate_for_duplicate_items

Co-authored-by: Sanket322 <shahsanket322003.com>
2025-01-24 15:10:43 +05:30
Venkatesh
97acbb3134 fix: disable load_after_mapping when purchase order created from sales order (#45405) 2025-01-24 15:02:56 +05:30
Raffael Meyer
a9bc395e98 fix: secure bulk transaction (#45386) 2025-01-24 14:44:26 +05:30
Tufan Kaynak
42edb9f5b1 fix(material request): mapping Sales Order Item Delivery Date to Mate… (#45227)
* fix(material request): mapping Sales Order Item Delivery Date to Material Request Item Required By

as mentioned in https://discuss.frappe.io/t/item-delivery-date-on-sales-order-is-not-transferred-to-material-request-item-required-by-date/140479 
fixing
When you create a Material Request directly on the Sales Order via → Create → Material Request, Delivery Date on Sales Order Item is not transferred to Material Request Item Required By date.

* fix(linters): meaningless linters formatting message applied

In order to pass the linters test which I find meaningless as it asks for the comma after the last item in a dictionary data type

* fix(linters): formatting code for linters pass

Linters formatting applied
2025-01-24 14:20:20 +05:30
Martin Luessi
e82911041d fix: remove unnecessary auth from plaid connector (#44305) 2025-01-24 13:11:32 +05:30
rohitwaghchaure
3d7f1026ca Merge pull request #45335 from rohitwaghchaure/fixed-valuation-for-batch
fix: valuation for batch
2025-01-24 12:50:55 +05:30
Diptanil Saha
54d234e05d fix: resolved pos return setting to default mode of payment instead of user selection (#45377)
* fix: resolved pos return setting to default mode of payment instead of user selection

* refactor: removed console log statement

* refactor: moved get_payment_data to sales_and_purchase_return.py
2025-01-24 12:41:01 +05:30
Diptanil Saha
78c7c1c631 feat: full screen on pos (#45404)
* feat: full screen on pos

* refactor: variables for label

* fix: refactor and handled button label change

* refactor: rename enable fullscreen label
2025-01-24 07:07:36 +00:00
Rohit Waghchaure
8028dd2683 fix: version 2025-01-24 12:32:16 +05:30
mahsem
cd3f03696e fix: use frappe.datetime.str_to_user (#45216)
* fix: default_datetime_format

* fix: add_format_datetime

* fix: update to str_to_user  in point_of_sale/pos_controller.js

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

* fix: convert_to_str_to_user

* fix: linters

* fix: whitespace

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-01-24 12:16:47 +05:30
Sugesh G
6ec18fb40d fix: set expense_account and cost_center based on company in stock entry (#45159)
* fix: set expense_account and cost_center based on company in stock entry

* fix: remove is_perpetual_inventory_enabled validation for cost_center
2025-01-24 12:13:44 +05:30
ruthra kumar
5bf90dccbb Merge pull request #45412 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-24 11:05:12 +05:30
Frappe PR Bot
9888c62e13 fix: Swedish translations 2025-01-24 02:26:17 +05:30
Rohit Waghchaure
5088d8576f fix: valuation for batch 2025-01-23 20:38:14 +05:30
Raffael Meyer
d92f9330fa fix: get default stock uom (#45384)
Co-authored-by: Sagar Vora <sagar@resilient.tech>
2025-01-23 14:32:39 +01:00
mergify[bot]
d1329c2910 fix: add condition to check if item is delivered by supplier in make_purchase_order_for_default_supplier() (backport #45370) (#45409)
fix: add condition to check if item is delivered by supplier in make_purchase_order_for_default_supplier() (#45370)

(cherry picked from commit 69464ab7ff)

Co-authored-by: Shanuka Hewage <89955436+Shanuka-98@users.noreply.github.com>
2025-01-23 18:18:58 +05:30
meike289
1758e125e0 fix: fix creating documents from sales invoice (#45346)
Co-authored-by: Meike Nedwidek <nedwidek@kk-software.de>
2025-01-23 17:54:00 +05:30
ruthra kumar
85b6fdd067 Merge pull request #45248 from niyazrazak/patch-22
chore: consider currency precision in bank clearance
2025-01-23 16:57:28 +05:30
ruthra kumar
c889bdba0f chore: import statements and linter 2025-01-23 16:39:03 +05:30
ruthra kumar
766b5fa5ed Merge pull request #44790 from ruthra-kumar/configurable_posting_date_for_exc_gain_loss
refactor: configurable posting date for Exc Gain / Loss journal
2025-01-23 16:28:21 +05:30
ruthra kumar
3923b784e3 Merge pull request #45129 from marination/ac-match-simplify
fix: Wrong `bank_ac_no` filter + simplify logic in automatch
2025-01-23 16:27:49 +05:30
ruthra kumar
7b8a099d95 Merge pull request #44950 from mahsem/postal_code_move_and_fixes
fix: postal_code_move_and_fixes
2025-01-23 16:17:15 +05:30
ruthra kumar
a71718883e refactor: support JE posting date in semi-auto reconciilation tool 2025-01-23 16:11:35 +05:30
ruthra kumar
2f3281579a test: exc gain/loss posting date based on configuration 2025-01-23 14:44:40 +05:30
ruthra kumar
b2c3da135e refactor: only apply configuration on normal payments
patch to update default value
2025-01-23 14:12:24 +05:30
ruthra kumar
95af63e305 refactor: allow reconciliation date for exchange gain / loss 2025-01-23 13:48:09 +05:30
rohitwaghchaure
f79eea2261 Merge pull request #45394 from rohitwaghchaure/fixed-support-30031
fix: JobCardTimeLog' object has no attribute 'remaining_time_in_mins'
2025-01-23 13:22:17 +05:30
ruthra kumar
5257413a93 refactor: configurable posting date for Exc Gain / Loss journal 2025-01-23 13:06:30 +05:30
Rohit Waghchaure
41dda35db7 fix: JobCardTimeLog' object has no attribute 'remaining_time_in_mins' 2025-01-23 13:03:37 +05:30
Sanket322
50223c6bec fix: remove applied pricing rule 2025-01-23 11:55:12 +05:30
Sanket Shah
2a400dd3f8 perf: optimize DB calls with frappe.get_all (#45289)
* perf: reduce multiple db queries

* fix: use frappe._dict instread of extra iteration

---------

Co-authored-by: Sanket322 <shahsanket322003.com>
2025-01-23 11:29:41 +05:30
Frappe PR Bot
551fa500e8 fix: sync translations from crowdin (#45387)
* fix: Swedish translations

* fix: Turkish translations
2025-01-23 11:18:59 +05:30
Mihir Kandoi
fe43d20545 fix: added item_group filter in item_code field in stock balance report (#45340)
* fix: added item_group filter in item_code field in stock balance report

* feat: added filter to not show non stock items
2025-01-23 11:17:35 +05:30
rohitwaghchaure
8e18c572f4 Merge pull request #45367 from rohitwaghchaure/fixed-incorrect-calculation-of-qty
fix: batch qty calculation
2025-01-23 09:40:16 +05:30
Rohit Waghchaure
f07a71a882 fix: batch qty calculation 2025-01-22 21:06:43 +05:30
rohitwaghchaure
c5f21a5686 Merge pull request #45382 from rohitwaghchaure/fixed-support-29769
fix: precision issue causing incorrect status
2025-01-22 20:17:20 +05:30
Rohit Waghchaure
4a7586cc01 fix: precision issue causing incorrect status 2025-01-22 18:04:52 +05:30
ruthra kumar
3fbd2ca0d9 refactor: configurable posting date for Exc Gain / Loss journal 2025-01-22 17:54:00 +05:30
Diógenes Souza
b26f0b6633 fix: System was allowing to save payment schedule amount less than grand total (#45322)
* fix: System was allowing to save payment schedule amount less than grand_total

* style: After run pre-commit
2025-01-22 17:52:26 +05:30
Rethik M
05579959f2 fix: validate non-stock item for exchange loss/gain (#45306)
* fix: validate non-stock item

* test: add unit test to validate non-stock item exchange difference

* fix: use usd supplier
2025-01-22 17:44:53 +05:30
gavin
4481ca83ff fix: set preferred email in Employee via backend controller (#45320)
fix: set preferred email in Employee (backend)

Set "Preferred Email" for Employee via validate. Unset value when
prefered_contact_email is also unset.
2025-01-22 17:41:41 +05:30
Khushi Rawat
9ff3101b2d fix: added debounce to prevent multiple clicks (#45369)
* fix: added debounce to prevent multiple clicks

* fix: linters check
2025-01-22 17:26:38 +05:30
mergify[bot]
8b6a20d501 fix: precision on work order total qty (backport #45341) (#45361)
fix: precision on work order total qty (#45341)

* fix: precision on work order total qty

* chore: linters

(cherry picked from commit 53468202de)

Co-authored-by: Dany Robert <rtdany10@gmail.com>
2025-01-22 17:25:50 +05:30
Raffael Meyer
be2593bb51 refactor!: remove redundant title field from sales transactions (#45115)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2025-01-22 11:35:41 +00:00
Ejaaz Khan
5207917993 feat: set default view as tree-view (#45365)
feat: set default view as treeview
2025-01-22 16:18:31 +05:30
Deepesh Garg
84d379914c Merge pull request #45371 from deepeshgarg007/closing_balance_patch_rerun
fix: Do no query GLs if no PCVs are posted
2025-01-22 14:02:23 +05:30
Deepesh Garg
f4d1a54588 fix: Do no query GLs if no PCVs are posted 2025-01-22 13:46:37 +05:30
ruthra kumar
1c6a7830c3 Merge pull request #45363 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-22 12:12:43 +05:30
Frappe PR Bot
4455312b73 fix: Turkish translations 2025-01-22 01:34:43 +05:30
Sugesh393
36bae55299 chore: update variable names for improved readability 2025-01-21 17:38:24 +05:30
Niklas Liechti
dedb96d337 refactor: return job_id to be able to react to it when job is finished. (#45111) 2025-01-21 17:38:12 +05:30
ruthra kumar
d73c17aa98 Merge pull request #45242 from aerele/pos-invoice-item
fix: include pos invoice in modifing key for returned item validation
2025-01-21 16:34:59 +05:30
ruthra kumar
4f55356c79 Merge pull request #45215 from aerele/handle_aed_exchange_rate_manually
fix: calculate AED exchange rate based on pegged value with USD
2025-01-21 13:58:21 +05:30
Deepesh Garg
993f40fa43 perf: Ignore is_opening column in GL Queries (#45327)
* perf: Ignore is_opening column in GL Queries

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes

* chore: Remove unwanted changes
2025-01-21 13:45:25 +05:30
ruthra kumar
2e535955b3 refactor: use dictionary for better expandability 2025-01-21 12:15:26 +05:30
ruthra kumar
dd923332cb refactor: fix type error 2025-01-21 12:15:26 +05:30
Frappe PR Bot
7b0c21e989 fix: sync translations from crowdin (#45323)
* fix: Swedish translations

* fix: Turkish translations

* fix: French translations

* fix: Spanish translations

* fix: Arabic translations

* fix: Hungarian translations

* fix: Polish translations

* fix: Russian translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Chinese Simplified translations

* fix: Persian translations

* fix: Bosnian translations

* fix: German translations

* fix: Esperanto translations

* fix: Swedish translations

* fix: Turkish translations

* fix: Persian translations
2025-01-20 22:57:21 +01:00
rohitwaghchaure
6c32397313 Merge pull request #45282 from mihir-kandoi/44976
feat: Add corrective job card operating cost as additional costs in s…
2025-01-20 22:48:46 +05:30
rohitwaghchaure
5638dac414 Merge pull request #45344 from iamejaaz/broken-bom-tree
fix: broken image issue in BOM Tree
2025-01-20 22:48:05 +05:30
Sanket322
e2a32b7257 fix: use user defined discount amount or default 2025-01-20 18:01:31 +05:30
Ejaaz Khan
7c6e279599 fix: broken image issue in BOM Tree 2025-01-20 17:49:18 +05:30
Diptanil Saha
68fb1b28eb fix: set invoice start date to subscription start date (#45342) 2025-01-20 17:06:02 +05:30
Mihir Kandoi
47f8a86003 fix: logical error in where condition of qb query 2025-01-20 12:41:28 +05:30
ruthra kumar
5fb158a6f7 Merge pull request #45202 from DaizyModi/fix-party-bank-acc-in-pe
fix: Correct Party Bank Account mapping in `Payment Entry` from Transactional Doctypes
2025-01-20 12:22:10 +05:30
ruthra kumar
30e87c37dc Merge pull request #45334 from mahsem/translation_fixes
fix: translation fixes
2025-01-20 12:11:32 +05:30
ruthra kumar
f9200a9575 Merge pull request #45167 from aerele/validate-sales-person
fix: validate linked sales person
2025-01-20 11:49:14 +05:30
mahsem
3697ba0772 fix: linters 2025-01-19 21:00:02 +01:00
mahsem
1d81a9f933 fix: translation fixes 2025-01-19 20:52:58 +01:00
Frappe PR Bot
da34d7923d chore: update POT file (#45331) 2025-01-19 11:47:50 +01:00
rohitwaghchaure
04364d680f Merge pull request #44783 from rohitwaghchaure/fixed-support-27273
fix: do not reset picked items
2025-01-18 14:22:54 +05:30
Rohit Waghchaure
34a80bfcd3 fix: do not reset picked items 2025-01-18 12:44:30 +05:30
rohitwaghchaure
76ba17808b Merge pull request #45298 from rohitwaghchaure/fixed-qi-company
fix: company in quality inspection
2025-01-18 12:14:54 +05:30
Rohit Waghchaure
397cd79e1e fix: company in quality inspection 2025-01-18 11:09:00 +05:30
Diptanil Saha
97e3770872 fix: fixed typo in manufacturing settings and field rename (#45238)
* chore: field rename and patch entry

* chore: patch file rename and description improvement

---------

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
2025-01-17 11:54:39 +00:00
ruthra kumar
8f9b5aaae7 Merge pull request #44811 from sagarvora/fix-pcv-patch-99
fix!: ensure multiple PCVs in same fiscal year are considered in patch
2025-01-17 17:00:40 +05:30
Diptanil Saha
d13c03676b chore: quickbooks migrator integration removal (#45240) 2025-01-17 16:53:36 +05:30
Diptanil Saha
98d401bee4 fix: fetching items from blanket order to sales/purchase order (#45262)
* fix: blanket order with zero item quantity

* fix: item quantity validation

* refactor: resolve linter issue
2025-01-17 16:53:06 +05:30
Nabin Hait
ec487c14d9 fix: Ambiguous column error while creating Sales Return (#45275) 2025-01-17 16:51:07 +05:30
Nabin Hait
2d45d0e5d5 fix: updated modified timestamp for stock entry type (#45280) 2025-01-17 16:50:43 +05:30
Lakshit Jain
ada272a29b fix: round off tax withholding amount (#45271) 2025-01-17 16:50:15 +05:30
Patrick Eißler
8d66142865 fix(Project): re-phrase welcome email (#45175) 2025-01-17 12:17:32 +01:00
Sagar Vora
84e0b41c4f fix: ensure multiple PCVs in same fiscal year are considered in patch 2025-01-17 16:44:06 +05:30
Sanket Shah
19c8708e5e fix: don't update party-type on change of cost center in Journal Entry (#45291)
fix: don't update party-type on change of cost center

Co-authored-by: Sanket322 <shahsanket322003.com>
2025-01-17 16:42:04 +05:30
Ejaaz Khan
aa38895caf feat: add option to update modified on communication recieved (#45307) 2025-01-17 16:40:35 +05:30
rohitwaghchaure
9163f60191 fix: sales return for multi-uom (#45303) 2025-01-17 16:35:59 +05:30
Sugesh393
454067198e fix: set company related values 2025-01-17 11:46:17 +05:30
Sugesh393
c94091d68f test: add new unit test for company validation in accounting dimension 2025-01-17 11:45:51 +05:30
Sugesh393
60efd3e219 feat: add company level validation for accounting dimension 2025-01-17 11:41:52 +05:30
Frappe PR Bot
0e7e9b5f0a fix: sync translations from crowdin (#45299)
* fix: Turkish translations

* fix: Persian translations
2025-01-16 22:49:23 +01:00
HarryPaulo
87de5c7450 fix: prevent unnecessary db.commit for contact insert [Linters] 2025-01-16 17:51:22 -03:00
HarryPaulo
5f15b0b65b fix: prevent unnecessary db.commit 2025-01-16 17:40:54 -03:00
Mihir Kandoi
063a205e5a refactor: added condition which checks for corrective operation setting 2025-01-16 20:55:27 +05:30
rohitwaghchaure
43ce185429 Merge pull request #45283 from rohitwaghchaure/fixed-github-45219
fix: Does not allow to create Sub-Asseblies of Sub Assemblies
2025-01-16 14:57:56 +05:30
Rohit Waghchaure
318a945d66 fix: Does not allow to create Sub-Asseblies of Sub Assemblies 2025-01-16 13:51:11 +05:30
ruthra kumar
6628d290de Merge pull request #45285 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-16 11:10:32 +05:30
Mihir Kandoi
4fb48b7f22 test: Added test for new feature 2025-01-16 10:08:47 +05:30
Frappe PR Bot
ed2764ab8d fix: Persian translations 2025-01-15 23:49:21 +05:30
Frappe PR Bot
a7cd17ac8b fix: Turkish translations 2025-01-15 23:49:14 +05:30
Sugesh393
484ecf2479 test: add new unit test to check payments amount of pos_invoice returns 2025-01-15 18:27:57 +05:30
Sugesh393
2af6fca7fa fix: set party_account_currency for pos_invoice returns 2025-01-15 18:20:20 +05:30
Mihir Kandoi
2bf10f68a8 feat: Add corrective job card operating cost as additional costs in stock entry 2025-01-15 18:14:19 +05:30
rohitwaghchaure
cb91e8e69e Merge pull request #45278 from rohitwaghchaure/fixed-github-45246
fix: status of the serial no for the raw materials
2025-01-15 18:13:34 +05:30
Rohit Waghchaure
9607b16dcf fix: status of the serial no for the raw materials 2025-01-15 17:53:11 +05:30
Lakshit Jain
37a5767be5 fix: check if tds deducted based on Purchase Taxes and Charges (#45161) 2025-01-15 15:31:56 +05:30
rohitwaghchaure
195254756f Merge pull request #45270 from mihir-kandoi/st29053
fix: Skip WIP Warehouse transfer
2025-01-15 15:05:49 +05:30
Lakshit Jain
a4453fb77b fix: use currency defined in plan for subscription invoice (#45104) 2025-01-15 14:52:25 +05:30
Mihir Kandoi
6edb454eea test: Added new test to check wip skip 2025-01-15 13:49:11 +05:30
rohitwaghchaure
001d1eb3f9 Merge pull request #45259 from rohitwaghchaure/fixed-support-29045
fix: incorrect valuation for sales return with different warhouse
2025-01-14 18:33:34 +05:30
Rohit Waghchaure
3a2e816759 fix: incorrect valuation for sales return with different warhouse 2025-01-14 18:11:17 +05:30
ruthra kumar
ce9c606f71 refactor: allow users to configure interval for Semi-Auto payment reconciliation (#45211)
* refactor: configurable interval for reconciliation trigger

* refactor: set default value for interval

* refactor: configurable queue size

* refactor: use patch to setup default cron job

* refactor: use 'after_migrate' to setup cron for reconciliation

User specified interval will be used

* chore: type casting

* refactor: use scheduler_event to persist cron

* chore: rename field

* chore: use configured queue size

* chore: remove unwanted field
2025-01-14 16:59:15 +05:30
Mihir Kandoi
09d26a835f fix: tests 2025-01-14 16:27:49 +05:30
ruthra kumar
aea69af4ff Merge pull request #45182 from ruthra-kumar/configurable_reconciliation_dates
refactor: configurable reconciliation dates for Advance Payments
2025-01-14 15:18:46 +05:30
ruthra kumar
fd442a36a9 Merge pull request #45260 from ruthra-kumar/incorrect_label
fix: incorrect label in Item-wise sales register
2025-01-14 13:55:20 +05:30
ruthra kumar
9ee5fcc602 refactor: only update reconcile_effect_on advance in separate acc 2025-01-14 13:52:21 +05:30
ruthra kumar
d9013e1054 fix: incorrect label in Item-wise sales register 2025-01-14 13:34:12 +05:30
rohitwaghchaure
9bb7d4e428 Merge pull request #45241 from rohitwaghchaure/fixed-support-28979
fix: auto fetch batch and serial no for draft stock transactions
2025-01-14 12:35:17 +05:30
mahsem
a3165c5719 fix: change string to be able to translate (#45090)
* fix: change_string_to_translate

* fix: debit note translation

* chore: update 'modified' field

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2025-01-14 06:26:01 +00:00
ruthra kumar
601bc3026d Merge pull request #45249 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-14 11:17:27 +05:30
ruthra kumar
aeedaae761 Merge pull request #45078 from Sanket322/cancel_subscription
fix: don't create invoice if `current_invoice_start ` is in future
2025-01-14 10:12:33 +05:30
ruthra kumar
2e8723407f Merge pull request #45074 from Sanket322/set_shipping_address
fix: set billing and shipping address on change of company
2025-01-14 10:04:13 +05:30
ruthra kumar
4b483e176d Merge pull request #45001 from ljain112/fix-tds-excess
fix: deduct tds on excess amount if checked
2025-01-14 10:02:06 +05:30
ruthra kumar
f7a93640b0 Merge pull request #44798 from aerele/apply_pricing_rule
fix: update  discounting on mixed conditions
2025-01-14 09:58:03 +05:30
Frappe PR Bot
8f8cddb03c fix: Persian translations 2025-01-13 23:39:50 +05:30
Frappe PR Bot
ea4526ad30 fix: Turkish translations 2025-01-13 23:39:40 +05:30
Frappe PR Bot
6c5a9f5a74 fix: sync translations from crowdin (#45174) 2025-01-13 18:02:50 +01:00
Mihir Kandoi
bbb5f8056b fix: Skip WIP Warehouse transfer 2025-01-13 21:32:34 +05:30
NIYAZ RAZAK
83bb3926b1 chore: consider currency precision 2025-01-13 18:53:59 +03:00
ruthra kumar
9fa1865cb7 refactor: backwards compatibility 2025-01-13 17:17:18 +05:30
Rohit Waghchaure
88ab9be79c fix: auto fetch batch and serial no for draft stock transactions 2025-01-13 17:17:03 +05:30
venkat102
2936139c79 fix: include pos invoice in modifing key for returned item validation 2025-01-13 16:51:25 +05:30
ruthra kumar
a4271aa5d1 refactor: save reconcile effect on reference table 2025-01-13 16:20:58 +05:30
ruthra kumar
7e7775aa44 refactor: store reconciliation date in reference
Helps with reposting
2025-01-13 15:56:41 +05:30
Diptanil Saha
e529f82392 fix: batch number search on pos (#45209)
* fix: price of items with batch number not having seperate item price on pos search bar

* fix: introduced batch number based sorting
2025-01-13 12:06:46 +05:30
Ejaaz Khan
eea0eff001 fix: ui issues across multiple pages (#45224)
* fix: ui issues across multiple pages

* refactor: change cur_from to frm
2025-01-13 11:56:59 +05:30
Ejaaz Khan
6eb3b0b1c2 refactor: remove columns from list in child table (#45189) 2025-01-13 11:54:44 +05:30
Ejaaz Khan
53c282c86e refactor: remove party type from general ledger print format (#45196)
refactor: remove party type from print format
2025-01-13 11:52:45 +05:30
Frappe PR Bot
959434601d chore: update POT file (#45231) 2025-01-12 11:27:09 +01:00
rohitwaghchaure
c6c7d7832a Merge pull request #45207 from rohitwaghchaure/fixed-support-29140
fix: incorrect valuation rate for PI based revaluation
2025-01-11 10:07:54 +05:30
rohitwaghchaure
edb254e43e Merge pull request #45214 from rohitwaghchaure/fixed-support-29172
fix: delivery_document_no column issue
2025-01-11 10:07:26 +05:30
Kavin
455ef6f084 fix: calculate AED exchange rate based on pegged value with USD 2025-01-10 18:20:47 +05:30
Rohit Waghchaure
61efb2bb39 fix: delivery_document_no column issue 2025-01-10 18:19:48 +05:30
ruthra kumar
856ec08484 Merge pull request #45195 from sokumon/coa-ui
fix: coa actions cleanup
2025-01-10 16:08:19 +05:30
rohitwaghchaure
31005c5984 Merge pull request #45076 from frappe/valuation_in_ageing_report
feat: Added valuation of quantity for each age group in stock ageing …
2025-01-10 14:25:50 +05:30
Rohit Waghchaure
14ce2337df fix: incorrect valuation rate for PI based revaluation 2025-01-10 14:20:48 +05:30
ruthra kumar
e0517852bc test: ensure reconciliation date config takes effect 2025-01-10 12:17:13 +05:30
rohitwaghchaure
e4d3235b9c Merge pull request #45204 from frappe/mergify/bp/develop/pr-45197
fix: precision loss causing process loss variance (backport #45197)
2025-01-10 12:00:37 +05:30
rohitwaghchaure
5ccf4a1783 chore: fix conflicts 2025-01-10 11:25:53 +05:30
FATHIH MOHAMMED
1ef9f7f8fd fix: precision loss causing process loss variance
(cherry picked from commit d84601b2a3)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-01-10 05:49:59 +00:00
DaizyModi
376bdc75f4 fix: Correct Party Bank Account mapping in Payment Entry 2025-01-10 11:06:39 +05:30
ruthra kumar
c8e93e7a61 refactor: hide old checkbox 2025-01-09 20:41:29 +05:30
ruthra kumar
bb8d2c994c refactor: payment entry to handle posting date on configuation 2025-01-09 20:41:29 +05:30
ruthra kumar
fb6c72a247 refactor: test cases updated 2025-01-09 20:41:29 +05:30
ruthra kumar
a8a8ac71b6 refactor: patch to migrate checkbox to select 2025-01-09 20:41:27 +05:30
ruthra kumar
8b2c981fc3 refactor: introduce select fields in company and payment entry 2025-01-09 20:22:05 +05:30
sokumon
108a91d788 fix: coa actions cleanup 2025-01-09 16:49:24 +05:30
rohitwaghchaure
1f1c01d88e Merge pull request #44988 from frappe/35225
feat: Validate sub assembly and material request items in Production …
2025-01-09 16:28:29 +05:30
rohitwaghchaure
90339511aa Merge pull request #45190 from diptanilsaha/fix-typo-in-manufacturing-settings
fix: typo in manufacturing settings
2025-01-09 16:26:01 +05:30
diptanilsaha
a9b761f862 fix: typo in manufacturing settings 2025-01-09 16:05:40 +05:30
rohitwaghchaure
4ec824d71e Merge pull request #45183 from rohitwaghchaure/fixed-support-29059
fix: not able to see create Quality Inspection button
2025-01-09 14:36:52 +05:30
rohitwaghchaure
bb9dd7b8cc Merge pull request #45180 from rohitwaghchaure/fixed-support-28867
fix: do not add ordered items from Quotation to new Sales Order
2025-01-09 14:34:13 +05:30
Rohit Waghchaure
b291835ccd fix: not able to see create Quality Inspection button 2025-01-09 14:31:01 +05:30
Rohit Waghchaure
2e930eb97b fix: do not add ordered items from Quotation to new Sales Order 2025-01-09 13:52:34 +05:30
rohitwaghchaure
00dadc1a89 Merge pull request #45177 from rohitwaghchaure/fixed-support-29008
fix: timeout error for work order
2025-01-09 13:47:17 +05:30
Rohit Waghchaure
b4ceda6f2c fix: timeout error for work order 2025-01-09 13:27:43 +05:30
Khushi Rawat
7ea73d8265 chore: removal of decapitalization feature (#45162)
* chore: removal of decapitalization feature

* fix: rearrangement of asset capitalization doctype fields
2025-01-08 20:44:12 +05:30
Raffael Meyer
9e760e54a5 fix(Timesheet): ignore permissions when updating Task and Project (#45168) 2025-01-08 13:36:31 +01:00
Sudharsanan11
e614f07795 fix: validate linked sales person 2025-01-08 13:51:33 +05:30
Ejaaz Khan
81d8f257aa refactor: validate due date code and message according to doctype (#45126)
* refactor: change message of date comparision and refactor code

* refactor: commonify function call for sales and purchase invoice

* refactor: remove redundant mandatory error validation
2025-01-08 12:49:02 +05:30
Mihir Kandoi
dbb572eec1 test: Valuation of ageing stock 2025-01-08 11:52:24 +05:30
ruthra kumar
2b453219fc Merge pull request #45148 from frappe/l10n_develop
fix: sync translations from crowdin
2025-01-08 09:01:02 +05:30
ruthra kumar
ca44a31420 Merge pull request #45154 from ruthra-kumar/remove_possible_deadlock_in_auto_reconcile
fix: possible deadlock while using auto reconciliation
2025-01-08 09:00:42 +05:30
ruthra kumar
4620025dcd chore: remove 'Experimental' tag 2025-01-08 08:22:37 +05:30
ruthra kumar
5df9a8ab99 Merge pull request #45112 from marination/bank-reco-bank-balance
fix: Missing company filter breaks `get_account_balance` in Bank Reco
2025-01-08 08:16:21 +05:30
Frappe PR Bot
731822efac fix: Spanish translations 2025-01-07 21:48:22 +05:30
rohitwaghchaure
a24d7e8ecd Merge pull request #45144 from rohitwaghchaure/fixed-support-28796
fix: issue in returning components against the SCO
2025-01-07 18:31:18 +05:30
Rohit Waghchaure
729ce1dc50 fix: issue in returning components against the SCO 2025-01-07 18:11:56 +05:30
Diptanil Saha
31dd32dcdf fix: serial and batch no. buttons on pos (#45048) 2025-01-07 18:06:02 +05:30
Khushi Rawat
6850019649 feat: work in progress status for asset (#45066)
* feat: work in progress status for asset

* fix: test case correction

* fix(patch): added patch to update status of assets

* fix: updated tests
2025-01-07 17:38:21 +05:30
Diptanil Saha
2788739c1e feat: pos configuration for print receipt on complete order (#45024) 2025-01-07 17:35:01 +05:30
Diptanil Saha
9f77793f16 chore: removal of tally migration feature (#45100) 2025-01-07 17:28:46 +05:30
Ejaaz Khan
a0f17f8e73 refactor: change sales invoice button position (#45130) 2025-01-07 17:17:59 +05:30
Venkatesh
dc5bff9008 fix: ignore crm deal in tax_rule search filter (#45134) 2025-01-07 17:11:31 +05:30
marination
8521796811 fix: Wrong bank_ac_no filter + simplify convoluted logic 2025-01-07 13:28:55 +05:30
Sanket322
61d4593236 fix: minor update for readability 2025-01-07 12:44:29 +05:30
marination
d7bf73cffa fix: Override pre-commit behaviour due to conflicts with CI 2025-01-06 21:14:41 +05:30
marination
8de0fe78ea fix: Missing company filter breaks get_account_balance in Bank Reco 2025-01-06 20:27:06 +05:30
Mihir Kandoi
87f1f6e15c fix: Attempt to fix status updater 1 2025-01-06 19:47:44 +05:30
Mihir Kandoi
1f2d7da426 fix: Fixed final test case 2025-01-04 15:22:28 +05:30
Mihir Kandoi
f996f71d16 fix: Fixed more test cases 2025-01-04 15:17:31 +05:30
Mihir Kandoi
839b79ffd0 fix: Test case for ageing report 2025-01-03 20:00:36 +05:30
Sanket322
ce99764772 fix: pass right existing address 2025-01-03 18:04:56 +05:30
Sanket322
058fdca981 fix: don't create invoice if invoice start date is in future 2025-01-03 17:44:05 +05:30
Mihir Kandoi
2f80c4dee5 feat: Added valuation of quantity for each age group in stock ageing report 2025-01-03 16:51:44 +05:30
Sanket322
f46f1bead4 fix: set billing and shipping address on change of company 2025-01-03 16:44:24 +05:30
Mihir Kandoi
5dacfd5cda fix: Test case and refactored some code 2025-01-02 12:28:01 +05:30
Mihir Kandoi
015fd4a05b fix: Made requested changes by mentor and fixed some bugs in Production Plan Summary report 2025-01-01 16:51:48 +05:30
ljain112
a203e3ffaf fix: deduct tds on excess amount if checked 2024-12-31 13:46:50 +05:30
Mihir Kandoi
22d38c2af4 feat: Validate sub assembly and material request items in Production Plan and fix Production Plan summary reports not showing correct received quantity from subcontracted POs 2024-12-30 20:12:14 +05:30
mahsem
185bbb4c20 fix: postal_code_move_and_fixes 2024-12-28 09:25:30 +01:00
DHINESH00
547c8004eb fix: Semgrep rules 2024-12-23 11:23:23 +05:30
DHINESH00
d541259da9 fix: update discounting on mixed conditions 2024-12-19 17:15:11 +05:30
243 changed files with 27178 additions and 29426 deletions

View File

@@ -133,7 +133,7 @@ jobs:
run: cat ~/frappe-bench/bench_start.log || true
- name: Upload coverage data
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with:
name: coverage-${{ matrix.container }}
@@ -149,7 +149,7 @@ jobs:
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Upload coverage data
uses: codecov/codecov-action@v4

View File

@@ -3,6 +3,7 @@
"allow_copy": 1,
"allow_import": 1,
"creation": "2013-01-30 12:49:46",
"default_view": "Tree",
"description": "Heads (or groups) against which Accounting Entries are made and balances are maintained.",
"doctype": "DocType",
"document_type": "Setup",
@@ -194,7 +195,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2024-08-19 15:19:11.095045",
"modified": "2025-01-22 10:40:35.766017",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",

View File

@@ -10,6 +10,7 @@ frappe.treeview_settings["Account"] = {
fieldtype: "Select",
options: erpnext.utils.get_tree_options("company"),
label: __("Company"),
render_on_toolbar: true,
default: erpnext.utils.get_tree_default("company"),
on_change: function () {
var me = frappe.treeview_settings["Account"].treeview;
@@ -182,7 +183,9 @@ frappe.treeview_settings["Account"] = {
function () {
frappe.set_route("Tree", "Cost Center", { company: get_company() });
},
__("View")
__("View"),
"default",
true
);
treeview.page.add_inner_button(
@@ -190,31 +193,12 @@ frappe.treeview_settings["Account"] = {
function () {
frappe.set_route("Form", "Opening Invoice Creation Tool", { company: get_company() });
},
__("View")
__("View"),
"default",
true
);
treeview.page.add_inner_button(
__("Period Closing Voucher"),
function () {
frappe.set_route("List", "Period Closing Voucher", { company: get_company() });
},
__("View")
);
treeview.page.add_inner_button(
__("Journal Entry"),
function () {
frappe.new_doc("Journal Entry", { company: get_company() });
},
__("Create")
);
treeview.page.add_inner_button(
__("Company"),
function () {
frappe.new_doc("Company");
},
__("Create")
);
treeview.page.add_divider_to_button_group(__("View"));
// financial statements
for (let report of [
@@ -231,7 +215,7 @@ frappe.treeview_settings["Account"] = {
function () {
frappe.set_route("query-report", report, { company: get_company() });
},
__("Financial Statements")
__("View")
);
}
},

View File

@@ -0,0 +1,532 @@
{
"country_code": "ch",
"name": "240812 Schulkontenrahmen VEB - DE",
"tree": {
"Aktiven": {
"account_number": "1",
"is_group": 1,
"root_type": "Asset",
"Umlaufvermögen": {
"account_number": "10",
"is_group": 1,
"Flüssige Mittel": {
"account_number": "100",
"is_group": 1,
"Kasse": {
"account_number": "1000",
"account_type": "Cash"
},
"Bankguthaben": {
"account_number": "1020",
"account_type": "Bank"
}
},
"Kurzfristig gehaltene Aktiven mit Börsenkurs": {
"account_number": "106",
"is_group": 1,
"Wertschriften": {
"account_number": "1060"
},
"Wertberichtigungen Wertschriften": {
"account_number": "1069"
}
},
"Forderungen aus Lieferungen und Leistungen": {
"account_number": "110",
"is_group": 1,
"Forderungen aus Lieferungen und Leistungen (Debitoren)": {
"account_number": "1100"
},
"Delkredere": {
"account_number": "1109"
}
},
"Übrige kurzfristige Forderungen": {
"account_number": "114",
"is_group": 1,
"Vorschüsse und Darlehen": {
"account_number": "1140"
},
"Wertberichtigungen Vorschüsse und Darlehen": {
"account_number": "1149"
},
"Vorsteuer MWST Material, Waren, Dienstleistungen, Energie": {
"account_number": "1170"
},
"Vorsteuer MWST Investitionen, übriger Betriebsaufwand": {
"account_number": "1171"
},
"Verrechnungssteuer": {
"account_number": "1176"
},
"Forderungen gegenüber Sozialversicherungen und Vorsorgeeinrichtungen": {
"account_number": "1180"
},
"Quellensteuer": {
"account_number": "1189"
},
"Sonstige kurzfristige Forderungen": {
"account_number": "1190"
},
"Wertberichtigungen sonstige kurzfristige Forderungen": {
"account_number": "1199"
}
},
"Vorräte und nicht fakturierte Dienstleistungen": {
"account_number": "120",
"is_group": 1,
"Handelswaren": {
"account_number": "1200"
},
"Rohstoffe": {
"account_number": "1210"
},
"Werkstoffe": {
"account_number": "1220"
},
"Hilfs- und Verbrauchsmaterial": {
"account_number": "1230"
},
"Handelswaren in Konsignation": {
"account_number": "1250"
},
"Fertige Erzeugnisse": {
"account_number": "1260"
},
"Unfertige Erzeugnisse": {
"account_number": "1270"
},
"Nicht fakturierte Dienstleistungen": {
"account_number": "1280"
}
},
"Aktive Rechnungsabgrenzungen": {
"account_number": "130",
"is_group": 1,
"Bezahlter Aufwand des Folgejahres": {
"account_number": "1300"
},
"Noch nicht erhaltener Ertrag": {
"account_number": "1301"
}
}
},
"Anlagevermögen": {
"account_number": "14",
"is_group": 1,
"Finanzanlagen": {
"account_number": "140",
"is_group": 1,
"Wertschriften": {
"account_number": "1400"
},
"Wertberichtigungen Wertschriften": {
"account_number": "1409"
},
"Darlehen": {
"account_number": "1440"
},
"Hypotheken": {
"account_number": "1441"
},
"Wertberichtigungen langfristige Forderungen": {
"account_number": "1449"
}
},
"Beteiligungen": {
"account_number": "148",
"is_group": 1,
"Beteiligungen": {
"account_number": "1480"
},
"Wertberichtigungen Beteiligungen": {
"account_number": "1489"
}
},
"Mobile Sachanlagen": {
"account_number": "150",
"is_group": 1,
"Maschinen und Apparate": {
"account_number": "1500"
},
"Wertberichtigungen Maschinen und Apparate": {
"account_number": "1509"
},
"Mobiliar und Einrichtungen": {
"account_number": "1510"
},
"Wertberichtigungen Mobiliar und Einrichtungen": {
"account_number": "1519"
},
"Büromaschinen, Informatik, Kommunikationstechnologie": {
"account_number": "1520"
},
"Wertberichtigungen Büromaschinen, Informatik, Kommunikationstechnologie": {
"account_number": "1529"
},
"Fahrzeuge": {
"account_number": "1530"
},
"Wertberichtigungen Fahrzeuge": {
"account_number": "1539"
},
"Werkzeuge und Geräte": {
"account_number": "1540"
},
"Wertberichtigungen Werkzeuge und Geräte": {
"account_number": "1549"
}
},
"Immobile Sachanlagen": {
"account_number": "160",
"is_group": 1,
"Geschäftsliegenschaften": {
"account_number": "1600"
},
"Wertberichtigungen Geschäftsliegenschaften": {
"account_number": "1609"
}
},
"Immaterielle Werte": {
"account_number": "170",
"is_group": 1,
"Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
"account_number": "1700"
},
"Wertberichtigungen Patente, Know-how, Lizenzen, Rechte, Entwicklungen": {
"account_number": "1709"
},
"Goodwill": {
"account_number": "1770"
},
"Wertberichtigungen Goodwill": {
"account_number": "1779"
}
},
"Nicht einbezahltes Grund-, Gesellschafter- oder Stiftungskapital": {
"account_number": "180",
"is_group": 1,
"Nicht einbezahltes Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
"account_number": "1850"
}
}
}
},
"Passiven": {
"account_number": "2",
"is_group": 1,
"root_type": "Liability",
"Kurzfristiges Fremdkapital": {
"account_number": "20",
"is_group": 1,
"Verbindlichkeiten aus Lieferungen und Leistungen": {
"account_number": "200",
"is_group": 1,
"Verbindlichkeiten aus Lieferungen und Leistungen (Kreditoren)": {
"account_number": "2000"
},
"Erhaltene Anzahlungen": {
"account_number": "2030"
}
},
"Kurzfristige verzinsliche Verbindlichkeiten": {
"account_number": "210",
"is_group": 1,
"Bankverbindlichkeiten": {
"account_number": "2100"
},
"Verbindlichkeiten aus Finanzierungsleasing": {
"account_number": "2120"
},
"Übrige verzinsliche Verbindlichkeiten": {
"account_number": "2140"
}
},
"Übrige kurzfristige Verbindlichkeiten": {
"account_number": "220",
"is_group": 1,
"Geschuldete MWST (Umsatzsteuer)": {
"account_number": "2200"
},
"Abrechnungskonto MWST": {
"account_number": "2201"
},
"Verrechnungssteuer": {
"account_number": "2206"
},
"Direkte Steuern": {
"account_number": "2208"
},
"Sonstige kurzfristige Verbindlichkeiten": {
"account_number": "2210"
},
"Beschlossene Ausschüttungen": {
"account_number": "2261"
},
"Sozialversicherungen und Vorsorgeeinrichtungen": {
"account_number": "2270"
},
"Quellensteuer": {
"account_number": "2279"
}
},
"Passive Rechnungsabgrenzungen und kurzfristige Rückstellungen": {
"account_number": "230",
"is_group": 1,
"Noch nicht bezahlter Aufwand": {
"account_number": "2300"
},
"Erhaltener Ertrag des Folgejahres": {
"account_number": "2301"
},
"Kurzfristige Rückstellungen": {
"account_number": "2330"
}
}
},
"Langfristiges Fremdkapital": {
"account_number": "24",
"is_group": 1,
"Langfristige verzinsliche Verbindlichkeiten": {
"account_number": "240",
"is_group": 1,
"Bankverbindlichkeiten": {
"account_number": "2400"
},
"Verbindlichkeiten aus Finanzierungsleasing": {
"account_number": "2420"
},
"Obligationenanleihen": {
"account_number": "2430"
},
"Darlehen": {
"account_number": "2450"
},
"Hypotheken": {
"account_number": "2451"
}
},
"Übrige langfristige Verbindlichkeiten": {
"account_number": "250",
"is_group": 1,
"Übrige langfristige Verbindlichkeiten (unverzinslich)": {
"account_number": "2500"
}
},
"Rückstellungen sowie vom Gesetz vorgesehene ähnliche Positionen": {
"account_number": "260",
"is_group": 1,
"Rückstellungen": {
"account_number": "2600"
}
}
},
"Eigenkapital (juristische Personen)": {
"account_number": "28",
"is_group": 1,
"Grund-, Gesellschafter- oder Stiftungskapital": {
"account_number": "280",
"is_group": 1,
"Aktien-, Stamm-, Anteilschein- oder Stiftungskapital": {
"account_number": "2800"
}
},
"Reserven und Jahresgewinn oder Jahresverlust": {
"account_number": "290",
"is_group": 1,
"Gesetzliche Kapitalreserve": {
"account_number": "2900"
},
"Reserve für eigene Kapitalanteile": {
"account_number": "2930"
},
"Aufwertungsreserve": {
"account_number": "2940"
},
"Gesetzliche Gewinnreserve": {
"account_number": "2950"
},
"Freiwillige Gewinnreserven": {
"account_number": "2960"
},
"Gewinnvortrag oder Verlustvortrag": {
"account_number": "2970"
},
"Jahresgewinn oder Jahresverlust": {
"account_number": "2979"
},
"Eigene Aktien, Stammanteile oder Anteilscheine (Minusposten)": {
"account_number": "2980"
}
}
}
},
"Betrieblicher Ertrag aus Lieferungen und Leistungen": {
"account_number": "3",
"is_group": 1,
"root_type": "Income",
"Produktionserlöse": {
"account_number": "3000"
},
"Handelserlöse": {
"account_number": "3200"
},
"Dienstleistungserlöse": {
"account_number": "3400"
},
"Übrige Erlöse aus Lieferungen und Leistungen": {
"account_number": "3600"
},
"Eigenleistungen": {
"account_number": "3700"
},
"Eigenverbrauch": {
"account_number": "3710"
},
"Erlösminderungen": {
"account_number": "3800"
},
"Verluste Forderungen (Debitoren), Veränderung Delkredere": {
"account_number": "3805"
},
"Bestandesänderungen unfertige Erzeugnisse": {
"account_number": "3900"
},
"Bestandesänderungen fertige Erzeugnisse": {
"account_number": "3901"
},
"Bestandesänderungen nicht fakturierte Dienstleistungen": {
"account_number": "3940"
}
},
"Aufwand für Material, Handelswaren, Dienstleistungen und Energie": {
"account_number": "4",
"is_group": 1,
"root_type": "Expense",
"Materialaufwand Produktion": {
"account_number": "4000"
},
"Handelswarenaufwand": {
"account_number": "4200"
},
"Aufwand für bezogene Dienstleistungen": {
"account_number": "4400"
},
"Energieaufwand zur Leistungserstellung": {
"account_number": "4500"
},
"Aufwandminderungen": {
"account_number": "4900"
}
},
"Personalaufwand": {
"account_number": "5",
"is_group": 1,
"root_type": "Expense",
"Lohnaufwand": {
"account_number": "5000"
},
"Sozialversicherungsaufwand": {
"account_number": "5700"
},
"Übriger Personalaufwand": {
"account_number": "5800"
},
"Leistungen Dritter": {
"account_number": "5900"
}
},
"Übriger betrieblicher Aufwand, Abschreibungen und Wertberichtigungen sowie Finanzergebnis": {
"account_number": "6",
"is_group": 1,
"root_type": "Expense",
"Raumaufwand": {
"account_number": "6000"
},
"Unterhalt, Reparaturen, Ersatz mobile Sachanlagen": {
"account_number": "6100"
},
"Leasingaufwand mobile Sachanlagen": {
"account_number": "6105"
},
"Fahrzeug- und Transportaufwand": {
"account_number": "6200"
},
"Fahrzeugleasing und -mieten": {
"account_number": "6260"
},
"Sachversicherungen, Abgaben, Gebühren, Bewilligungen": {
"account_number": "6300"
},
"Energie- und Entsorgungsaufwand": {
"account_number": "6400"
},
"Verwaltungsaufwand": {
"account_number": "6500"
},
"Informatikaufwand inkl. Leasing": {
"account_number": "6570"
},
"Werbeaufwand": {
"account_number": "6600"
},
"Sonstiger betrieblicher Aufwand": {
"account_number": "6700"
},
"Abschreibungen und Wertberichtigungen auf Positionen des Anlagevermögens": {
"account_number": "6800"
},
"Finanzaufwand": {
"account_number": "6900"
},
"Finanzertrag": {
"account_number": "6950"
}
},
"Betrieblicher Nebenerfolg": {
"account_number": "7",
"is_group": 1,
"root_type": "Income",
"Ertrag Nebenbetrieb": {
"account_number": "7000"
},
"Aufwand Nebenbetrieb": {
"account_number": "7010"
},
"Ertrag betriebliche Liegenschaft": {
"account_number": "7500"
},
"Aufwand betriebliche Liegenschaft": {
"account_number": "7510"
}
},
"Betriebsfremder, ausserordentlicher, einmaliger oder periodenfremder Aufwand und Ertrag": {
"account_number": "8",
"is_group": 1,
"root_type": "Expense",
"Betriebsfremder Aufwand": {
"account_number": "8000"
},
"Betriebsfremder Ertrag": {
"account_number": "8100"
},
"Ausserordentlicher, einmaliger oder periodenfremder Aufwand": {
"account_number": "8500"
},
"Ausserordentlicher, einmaliger oder periodenfremder Ertrag": {
"account_number": "8510"
},
"Direkte Steuern": {
"account_number": "8900"
}
},
"Abschluss": {
"account_number": "9",
"is_group": 1,
"root_type": "Equity",
"Jahresgewinn oder Jahresverlust": {
"account_number": "9200"
}
}
}
}

View File

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

View File

@@ -12,7 +12,7 @@ frappe.ui.form.on("Accounts Settings", {
msg += " ";
msg += __("Please enable only if the understand the effects of enabling this.");
msg += "<br>";
msg += "Do you still want to enable immutable ledger?";
msg += __("Do you still want to enable immutable ledger?");
frappe.confirm(
msg,

View File

@@ -40,9 +40,14 @@
"show_payment_schedule_in_print",
"currency_exchange_section",
"allow_stale",
"column_break_yuug",
"stale_days",
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days",
"auto_reconciliation_job_trigger",
"reconciliation_queue_size",
"column_break_resa",
"exchange_gain_loss_posting_date",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
@@ -72,6 +77,7 @@
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"ignore_is_opening_check_for_reporting",
"column_break_lvjk",
"receivable_payable_remarks_length",
"payment_request_settings",
@@ -384,7 +390,7 @@
{
"fieldname": "section_break_jpd0",
"fieldtype": "Section Break",
"label": "Payment Reconciliations"
"label": "Payment Reconciliation Settings"
},
{
"default": "0",
@@ -489,6 +495,43 @@
"fieldname": "create_pr_in_draft_status",
"fieldtype": "Check",
"label": "Create in Draft Status"
},
{
"fieldname": "column_break_yuug",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_resa",
"fieldtype": "Column Break"
},
{
"default": "15",
"description": "Interval should be between 1 to 59 MInutes",
"fieldname": "auto_reconciliation_job_trigger",
"fieldtype": "Int",
"label": "Auto Reconciliation Job Trigger"
},
{
"default": "5",
"description": "Documents Processed on each trigger. Queue Size should be between 5 and 100",
"fieldname": "reconciliation_queue_size",
"fieldtype": "Int",
"label": "Reconciliation Queue Size"
},
{
"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": "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"
}
],
"icon": "icon-cog",
@@ -496,7 +539,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-07-26 06:48:52.714630",
"modified": "2025-01-23 13:15:44.077853",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -10,6 +10,7 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.utils import sync_auto_reconcile_config
from erpnext.stock.utils import check_pending_reposting
@@ -27,6 +28,7 @@ class AccountsSettings(Document):
allow_multi_currency_invoices_against_single_party_account: DF.Check
allow_stale: DF.Check
auto_reconcile_payments: DF.Check
auto_reconciliation_job_trigger: DF.Int
automatically_fetch_payment_terms: DF.Check
automatically_process_deferred_accounting_entry: DF.Check
book_asset_depreciation_entry_automatically: DF.Check
@@ -43,14 +45,17 @@ class AccountsSettings(Document):
enable_fuzzy_matching: DF.Check
enable_immutable_ledger: DF.Check
enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
post_change_gl_entries: DF.Check
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
round_row_wise_tax: DF.Check
show_balance_in_coa: DF.Check
@@ -90,6 +95,8 @@ class AccountsSettings(Document):
if clear_cache:
frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint(
@@ -114,3 +121,17 @@ class AccountsSettings(Document):
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)
def validate_and_sync_auto_reconcile_config(self):
if self.has_value_changed("auto_reconciliation_job_trigger"):
if (
cint(self.auto_reconciliation_job_trigger) > 0
and cint(self.auto_reconciliation_job_trigger) < 60
):
sync_auto_reconcile_config(self.auto_reconciliation_job_trigger)
else:
frappe.throw(_("Cron Interval should be between 1 and 59 Min"))
if self.has_value_changed("reconciliation_queue_size"):
if cint(self.reconciliation_queue_size) < 5 or cint(self.reconciliation_queue_size) > 100:
frappe.throw(_("Queue Size should be between 5 and 100"))

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
from frappe.utils import cint, flt, fmt_money, get_link_to_form, getdate
from pypika import Order
import erpnext
@@ -48,6 +48,7 @@ class BankClearance(Document):
entries = []
# get entries from all the apps
precision = cint(frappe.db.get_default("currency_precision")) or 2
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"):
entries += (
frappe.get_attr(method_name)(
@@ -77,7 +78,7 @@ class BankClearance(Document):
if not d.get("account_currency"):
d.account_currency = default_currency
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
formatted_amount = fmt_money(abs(amount), precision, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
d.posting_date = getdate(d.posting_date)

View File

@@ -120,6 +120,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
args: {
bank_account: frm.doc.bank_account,
till_date: frappe.datetime.add_days(frm.doc.bank_statement_from_date, -1),
company: frm.doc.company,
},
callback: (response) => {
frm.set_value("account_opening_balance", response.message);
@@ -135,6 +136,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
args: {
bank_account: frm.doc.bank_account,
till_date: frm.doc.bank_statement_to_date,
company: frm.doc.company,
},
callback: (response) => {
frm.cleared_balance = response.message;

View File

@@ -79,10 +79,17 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist()
def get_account_balance(bank_account, till_date):
def get_account_balance(bank_account, till_date, company):
# returns account balance till the specified date
account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1})
filters = frappe._dict(
{
"account": account,
"report_date": till_date,
"include_pos_transactions": 1,
"company": company,
}
)
data = get_entries(filters)
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
@@ -94,11 +101,7 @@ def get_account_balance(bank_account, till_date):
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
bank_bal = (
flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
)
return bank_bal
return flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
@frappe.whitelist()
@@ -799,7 +802,6 @@ def get_je_matching_query(
.where(je.clearance_date.isnull())
.where(jea.account == common_filters.bank_account)
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
.where(je.docstatus == 1)
.where(filter_by_date)
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
)

View File

@@ -99,9 +99,9 @@ class BankStatementImport(DataImport):
template_options=self.template_options,
now=run_now,
)
return True
return job_id
return False
return None
@frappe.whitelist()
@@ -113,7 +113,8 @@ def get_preview_from_template(data_import, import_file=None, google_sheets_url=N
@frappe.whitelist()
def form_start_import(data_import):
return frappe.get_doc("Bank Statement Import", data_import).start_import()
job_id = frappe.get_doc("Bank Statement Import", data_import).start_import()
return job_id is not None
@frappe.whitelist()

View File

@@ -45,45 +45,41 @@ class AutoMatchbyAccountIBAN:
if not (self.bank_party_account_number or self.bank_party_iban):
return None
result = self.match_account_in_party()
return result
return self.match_account_in_party()
def match_account_in_party(self) -> tuple | 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()
"""
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
for party in parties:
party_result = frappe.db.get_all(
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
)
# 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"])
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")
# 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])
party_result = frappe.db.get_all(
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
if "bank_ac_no" in or_filters:
or_filters["bank_account_no"] = or_filters.pop("bank_ac_no")
if party_result:
result = (
party,
party_result[0],
)
break
return result
def get_or_filters(self) -> dict:
def get_or_filters(self, party: str | None = None) -> dict:
"""Return OR filters for Bank Account and IBAN"""
or_filters = {}
if self.bank_party_account_number:
or_filters["bank_account_no"] = 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
if self.bank_party_iban:
or_filters["iban"] = self.bank_party_iban
@@ -103,8 +99,7 @@ class AutoMatchbyPartyNameDescription:
if not (self.bank_party_name or self.description):
return None
result = self.match_party_name_desc_in_party()
return result
return self.match_party_name_desc_in_party()
def match_party_name_desc_in_party(self) -> tuple | None:
"""Fuzzy search party name and/or description against parties in the system"""
@@ -113,7 +108,7 @@ class AutoMatchbyPartyNameDescription:
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
field = party.lower() + "_name"
field = f"{party.lower()}_name"
names = frappe.get_all(party, filters=filters, fields=[f"{field} as party_name", "name"])
for field in ["bank_party_name", "description"]:
@@ -140,13 +135,7 @@ class AutoMatchbyPartyNameDescription:
)
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
return None, skip
return (
party,
party_name,
), skip
return ((party, party_name), skip) if party_name else (None, skip)
def process_fuzzy_result(self, result: list | None):
"""
@@ -164,8 +153,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]:
@@ -177,8 +166,8 @@ class AutoMatchbyPartyNameDescription:
def get_parties_in_order(deposit: float) -> list:
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
if flt(deposit) > 0:
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
return parties
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
)

View File

@@ -3,6 +3,7 @@
"allow_copy": 1,
"allow_import": 1,
"creation": "2013-01-23 19:57:17",
"default_view": "Tree",
"description": "Track separate Income and Expense for product verticals or divisions.",
"doctype": "DocType",
"document_type": "Setup",
@@ -125,7 +126,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2024-04-24 10:55:54.083042",
"modified": "2025-01-22 10:46:42.904001",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",

View File

@@ -430,12 +430,6 @@ frappe.ui.form.on("Journal Entry Account", {
});
}
},
cost_center: function (frm, dt, dn) {
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
erpnext.journal_entry.set_account_details(frm, dt, dn);
}
},
account: function (frm, dt, dn) {
erpnext.journal_entry.set_account_details(frm, dt, dn);

View File

@@ -154,10 +154,9 @@ class TestJournalEntry(IntegrationTestCase):
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
},
)
jv.insert()
if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
self.assertRaises(StockAccountInvalidTransaction, jv.save)
frappe.db.rollback()
else:
jv.submit()

View File

@@ -11,6 +11,7 @@ from frappe.utils.background_jobs import enqueue, is_job_enqueued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.stock.utils import get_default_stock_uom
class OpeningInvoiceCreationTool(Document):
@@ -172,7 +173,7 @@ class OpeningInvoiceCreationTool(Document):
income_expense_account_field = (
"income_account" if row.party_type == "Customer" else "expense_account"
)
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or "Nos"
default_uom = get_default_stock_uom()
rate = flt(row.outstanding_amount) / flt(row.qty)
item_dict = frappe._dict(

View File

@@ -21,6 +21,7 @@
"party_name",
"book_advance_payments_in_separate_party_account",
"reconcile_on_advance_payment_date",
"advance_reconciliation_takes_effect_on",
"column_break_11",
"bank_account",
"party_bank_account",
@@ -783,6 +784,16 @@
"options": "No\nYes",
"print_hide": 1,
"search_index": 1
},
{
"default": "Oldest Of Invoice Or Advance",
"fetch_from": "company.reconciliation_takes_effect_on",
"fieldname": "advance_reconciliation_takes_effect_on",
"fieldtype": "Select",
"hidden": 1,
"label": "Advance Reconciliation Takes Effect On",
"no_copy": 1,
"options": "Advance Payment Date\nOldest Of Invoice Or Advance\nReconciliation Date"
}
],
"index_web_pages_for_search": 1,
@@ -796,7 +807,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-11-07 11:19:19.320883",
"modified": "2025-01-13 16:03:47.169699",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -71,12 +71,16 @@ class PaymentEntry(AccountsController):
PaymentEntryReference,
)
advance_reconciliation_takes_effect_on: DF.Literal[
"Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date"
]
amended_from: DF.Link | None
apply_tax_withholding_amount: DF.Check
auto_repeat: DF.Link | None
bank: DF.ReadOnly | None
bank_account: DF.Link | None
bank_account_no: DF.ReadOnly | None
base_in_words: DF.SmallText | None
base_paid_amount: DF.Currency
base_paid_amount_after_tax: DF.Currency
base_received_amount: DF.Currency
@@ -92,6 +96,8 @@ class PaymentEntry(AccountsController):
custom_remarks: DF.Check
deductions: DF.Table[PaymentEntryDeduction]
difference_amount: DF.Currency
in_words: DF.SmallText | None
is_opening: DF.Literal["No", "Yes"]
letter_head: DF.Link | None
mode_of_payment: DF.Link | None
naming_series: DF.Literal["ACC-PAY-.YYYY.-"]
@@ -119,6 +125,7 @@ class PaymentEntry(AccountsController):
purchase_taxes_and_charges_template: DF.Link | None
received_amount: DF.Currency
received_amount_after_tax: DF.Currency
reconcile_on_advance_payment_date: DF.Check
reference_date: DF.Date | None
reference_no: DF.Data | None
references: DF.Table[PaymentEntryReference]
@@ -635,7 +642,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])
)
)
@@ -1500,16 +1507,26 @@ class PaymentEntry(AccountsController):
"voucher_detail_no": invoice.name,
}
if self.reconcile_on_advance_payment_date:
posting_date = self.posting_date
if invoice.reconcile_effect_on:
posting_date = invoice.reconcile_effect_on
else:
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(invoice.reference_doctype, invoice.reference_name, date_field)
if getdate(posting_date) < getdate(self.posting_date):
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
if self.advance_reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(
invoice.reference_doctype, invoice.reference_name, date_field
)
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
elif self.advance_reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate()
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
args_dict["account"] = account
@@ -1665,6 +1682,14 @@ class PaymentEntry(AccountsController):
elif self.payment_type in ("Pay", "Internal Transfer"):
return self.paid_from
def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
company_currency = erpnext.get_company_currency(self.company)
conversion_rate = self.target_exchange_rate
if self.paid_from_account_currency != company_currency:
conversion_rate = self.source_exchange_rate
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
advance_payment_doctypes = frappe.get_hooks(
@@ -2918,7 +2943,7 @@ def get_payment_entry(
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
pe.set("party_bank_account", bank_account)
pe.set_bank_account_data()
# only Purchase Invoice can be blocked individually

View File

@@ -13,6 +13,7 @@
"payment_term_outstanding",
"account_type",
"payment_type",
"reconcile_effect_on",
"column_break_4",
"total_amount",
"outstanding_amount",
@@ -144,12 +145,18 @@
"is_virtual": 1,
"label": "Payment Request Outstanding",
"read_only": 1
},
{
"fieldname": "reconcile_effect_on",
"fieldtype": "Date",
"label": "Reconcile Effect On",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-09-16 18:11:50.019343",
"modified": "2025-01-13 15:56:18.895082",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -30,6 +30,7 @@ class PaymentEntryReference(Document):
payment_term: DF.Link | None
payment_term_outstanding: DF.Float
payment_type: DF.Data | None
reconcile_effect_on: DF.Date | None
reference_doctype: DF.Link
reference_name: DF.DynamicLink
total_amount: DF.Float

View File

@@ -335,6 +335,7 @@ 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
@@ -424,6 +425,9 @@ 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"
@@ -450,6 +454,11 @@ 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

@@ -6,6 +6,7 @@ import frappe
from frappe import qb
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
from frappe.utils.data import getdate as convert_to_date
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -1680,7 +1681,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconcile_on_advance_payment_date": 1,
"reconciliation_takes_effect_on": "Advance Payment Date",
},
)
@@ -1729,7 +1730,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_received_account": self.advance_receivable_account,
"reconcile_on_advance_payment_date": 0,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
amount = 200.0
@@ -1838,7 +1839,7 @@ class TestPaymentReconciliation(IntegrationTestCase):
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconcile_on_advance_payment_date": 0,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
amount = 200.0
@@ -2057,6 +2058,102 @@ class TestPaymentReconciliation(IntegrationTestCase):
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
def test_advance_reconciliation_effect_on_same_date(self):
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_received_account": self.advance_receivable_account,
"reconciliation_takes_effect_on": "Reconciliation Date",
},
)
inv_date = convert_to_date(add_days(nowdate(), -1))
adv_date = convert_to_date(add_days(nowdate(), -2))
si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=200)
pe = self.create_payment_entry(posting_date=adv_date, amount=80).save().submit()
pr = self.create_payment_reconciliation()
pr.from_invoice_date = add_days(nowdate(), -1)
pr.to_invoice_date = nowdate()
pr.from_payment_date = add_days(nowdate(), -2)
pr.to_payment_date = nowdate()
pr.default_advance_account = self.advance_receivable_account
# reconcile multiple payments against invoice
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()
si.reload()
self.assertEqual(si.status, "Partly Paid")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 120)
self.assertEqual(pr.get("payments"), [])
# Assert Ledger Entries
gl_entries = frappe.db.get_all(
"GL Entry",
filters={"voucher_no": pe.name},
fields=["account", "posting_date", "voucher_no", "against_voucher", "debit", "credit"],
order_by="account, against_voucher, debit",
)
expected_gl = [
{
"account": self.advance_receivable_account,
"posting_date": adv_date,
"voucher_no": pe.name,
"against_voucher": pe.name,
"debit": 0.0,
"credit": 80.0,
},
{
"account": self.advance_receivable_account,
"posting_date": convert_to_date(nowdate()),
"voucher_no": pe.name,
"against_voucher": pe.name,
"debit": 80.0,
"credit": 0.0,
},
{
"account": self.debit_to,
"posting_date": convert_to_date(nowdate()),
"voucher_no": pe.name,
"against_voucher": si.name,
"debit": 0.0,
"credit": 80.0,
},
{
"account": self.bank,
"posting_date": adv_date,
"voucher_no": pe.name,
"against_voucher": None,
"debit": 80.0,
"credit": 0.0,
},
]
self.assertEqual(expected_gl, gl_entries)
# cancel PE
pe.reload()
pe.cancel()
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -238,9 +238,6 @@ class TestPOSClosingEntry(IntegrationTestCase):
pos_inv2.payments[0].amount = pos_inv2.grand_total
pos_inv2.submit()
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
self.assertEqual(batch_qty, 10)
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
self.assertEqual(batch_qty_with_pos, 0.0)
@@ -270,9 +267,6 @@ class TestPOSClosingEntry(IntegrationTestCase):
pcv_doc.reload()
pcv_doc.cancel()
batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty")
self.assertEqual(batch_qty, 10)
batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code)
self.assertEqual(batch_qty_with_pos, 0.0)

View File

@@ -8,7 +8,6 @@
"engine": "InnoDB",
"field_order": [
"customer_section",
"title",
"naming_series",
"customer",
"customer_name",
@@ -192,16 +191,6 @@
"fieldtype": "Section Break",
"options": "fa fa-user"
},
{
"allow_on_submit": 1,
"default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
},
{
"bold": 1,
"fieldname": "naming_series",
@@ -1584,7 +1573,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2024-11-26 13:10:50.309570",
"modified": "2025-01-06 15:03:19.957277",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1635,7 +1624,7 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "customer",
"title_field": "title",
"title_field": "customer_name",
"track_changes": 1,
"track_seen": 1
}

View File

@@ -161,7 +161,6 @@ class POSInvoice(SalesInvoice):
terms: DF.TextEditor | None
territory: DF.Link | None
timesheets: DF.Table[SalesInvoiceTimesheet]
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

View File

@@ -843,7 +843,8 @@ class TestPOSInvoice(IntegrationTestCase):
{
"item_code": item.name,
"warehouse": pos_inv2.items[0].warehouse,
"voucher_type": "Delivery Note",
"voucher_type": "POS Invoice",
"voucher_no": pos_inv2.name,
"qty": 2,
"avg_rate": 300,
"batches": frappe._dict({"TestBatch 01": 2}),

View File

@@ -25,6 +25,7 @@
"hide_unavailable_items",
"auto_add_item_to_cart",
"validate_stock_on_save",
"print_receipt_on_order_complete",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
@@ -374,24 +375,30 @@
},
{
"fieldname": "utm_campaign",
"print_hide": 1,
"fieldtype": "Link",
"label": "Campaign",
"options": "UTM Campaign"
"options": "UTM Campaign",
"print_hide": 1
},
{
"fieldname": "utm_source",
"print_hide": 1,
"fieldtype": "Link",
"label": "Source",
"options": "UTM Source"
"options": "UTM Source",
"print_hide": 1
},
{
"fieldname": "utm_medium",
"print_hide": 1,
"fieldtype": "Link",
"label": "Medium",
"options": "UTM Campaign"
"options": "UTM Campaign",
"print_hide": 1
},
{
"default": "0",
"fieldname": "print_receipt_on_order_complete",
"fieldtype": "Check",
"label": "Print Receipt on Order Complete"
}
],
"icon": "icon-cog",
@@ -419,7 +426,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2024-06-28 10:51:48.543766",
"modified": "2025-01-01 11:07:03.161950",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
@@ -448,4 +455,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -46,6 +46,7 @@ class POSProfile(Document):
letter_head: DF.Link | None
payments: DF.Table[POSPaymentMethod]
print_format: DF.Link | None
print_receipt_on_order_complete: DF.Check
select_print_heading: DF.Link | None
selling_price_list: DF.Link | None
tax_category: DF.Link | None

View File

@@ -415,8 +415,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
"parent": args.parent,
"parenttype": args.parenttype,
"child_docname": args.get("child_docname"),
"discount_percentage": 0.0,
"discount_amount": 0,
}
)

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:ACC-PPR-{#####}",
"beta": 1,
"creation": "2023-03-30 21:28:39.793927",
"default_view": "List",
"doctype": "DocType",
@@ -158,7 +157,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-08-27 14:48:56.715320",
"modified": "2025-01-08 08:22:14.798085",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
@@ -192,4 +191,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "company"
}
}

View File

@@ -210,9 +210,9 @@ def trigger_reconciliation_for_queued_docs():
docs_to_trigger = []
unique_filters = set()
queue_size = 5
queue_size = frappe.db.get_single_value("Accounts Settings", "reconciliation_queue_size") or 5
fields = ["company", "party_type", "party", "receivable_payable_account"]
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
def get_filters_as_tuple(fields, doc):
filters = ()

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:PPR-LOG-{##}",
"beta": 1,
"creation": "2023-03-13 15:00:09.149681",
"default_view": "List",
"doctype": "DocType",
@@ -110,7 +109,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:10:18.769659",
"modified": "2025-01-08 08:22:19.104975",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation Log",

View File

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

View File

@@ -20,6 +20,7 @@ class ProcessPaymentReconciliationLogAllocations(Document):
difference_account: DF.Link | None
difference_amount: DF.Currency
exchange_rate: DF.Float
gain_loss_posting_date: DF.Date | None
invoice_number: DF.DynamicLink
invoice_type: DF.Link
is_advance: DF.Data | None

View File

@@ -1625,7 +1625,7 @@
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"description": "Debit Note will update it's own outstanding amount, even if 'Return Against' is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self"
@@ -1641,7 +1641,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-10-25 18:13:01.944477",
"modified": "2025-01-14 11:39:04.564610",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -10,7 +10,6 @@ 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,
@@ -33,7 +32,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
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
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
@@ -840,12 +839,12 @@ class PurchaseInvoice(BuyingController):
def update_supplier_outstanding(self, update_outstanding):
if update_outstanding == "No":
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,
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,
)
def get_gl_entries(self, warehouse_account=None):
@@ -1128,6 +1127,7 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map[item.purchase_receipt]
and self.conversion_rate != exchange_rate_map[item.purchase_receipt]
and item.net_rate == net_rate_map[item.pr_detail]
and item.item_code in stock_items
):
discrepancy_caused_by_exchange_rate_difference = (
item.qty * item.net_rate
@@ -1802,13 +1802,13 @@ class PurchaseInvoice(BuyingController):
self.remove(d)
## Add pending vouchers on which tax was withheld
for voucher_no, voucher_details in voucher_wise_amount.items():
for row in voucher_wise_amount:
self.append(
"tax_withheld_vouchers",
{
"voucher_name": voucher_no,
"voucher_type": voucher_details.get("voucher_type"),
"taxable_amount": voucher_details.get("amount"),
"voucher_name": row.voucher_name,
"voucher_type": row.voucher_type,
"taxable_amount": row.taxable_amount,
},
)

View File

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

View File

@@ -383,6 +383,53 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_with_exchange_rate_difference_for_non_stock_item(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice,
)
# Creating Purchase Invoice with USD currency
pr = frappe.new_doc("Purchase Receipt")
pr.currency = "USD"
pr.company = "_Test Company with perpetual inventory"
pr.conversion_rate = (70,)
pr.supplier = "_Test Supplier USD"
pr.append(
"items",
{
"item_code": "_Test Non Stock Item",
"qty": 1,
"rate": 100,
},
)
pr.append(
"items",
{"item_code": "_Test Item", "qty": 1, "rate": 5, "warehouse": "Stores - TCP1"},
)
pr.insert()
pr.submit()
# Createing purchase invoice against Purchase Receipt
pi = create_purchase_invoice(pr.name)
pi.conversion_rate = 80
pi.credit_to = "_Test Payable USD - TCP1"
pi.insert()
pi.submit()
# Get exchnage gain and loss account
exchange_gain_loss_account = frappe.db.get_value("Company", pi.company, "exchange_gain_loss_account")
# fetching the latest GL Entry with exchange gain and loss account account
amount = frappe.db.get_value(
"GL Entry", {"account": exchange_gain_loss_account, "voucher_no": pi.name}, "debit"
)
discrepancy_caused_by_exchange_rate_diff = abs(
pi.items[1].base_net_amount - pr.items[1].base_net_amount
)
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(self.globalTestRecords["Purchase Invoice"][1])
pi.insert()

View File

@@ -16,6 +16,10 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
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();
@@ -61,7 +65,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
refresh(doc, dt, dn) {
const me = this;
super.refresh();
if (this.frm.msgbox && this.frm.msgbox.$wrapper.is(":visible")) {
if (this.frm?.msgbox && this.frm.msgbox.$wrapper.is(":visible")) {
// hide new msgbox
this.frm.msgbox.hide();
}
@@ -125,12 +129,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
},
__("Create")
);
this.frm.add_custom_button(
__("Invoice Discounting"),
function () {
this.frm.events.create_invoice_discounting(this.frm);
},
this.make_invoice_discounting.bind(this),
__("Create")
);
@@ -139,22 +140,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
.reduce((prev, current) => prev || current, false);
if (payment_is_overdue) {
this.frm.add_custom_button(
__("Dunning"),
() => {
this.frm.events.create_dunning(this.frm);
},
__("Create")
);
this.frm.add_custom_button(__("Dunning"), this.make_dunning.bind(this), __("Create"));
}
}
if (doc.docstatus === 1) {
this.frm.add_custom_button(
__("Maintenance Schedule"),
function () {
this.frm.cscript.make_maintenance_schedule();
},
this.make_maintenance_schedule.bind(this),
__("Create")
);
}
@@ -189,6 +182,20 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
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",
@@ -993,67 +1000,57 @@ frappe.ui.form.on("Sales Invoice", {
refresh: function (frm) {
if (frm.doc.docstatus === 0 && !frm.doc.is_return) {
frm.add_custom_button(__("Fetch Timesheet"), function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
frm.add_custom_button(
__("Timesheet"),
function () {
let d = new frappe.ui.Dialog({
title: __("Fetch Timesheet"),
fields: [
{
label: __("From"),
fieldname: "from_time",
fieldtype: "Date",
reqd: 1,
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
});
d.hide();
},
{
fieldtype: "Column Break",
fieldname: "col_break_1",
},
{
label: __("To"),
fieldname: "to_time",
fieldtype: "Date",
reqd: 1,
},
{
label: __("Project"),
fieldname: "project",
fieldtype: "Link",
options: "Project",
default: frm.doc.project,
},
],
primary_action: function () {
const data = d.get_values();
frm.events.add_timesheet_data(frm, {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
});
d.hide();
},
primary_action_label: __("Get Timesheets"),
});
d.show();
});
primary_action_label: __("Get Timesheets"),
});
d.show();
},
__("Get Items From")
);
}
if (frm.doc.is_debit_note) {
frm.set_df_property("return_against", "label", __("Adjustment Against"));
}
},
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

@@ -7,7 +7,6 @@
"engine": "InnoDB",
"field_order": [
"customer_section",
"title",
"naming_series",
"customer",
"customer_name",
@@ -229,18 +228,6 @@
"hide_seconds": 1,
"options": "fa fa-user"
},
{
"allow_on_submit": 1,
"default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"hide_days": 1,
"hide_seconds": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
},
{
"bold": 1,
"fieldname": "naming_series",
@@ -2159,7 +2146,7 @@
{
"default": "1",
"depends_on": "eval: doc.is_return && doc.return_against",
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
"description": "Credit Note will update it's own outstanding amount, even if 'Return Against' is specified.",
"fieldname": "update_outstanding_for_self",
"fieldtype": "Check",
"label": "Update Outstanding for Self",
@@ -2223,7 +2210,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2024-11-26 12:34:09.110690",
"modified": "2025-01-14 11:38:30.446370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@@ -2275,7 +2262,7 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "customer",
"title_field": "title",
"title_field": "customer_name",
"track_changes": 1,
"track_seen": 1
}

View File

@@ -29,7 +29,11 @@ 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
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
update_voucher_outstanding,
)
from erpnext.assets.doctype.asset.depreciation import (
depreciate_asset,
get_gl_entries_on_asset_disposal,
@@ -206,7 +210,6 @@ class SalesInvoice(SellingController):
terms: DF.TextEditor | None
territory: DF.Link | None
timesheets: DF.Table[SalesInvoiceTimesheet]
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency
@@ -323,9 +326,7 @@ class SalesInvoice(SellingController):
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if not self.is_return:
self.validate_serial_numbers()
else:
if self.is_return:
self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
@@ -364,7 +365,7 @@ class SalesInvoice(SellingController):
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
asset.status == "Sold" and not self.is_return
):
frappe.throw(
@@ -1092,13 +1093,16 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if not self.timesheets and self.project:
self.add_timesheet_data()
else:
if self.timesheets:
self.calculate_billing_amount_for_timesheet()
@frappe.whitelist()
@frappe.whitelist(methods=["PUT"])
def add_timesheet_data(self):
if not self.timesheets and self.project:
self._add_timesheet_data()
self.save()
def _add_timesheet_data(self):
self.set("timesheets", [])
if self.project:
for data in get_projectwise_timesheet_data(self.project):
@@ -1192,14 +1196,14 @@ class SalesInvoice(SellingController):
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if update_outstanding == "No":
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,
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,
)
elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock):
@@ -1703,14 +1707,6 @@ class SalesInvoice(SellingController):
self.set("write_off_amount", reference_doc.get("write_off_amount"))
self.due_date = None
def validate_serial_numbers(self):
"""
validate serial number agains Delivery Note and Sales Invoice
"""
for item in self.items:
item.set_serial_no_against_delivery_note()
item.validate_serial_against_delivery_note()
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:

View File

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

View File

@@ -2617,6 +2617,7 @@ class TestSalesInvoice(IntegrationTestCase):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
<<<<<<< HEAD
def test_inter_company_transaction_without_default_warehouse(self):
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
# setup
@@ -2671,6 +2672,8 @@ class TestSalesInvoice(IntegrationTestCase):
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
=======
>>>>>>> df1f8fddf6 (fix: using DN for transfer w/o internal customer (#27798))
def test_sle_for_target_warehouse(self):
se = make_stock_entry(
item_code="138-CMS Shoe",
@@ -4294,6 +4297,7 @@ class TestSalesInvoice(IntegrationTestCase):
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()
@@ -4304,6 +4308,30 @@ class TestSalesInvoice(IntegrationTestCase):
doc = frappe.get_doc("Project", project.name)
self.assertEqual(doc.total_billed_amount, si.grand_total)
def test_pos_returns_with_party_account_currency(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
pos_profile = make_pos_profile()
pos_profile.payments = []
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
pos_profile.save()
pos = create_sales_invoice(
customer="_Test Customer USD",
currency="USD",
conversion_rate=86.595000000,
qty=2,
do_not_save=True,
)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.debit_to = "_Test Receivable USD - _TC"
pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35})
pos.save().submit()
pos_return = make_sales_return(pos.name)
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -8,7 +8,7 @@ from frappe.model.document import Document
from frappe.utils.data import cint
from erpnext.assets.doctype.asset.depreciation import get_disposal_account_and_cost_center
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class SalesInvoiceItem(Document):
@@ -128,39 +128,3 @@ class SalesInvoiceItem(Document):
self.income_account = disposal_account
if not self.cost_center:
self.cost_center = depreciation_cost_center
def set_serial_no_against_delivery_note(self):
"""Set serial no based on delivery note."""
if self.serial_no and self.delivery_note and self.qty != len(get_serial_nos(self.serial_no)):
self.serial_no = get_delivery_note_serial_no(self.item_code, self.qty, self.delivery_note)
def validate_serial_against_delivery_note(self):
"""Ensure the serial numbers in this Sales Invoice Item are same as in the linked Delivery Note."""
if not self.delivery_note or not self.dn_detail:
return
serial_nos = frappe.db.get_value("Delivery Note Item", self.dn_detail, "serial_no") or ""
dn_serial_nos = set(get_serial_nos(serial_nos))
serial_nos = self.serial_no or ""
si_serial_nos = set(get_serial_nos(serial_nos))
serial_no_diff = si_serial_nos - dn_serial_nos
if serial_no_diff:
dn_link = frappe.utils.get_link_to_form("Delivery Note", self.delivery_note)
msg = (
_("Row #{0}: The following serial numbers are not present in Delivery Note {1}:").format(
self.idx, dn_link
)
+ " "
+ ", ".join(frappe.bold(d) for d in serial_no_diff)
)
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
if self.serial_no and cint(self.qty) != len(si_serial_nos):
frappe.throw(
_(
"Row #{0}: {1} serial numbers are required for Item {2}. You have provided {3} serial numbers."
).format(self.idx, self.qty, self.item_code, len(si_serial_nos))
)

View File

@@ -114,10 +114,10 @@ class Subscription(Document):
if self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
elif date:
_current_invoice_start = date
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
else:
_current_invoice_start = nowdate()
@@ -414,8 +414,8 @@ class Subscription(Document):
if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
invoice.apply_tds = 1
# Add party currency to invoice
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
# Add currency to invoice
invoice.currency = frappe.db.get_value("Subscription Plan", {"name": self.plans[0].plan}, "currency")
# Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()
@@ -697,7 +697,7 @@ class Subscription(Document):
self.status = "Cancelled"
self.cancelation_date = nowdate()
if to_generate_invoice:
if to_generate_invoice and self.cancelation_date >= self.current_invoice_start:
self.generate_invoice(self.current_invoice_start, self.cancelation_date)
self.save()

View File

@@ -479,6 +479,28 @@ class TestSubscription(IntegrationTestCase):
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
self.assertEqual(currency, "USD")
@IntegrationTestCase.change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1},
)
def test_multi_currency_subscription_with_default_company_currency(self):
party = "Test Subscription Customer Multi Currency"
frappe.db.set_value("Customer", party, "default_currency", "USD")
subscription = create_subscription(
start_date="2018-01-01",
generate_invoice_at="Beginning of the current subscription period",
plans=[{"plan": "_Test Plan Multicurrency", "qty": 1, "currency": "USD"}],
party=party,
)
subscription.process(posting_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Unpaid")
# Check the currency of the created invoice
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
self.assertEqual(currency, "USD")
def test_subscription_recovery(self):
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
subscription = create_subscription(
@@ -590,6 +612,12 @@ def create_parties():
customer.append("accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"})
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer Multi Currency"):
customer = frappe.new_doc("Customer")
customer.customer_name = "Test Subscription Customer Multi Currency"
customer.default_currency = "USD"
customer.insert()
if not frappe.db.exists("Customer", "_Test Subscription Customer John Doe"):
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Subscription Customer John Doe"

View File

@@ -87,6 +87,7 @@ def get_party_details(inv):
def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if inv.doctype == "Payment Entry":
inv.tax_withholding_net_total = inv.net_total
inv.base_tax_withholding_net_total = inv.net_total
pan_no = ""
parties = []
@@ -156,6 +157,9 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
}
)
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:
@@ -302,6 +306,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
if party_type == "Supplier":
# if tds account is changed.
if not tax_deducted:
tax_deducted = is_tax_deducted_on_the_basis_of_inv(vouchers)
ldc = get_lower_deduction_certificate(inv.company, posting_date, tax_details, pan_no)
if tax_deducted:
net_total = inv.tax_withholding_net_total
@@ -319,7 +327,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:
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers)
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount)
elif party_type == "Customer":
if tax_deducted:
@@ -336,14 +344,29 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
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"
def is_tax_deducted_on_the_basis_of_inv(vouchers):
return frappe.db.exists(
"Purchase Taxes and Charges",
{
"parent": ["in", vouchers],
"is_tax_withholding_account": 1,
"parenttype": "Purchase Invoice",
"base_tax_amount_after_discount_amount": [">", 0],
},
)
voucher_wise_amount = {}
def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
voucher_wise_amount = []
vouchers = []
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",
]
filters = {
"company": company,
frappe.scrub(party_type): ["in", parties],
@@ -357,15 +380,24 @@ 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=["name", field])
invoices_details = frappe.get_all(doctype, filters=filters, fields=field)
for d in invoices_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}})
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": d.name,
"voucher_type": doctype,
"taxable_amount": d.base_net_total,
"grand_total": d.grand_total,
}
)
)
journal_entries_details = frappe.db.sql(
"""
SELECT j.name, ja.credit - ja.debit AS amount
SELECT j.name, ja.credit - ja.debit AS amount, ja.reference_type
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
j.name = ja.parent
@@ -384,13 +416,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
tax_details.get("tax_withholding_category"),
company,
),
as_dict=1,
)
if journal_entries_details:
for d in journal_entries_details:
vouchers.append(d.name)
voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}})
for d in journal_entries_details:
vouchers.append(d.name)
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": d.name,
"voucher_type": "Journal Entry",
"taxable_amount": d.amount,
"reference_type": d.reference_type,
}
)
)
return vouchers, voucher_wise_amount
@@ -489,12 +528,24 @@ def get_advance_tax_across_fiscal_year(tax_deducted_on_advances, tax_details):
return advance_tax_from_across_fiscal_year
def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
def get_tds_amount(ldc, parties, inv, tax_details, voucher_wise_amount):
tds_amount = 0
invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1}
pi_grand_total = 0
pi_base_net_total = 0
jv_credit_amt = 0
pe_credit_amt = 0
for row in voucher_wise_amount:
if row.voucher_type == "Purchase Invoice":
pi_grand_total += row.get("grand_total", 0)
pi_base_net_total += row.get("taxable_amount", 0)
if row.voucher_type == "Journal Entry" and row.reference_type != "Purchase Invoice":
jv_credit_amt += row.get("taxable_amount", 0)
## for TDS to be deducted on advances
payment_entry_filters = {
pe_filters = {
"party_type": "Supplier",
"party": ("in", parties),
"docstatus": 1,
@@ -505,70 +556,49 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
"company": inv.company,
}
field = "sum(tax_withholding_net_total)"
consider_party_ledger_amt = cint(tax_details.consider_party_ledger_amount)
if cint(tax_details.consider_party_ledger_amount):
invoice_filters.pop("apply_tds", None)
field = "sum(grand_total)"
payment_entry_filters.pop("apply_tax_withholding_amount", None)
payment_entry_filters.pop("tax_withholding_category", None)
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
supp_jv_credit_amt = (
frappe.db.get_value(
"Journal Entry Account",
{
"parent": ("in", vouchers),
"docstatus": 1,
"party": ("in", parties),
"reference_type": ("!=", "Purchase Invoice"),
},
"sum(credit_in_account_currency - debit_in_account_currency)",
)
or 0.0
)
if consider_party_ledger_amt:
pe_filters.pop("apply_tax_withholding_amount", None)
pe_filters.pop("tax_withholding_category", None)
# Get Amount via payment entry
payment_entry_amounts = frappe.db.get_all(
payment_entries = frappe.db.get_all(
"Payment Entry",
filters=payment_entry_filters,
fields=["sum(unallocated_amount) as amount", "payment_type"],
group_by="payment_type",
filters=pe_filters,
fields=["name", "unallocated_amount as taxable_amount", "payment_type"],
)
supp_credit_amt = supp_jv_credit_amt
supp_credit_amt += inv.get("tax_withholding_net_total", 0)
for type in payment_entry_amounts:
if type.payment_type == "Pay":
supp_credit_amt += type.amount
else:
supp_credit_amt -= type.amount
for row in payment_entries:
value = row.taxable_amount if row.payment_type == "Pay" else -1 * row.taxable_amount
pe_credit_amt += value
voucher_wise_amount.append(
frappe._dict(
{
"voucher_name": row.name,
"voucher_type": "Payment Entry",
"taxable_amount": value,
}
)
)
threshold = tax_details.get("threshold", 0)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
supp_credit_amt = jv_credit_amt + pe_credit_amt + inv.get("tax_withholding_net_total", 0)
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
if inv.doctype != "Payment Entry":
tax_withholding_net_total = inv.get("base_tax_withholding_net_total", 0)
else:
tax_withholding_net_total = inv.get("tax_withholding_net_total", 0)
# if consider_party_ledger_amount is checked, then threshold will be based on grand total
amt_for_threshold = pi_grand_total if consider_party_ledger_amt else pi_base_net_total
if (threshold and tax_withholding_net_total >= threshold) or (
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = (
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
)
supp_credit_amt += net_total
cumulative_threshold_breached = (
cumulative_threshold and (supp_credit_amt + amt_for_threshold) >= cumulative_threshold
)
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
tax_details.tax_on_excess_amount
):
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
if (threshold and tax_withholding_net_total >= threshold) or (cumulative_threshold_breached):
supp_credit_amt += pi_base_net_total
if cumulative_threshold_breached and cint(tax_details.tax_on_excess_amount):
supp_credit_amt = pi_base_net_total + tax_withholding_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(

View File

@@ -71,6 +71,49 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
for d in reversed(invoices):
d.cancel()
def test_tds_with_account_changed(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Multi Account TDS Category"
)
invoices = []
# create invoices for lower than single threshold tax rate
for _ in range(2):
pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit()
invoices.append(pi)
# create another invoice whose total when added to previously created invoice,
# surpasses cumulative threshhold
pi = create_purchase_invoice(supplier="Test TDS Supplier")
pi.submit()
# assert equal tax deduction on total invoice amount until now
self.assertEqual(pi.taxes_and_charges_deducted, 3000)
self.assertEqual(pi.grand_total, 7000)
invoices.append(pi)
# account changed
frappe.db.set_value(
"Tax Withholding Account",
{"parent": "Multi Account TDS Category"},
"account",
"_Test Account VAT - _TC",
)
# TDS should be on invoice only even though account is changed
pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
pi.submit()
# assert equal tax deduction on total invoice amount until now
self.assertEqual(pi.taxes_and_charges_deducted, 500)
invoices.append(pi)
# delete invoices to avoid clashing
for d in reversed(invoices):
d.cancel()
def test_single_threshold_tds(self):
invoices = []
frappe.db.set_value(
@@ -536,6 +579,15 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
pi1.submit()
invoices.append(pi1)
pe = create_payment_entry(
payment_type="Pay", party_type="Supplier", party="Test TDS Supplier6", paid_amount=1000
)
pe.apply_tax_withholding_amount = 1
pe.tax_withholding_category = "Test Multi Invoice Category"
pe.save()
pe.submit()
invoices.append(pe)
pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True)
pi2.apply_tds = 1
pi2.tax_withholding_category = "Test Multi Invoice Category"
@@ -551,6 +603,8 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total)
self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name)
self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total)
self.assertTrue(pi2.tax_withheld_vouchers[2].voucher_name == pe.name)
self.assertTrue(pi2.tax_withheld_vouchers[2].taxable_amount == pe.paid_amount)
# cancel invoices to avoid clashing
for d in reversed(invoices):
@@ -1071,6 +1125,16 @@ def create_tax_withholding_category_records():
consider_party_ledger_amount=1,
)
create_tax_withholding_category(
category_name="Multi Account TDS Category",
rate=10,
from_date=from_date,
to_date=to_date,
account="TDS - _TC",
single_threshold=0,
cumulative_threshold=30000,
)
def create_tax_withholding_category(
category_name,

View File

@@ -36,7 +36,7 @@ def make_gl_entries(
make_acc_dimensions_offsetting_entry(gl_map)
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
gl_map = process_gl_map(gl_map, merge_entries, from_repost=from_repost)
if gl_map and len(gl_map) > 1:
if gl_map[0].voucher_type != "Period Closing Voucher":
create_payment_ledger_entry(
@@ -164,12 +164,12 @@ def validate_accounting_period(gl_map):
)
def process_gl_map(gl_map, merge_entries=True, precision=None):
def process_gl_map(gl_map, merge_entries=True, precision=None, from_repost=False):
if not gl_map:
return []
if gl_map[0].voucher_type != "Period Closing Voucher":
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision, from_repost)
if merge_entries:
gl_map = merge_similar_entries(gl_map, precision)
@@ -179,13 +179,17 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
return gl_map
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_repost=False):
new_gl_map = []
for d in gl_map:
cost_center = d.get("cost_center")
# Validate budget against main cost center
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
if not from_repost:
validate_expense_against_budget(
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
)
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)

View File

@@ -621,34 +621,41 @@ def get_due_date_from_template(template_name, posting_date, bill_date):
return due_date
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None):
def validate_due_date(posting_date, due_date, bill_date=None, template_name=None, doctype=None):
if getdate(due_date) < getdate(posting_date):
frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date"))
doctype_date = "Date"
if doctype == "Purchase Invoice":
doctype_date = "Supplier Invoice Date"
if doctype == "Sales Invoice":
doctype_date = "Posting Date"
frappe.throw(_("Due Date cannot be before {0}").format(doctype_date))
else:
if not template_name:
return
validate_due_date_with_template(posting_date, due_date, bill_date, template_name)
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):
if not template_name:
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):
is_credit_controller = (
frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles()
)
if not default_due_date:
return
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()
if is_credit_controller:
msgprint(
_("Note: Due Date exceeds allowed customer credit days by {0} day(s)").format(
date_diff(due_date, 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))
)
else:
frappe.throw(_("Due Date cannot be after {0}").format(formatdate(default_due_date)))
@frappe.whitelist()
@@ -681,7 +688,7 @@ def set_taxes(
):
from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template
args = {party_type.lower(): party, "company": company}
args = {frappe.scrub(party_type): party, "company": company}
if tax_category:
args["tax_category"] = tax_category
@@ -701,10 +708,10 @@ def set_taxes(
else:
args.update(get_party_details(party, party_type))
if party_type in ("Customer", "Lead", "Prospect"):
if party_type in ("Customer", "Lead", "Prospect", "CRM Deal"):
args.update({"tax_type": "Sales"})
if party_type in ["Lead", "Prospect"]:
if party_type in ["Lead", "Prospect", "CRM Deal"]:
args["customer"] = None
del args[frappe.scrub(party_type)]
else:

View File

@@ -510,12 +510,16 @@ def get_accounting_entries(
.where(gl_entry.company == filters.company)
)
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if doctype == "GL Entry":
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
if ignore_opening_entries:
if ignore_opening_entries and not ignore_is_opening:
query = query.where(gl_entry.is_opening == "No")
else:
query = query.select(gl_entry.closing_date.as_("posting_date"))

View File

@@ -71,9 +71,6 @@
</div>
<div style="text-align:center; font-size:13px;">
<b>
{% if(filters.party_type) { %}
[ {%= filters.party_type %} ]<br>
{% } %}
{%= frappe.datetime.str_to_user(filters.from_date) %}
{%= __("to") %}
{%= frappe.datetime.str_to_user(filters.to_date) %}<br><br>

View File

@@ -208,6 +208,10 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if filters.get("account"):
filters.account = get_accounts_with_children(filters.account)
if filters.account:
@@ -270,9 +274,15 @@ def get_conditions(filters):
or filters.get("party")
or filters.get("group_by") in ["Group by Account", "Group by Party"]
):
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
if not ignore_is_opening:
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
else:
conditions.append("posting_date >=%(from_date)s")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
if not ignore_is_opening:
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
else:
conditions.append("posting_date <=%(to_date)s")
if filters.get("project"):
conditions.append("project in %(project)s")

View File

@@ -1,5 +1,5 @@
{
"add_total_row": 1,
"add_total_row": 0,
"columns": [],
"creation": "2013-02-25 17:03:34",
"disable_prepared_report": 0,
@@ -9,7 +9,7 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2022-02-11 10:18:36.956558",
"modified": "2025-01-27 18:40:24.493829",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",

View File

@@ -178,7 +178,14 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
# removing Item Code and Item Name columns
del columns[4:6]
total_base_amount = 0
total_buying_amount = 0
for src in gross_profit_data.si_list:
if src.indent == 1:
total_base_amount += src.base_amount or 0.0
total_buying_amount += src.buying_amount or 0.0
row = frappe._dict()
row.indent = src.indent
row.parent_invoice = src.parent_invoice
@@ -189,6 +196,27 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
data.append(row)
total_gross_profit = total_base_amount - total_buying_amount
data.append(
frappe._dict(
{
"sales_invoice": "Total",
"qty": None,
"avg._selling_rate": None,
"valuation_rate": None,
"selling_amount": total_base_amount,
"buying_amount": total_buying_amount,
"gross_profit": total_gross_profit,
"gross_profit_%": flt(
(total_gross_profit / total_base_amount) * 100.0,
cint(frappe.db.get_default("currency_precision")) or 3,
)
if total_base_amount
else 0,
}
)
)
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
for src in gross_profit_data.grouped_data:

View File

@@ -612,3 +612,33 @@ class TestGrossProfit(IntegrationTestCase):
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
self.assertEqual(len(item_from_sinv2), 1)
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
def test_gross_profit_groupby_invoices(self):
create_sales_invoice(
qty=1,
rate=100,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
income_account=self.income_account,
expense_account=self.expense_account,
)
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
_, data = execute(filters=filters)
total = data[-1]
self.assertEqual(total.selling_amount, 100.0)
self.assertEqual(total.buying_amount, 0.0)
self.assertEqual(total.gross_profit, 100.0)
self.assertEqual(total.get("gross_profit_%"), 100.0)

View File

@@ -319,7 +319,7 @@ def get_columns(additional_table_columns, filters):
"width": 100,
},
{
"label": _("Tax Rate"),
"label": _("Rate"),
"fieldname": "rate",
"fieldtype": "Float",
"options": "currency",

View File

@@ -14,14 +14,14 @@
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "Trial Balance",
"report_type": "Script Report",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}

View File

@@ -89,6 +89,10 @@ def get_data(filters):
)
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
ignore_is_opening = frappe.db.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if not accounts:
return None
@@ -96,7 +100,7 @@ def get_data(filters):
gl_entries_by_account = {}
opening_balances = get_opening_balances(filters)
opening_balances = get_opening_balances(filters, ignore_is_opening)
# add filter inside list so that the query in financial_statements.py doesn't break
if filters.project:
@@ -114,7 +118,13 @@ def get_data(filters):
ignore_opening_entries=True,
)
calculate_values(accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values"))
calculate_values(
accounts,
gl_entries_by_account,
opening_balances,
filters.get("show_net_values"),
ignore_is_opening=ignore_is_opening,
)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
@@ -125,15 +135,15 @@ def get_data(filters):
return data
def get_opening_balances(filters):
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet")
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss")
def get_opening_balances(filters, ignore_is_opening):
balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet", ignore_is_opening)
pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss", ignore_is_opening)
balance_sheet_opening.update(pl_opening)
return balance_sheet_opening
def get_rootwise_opening_balances(filters, report_type):
def get_rootwise_opening_balances(filters, report_type, ignore_is_opening):
gle = []
last_period_closing_voucher = ""
@@ -159,16 +169,24 @@ def get_rootwise_opening_balances(filters, report_type):
report_type,
accounting_dimensions,
period_closing_voucher=last_period_closing_voucher[0].name,
ignore_is_opening=ignore_is_opening,
)
# Report getting generate from the mid of a fiscal year
if getdate(last_period_closing_voucher[0].period_end_date) < getdate(add_days(filters.from_date, -1)):
start_date = add_days(last_period_closing_voucher[0].period_end_date, 1)
gle += get_opening_balance(
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
"GL Entry",
filters,
report_type,
accounting_dimensions,
start_date=start_date,
ignore_is_opening=ignore_is_opening,
)
else:
gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
gle = get_opening_balance(
"GL Entry", filters, report_type, accounting_dimensions, ignore_is_opening=ignore_is_opening
)
opening = frappe._dict()
for d in gle:
@@ -187,7 +205,13 @@ def get_rootwise_opening_balances(filters, report_type):
def get_opening_balance(
doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None
doctype,
filters,
report_type,
accounting_dimensions,
period_closing_voucher=None,
start_date=None,
ignore_is_opening=0,
):
closing_balance = frappe.qb.DocType(doctype)
account = frappe.qb.DocType("Account")
@@ -223,11 +247,16 @@ def get_opening_balance(
(closing_balance.posting_date >= start_date)
& (closing_balance.posting_date < filters.from_date)
)
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
if not ignore_is_opening:
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
else:
opening_balance = opening_balance.where(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
if not ignore_is_opening:
opening_balance = opening_balance.where(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
else:
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
if doctype == "GL Entry":
opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
@@ -298,7 +327,7 @@ def get_opening_balance(
return gle
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values, ignore_is_opening=0):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -316,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
d["opening_credit"] = opening_balances.get(d.name, {}).get("opening_credit", 0)
for entry in gl_entries_by_account.get(d.name, []):
if cstr(entry.is_opening) != "Yes":
if cstr(entry.is_opening) != "Yes" or ignore_is_opening:
d["debit"] += flt(entry.debit)
d["credit"] += flt(entry.credit)

View File

@@ -730,6 +730,23 @@ def update_reference_in_payment_entry(
}
update_advance_paid = []
# Update Reconciliation effect date in reference
if payment_entry.book_advance_payments_in_separate_party_account:
if payment_entry.advance_reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
reconcile_on = payment_entry.posting_date
elif payment_entry.advance_reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
@@ -1661,7 +1678,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
if wh_details.account == account and not wh_details.is_group
]
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
total_stock_value = get_stock_value_on(related_warehouses, posting_date, company=company)
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
@@ -2267,3 +2284,38 @@ def run_ledger_health_checks():
doc.general_and_payment_ledger_mismatch = True
doc.checked_on = run_date
doc.save()
def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
auto_reconciliation_job_trigger = auto_reconciliation_job_trigger or frappe.db.get_single_value(
"Accounts Settings", "auto_reconciliation_job_trigger"
)
method = "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs"
sch_event = frappe.get_doc(
"Scheduler Event", {"scheduled_against": "Process Payment Reconciliation", "method": method}
)
if frappe.db.get_value("Scheduled Job Type", {"method": method}):
frappe.get_doc(
"Scheduled Job Type",
{
"method": method,
},
).update(
{
"cron_format": f"0/{auto_reconciliation_job_trigger} * * * *",
"scheduler_event": sch_event.name,
}
).save()
else:
frappe.get_doc(
{
"doctype": "Scheduled Job Type",
"method": method,
"scheduler_event": sch_event.name,
"cron_format": f"0/{auto_reconciliation_job_trigger} * * * *",
"create_log": True,
"stopped": False,
"frequency": "Cron",
}
).save()

View File

@@ -353,7 +353,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nDecapitalized",
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"read_only": 1
},
{
@@ -592,7 +592,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2024-11-29 14:25:56.436124",
"modified": "2024-12-26 14:23:20.968882",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -111,7 +111,7 @@ class Asset(AccountsController):
"Issue",
"Receipt",
"Capitalized",
"Decapitalized",
"Work In Progress",
]
supplier: DF.Link | None
total_asset_cost: DF.Currency

View File

@@ -1,5 +1,6 @@
frappe.listview_settings["Asset"] = {
add_fields: ["status"],
add_fields: ["status", "docstatus"],
has_indicator_for_draft: 1,
get_indicator: function (doc) {
if (doc.status === "Fully Depreciated") {
return [__("Fully Depreciated"), "green", "status,=,Fully Depreciated"];
@@ -7,8 +8,10 @@ frappe.listview_settings["Asset"] = {
return [__("Partially Depreciated"), "grey", "status,=,Partially Depreciated"];
} else if (doc.status === "Sold") {
return [__("Sold"), "green", "status,=,Sold"];
} else if (["Capitalized", "Decapitalized"].includes(doc.status)) {
return [__(doc.status), "grey", "status,=," + doc.status];
} else if (doc.status === "Work In Progress") {
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
} else if (doc.status === "Capitalized") {
return [__("Capitalized"), "grey", "status,=,Capitalized"];
} else if (doc.status === "Scrapped") {
return [__("Scrapped"), "grey", "status,=,Scrapped"];
} else if (doc.status === "In Maintenance") {
@@ -21,7 +24,7 @@ frappe.listview_settings["Asset"] = {
return [__("Receipt"), "green", "status,=,Receipt"];
} else if (doc.status === "Submitted") {
return [__("Submitted"), "blue", "status,=,Submitted"];
} else if (doc.status === "Draft") {
} else if (doc.status === "Draft" || doc.docstatus === 0) {
return [__("Draft"), "red", "status,=,Draft"];
}
},

View File

@@ -436,7 +436,7 @@ def scrap_asset(asset_name, scrap_date=None):
if asset.docstatus != 1:
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"):
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
today_date = getdate(today())

View File

@@ -1752,6 +1752,10 @@ def create_asset(**args):
},
)
if asset.is_composite_asset:
asset.gross_purchase_amount = 0
asset.purchase_amount = 0
if not args.do_not_save:
try:
asset.insert(ignore_if_duplicate=True)

View File

@@ -36,11 +36,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.setup_warehouse_query();
me.frm.set_query("target_item_code", function () {
if (me.frm.doc.entry_type == "Capitalization") {
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
} else {
return erpnext.queries.item({ is_stock_item: 1, is_fixed_asset: 0 });
}
return erpnext.queries.item({ is_stock_item: 0, is_fixed_asset: 1 });
});
me.frm.set_query("target_asset", function () {
@@ -51,7 +47,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
me.frm.set_query("asset", "asset_items", function () {
var filters = {
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]],
status: ["not in", ["Draft", "Scrapped", "Sold", "Capitalized"]],
docstatus: 1,
};

View File

@@ -8,30 +8,26 @@
"engine": "InnoDB",
"field_order": [
"title",
"company",
"naming_series",
"entry_type",
"target_item_name",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"column_break_9",
"capitalization_method",
"target_item_code",
"target_asset_location",
"target_item_name",
"target_asset",
"target_asset_name",
"target_warehouse",
"target_qty",
"target_stock_uom",
"target_batch_no",
"target_serial_no",
"column_break_5",
"finance_book",
"target_asset_location",
"column_break_9",
"company",
"posting_date",
"posting_time",
"set_posting_time",
"finance_book",
"target_batch_no",
"target_serial_no",
"amended_from",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"section_break_16",
"stock_items",
"stock_items_total",
@@ -58,12 +54,12 @@
"label": "Title"
},
{
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || doc.capitalization_method=='Create a new composite asset'",
"fieldname": "target_item_code",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Item Code",
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"options": "Item"
},
{
@@ -84,22 +80,18 @@
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || doc.capitalization_method=='Choose a WIP composite asset'",
"fieldname": "target_asset",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Asset",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
"mandatory_depends_on": "eval:doc.capitalization_method=='Choose a WIP composite asset'",
"no_copy": 1,
"options": "Asset",
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
"read_only_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'"
},
{
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.capitalization_method=='Choose a WIP composite asset')",
"fetch_from": "target_asset.asset_name",
"fieldname": "target_asset_name",
"fieldtype": "Data",
@@ -162,7 +154,7 @@
"read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))",
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Consumed Stock Items"
@@ -173,14 +165,6 @@
"label": "Stock Items",
"options": "Asset Capitalization Stock Item"
},
{
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'",
"options": "Warehouse"
},
{
"depends_on": "target_has_batch_no",
"fieldname": "target_batch_no",
@@ -190,20 +174,9 @@
},
{
"default": "1",
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fieldname": "target_qty",
"fieldtype": "Float",
"label": "Target Qty",
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
},
{
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fetch_from": "target_item_code.stock_uom",
"fieldname": "target_stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
"label": "Target Qty"
},
{
"default": "0",
@@ -241,16 +214,6 @@
"label": "Assets",
"options": "Asset Capitalization Asset Item"
},
{
"default": "Capitalization",
"fieldname": "entry_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Entry Type",
"options": "Capitalization\nDecapitalization",
"reqd": 1
},
{
"fieldname": "stock_items_total",
"fieldtype": "Currency",
@@ -272,7 +235,7 @@
"options": "Finance Book"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))",
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
"fieldname": "service_expenses_section",
"fieldtype": "Section Break",
"label": "Service Expenses"
@@ -337,26 +300,24 @@
"read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"fieldname": "target_asset_location",
"fieldtype": "Link",
"label": "Target Asset Location",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"mandatory_depends_on": "eval:doc.capitalization_method=='Create a new composite asset'",
"options": "Location"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
"fieldname": "capitalization_method",
"fieldtype": "Select",
"label": "Capitalization Method",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:06:33.080441",
"modified": "2025-01-08 13:14:33.008458",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",

View File

@@ -42,7 +42,6 @@ force_fields = [
"target_is_fixed_asset",
"target_has_serial_no",
"target_has_batch_no",
"target_stock_uom",
"stock_uom",
"fixed_asset_account",
"valuation_rate",
@@ -74,7 +73,6 @@ class AssetCapitalization(StockController):
capitalization_method: DF.Literal["", "Create a new composite asset", "Choose a WIP composite asset"]
company: DF.Link
cost_center: DF.Link | None
entry_type: DF.Literal["Capitalization", "Decapitalization"]
finance_book: DF.Link | None
naming_series: DF.Literal["ACC-ASC-.YYYY.-"]
posting_date: DF.Date
@@ -97,8 +95,6 @@ class AssetCapitalization(StockController):
target_item_name: DF.Data | None
target_qty: DF.Float
target_serial_no: DF.SmallText | None
target_stock_uom: DF.Link | None
target_warehouse: DF.Link | None
title: DF.Data | None
total_value: DF.Currency
# end: auto-generated types
@@ -191,31 +187,18 @@ class AssetCapitalization(StockController):
def validate_target_item(self):
target_item = frappe.get_cached_doc("Item", self.target_item_code)
if not target_item.is_fixed_asset and not target_item.is_stock_item:
frappe.throw(
_("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name)
)
if self.entry_type == "Capitalization" and not target_item.is_fixed_asset:
if not target_item.is_fixed_asset:
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
elif self.entry_type == "Decapitalization" and not target_item.is_stock_item:
frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name))
if target_item.is_fixed_asset:
self.target_qty = 1
if flt(self.target_qty) <= 0:
frappe.throw(_("Target Qty must be a positive number"))
if not target_item.is_stock_item:
self.target_warehouse = None
if not target_item.has_batch_no:
self.target_batch_no = None
if not target_item.has_serial_no:
self.target_serial_no = ""
if target_item.is_stock_item and not self.target_warehouse:
frappe.throw(_("Target Warehouse is mandatory for Decapitalization"))
self.validate_item(target_item)
def validate_target_asset(self):
@@ -232,7 +215,7 @@ class AssetCapitalization(StockController):
)
)
if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"):
if target_asset.status in ("Scrapped", "Sold", "Capitalized"):
frappe.throw(
_("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status)
)
@@ -274,7 +257,7 @@ class AssetCapitalization(StockController):
asset = self.get_asset_for_validation(d.asset)
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized"):
frappe.throw(
_("Row #{0}: Consumed Asset {1} cannot be {2}").format(
d.idx, asset.name, asset.status
@@ -315,9 +298,6 @@ class AssetCapitalization(StockController):
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
def validate_source_mandatory(self):
if not self.target_is_fixed_asset and not self.get("asset_items"):
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
if self.capitalization_method == "Create a new composite asset" and not (
self.get("stock_items") or self.get("asset_items")
):
@@ -421,18 +401,6 @@ class AssetCapitalization(StockController):
)
sl_entries.append(sle)
if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset:
sle = self.get_sl_entries(
self,
{
"item_code": self.target_item_code,
"warehouse": self.target_warehouse,
"actual_qty": flt(self.target_qty),
"incoming_rate": flt(self.target_incoming_rate),
},
)
sl_entries.append(sle)
# reverse sl entries if cancel
if self.docstatus == 2:
sl_entries.reverse()
@@ -475,21 +443,18 @@ class AssetCapitalization(StockController):
return gl_entries
def get_target_account(self):
if self.target_is_fixed_asset:
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
if is_cwip_accounting_enabled(asset_category):
target_account = get_asset_category_account(
"capital_work_in_progress_account",
asset_category=asset_category,
company=self.company,
)
return target_account if target_account else self.target_fixed_asset_account
else:
return self.target_fixed_asset_account
asset_category = frappe.get_cached_value("Asset", self.target_asset, "asset_category")
if is_cwip_accounting_enabled(asset_category):
target_account = get_asset_category_account(
"capital_work_in_progress_account",
asset_category=asset_category,
company=self.company,
)
return target_account if target_account else self.target_fixed_asset_account
else:
return self.warehouse_account[self.target_warehouse]["account"]
return self.target_fixed_asset_account
def get_gl_entries_for_consumed_stock_items(self, gl_entries, target_account, target_against, precision):
# Consumed Stock Items
@@ -590,33 +555,9 @@ class AssetCapitalization(StockController):
item=self,
)
)
else:
# Target Stock Item
sle_list = self.sle_map.get(self.name)
for sle in sle_list:
stock_value_difference = flt(sle.stock_value_difference, precision)
account = self.warehouse_account[sle.warehouse]["account"]
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": ", ".join(target_against),
"cost_center": self.cost_center,
"project": self.get("project"),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
"debit": stock_value_difference,
},
self.warehouse_account[sle.warehouse]["account_currency"],
item=self,
)
)
def create_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Create a new composite asset"
):
if self.capitalization_method != "Create a new composite asset":
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
@@ -639,6 +580,7 @@ class AssetCapitalization(StockController):
self.target_fixed_asset_account = get_asset_category_account(
"fixed_asset_account", item=self.target_item_code, company=asset_doc.company
)
asset_doc.set_status("Work In Progress")
add_asset_activity(
asset_doc.name,
@@ -654,17 +596,15 @@ class AssetCapitalization(StockController):
)
def update_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Choose a WIP composite asset"
):
if self.capitalization_method != "Choose a WIP composite asset":
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_amount = total_target_asset_value
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
asset_doc.set_status("Work In Progress")
asset_doc.flags.ignore_validate = True
asset_doc.save()
@@ -699,14 +639,6 @@ class AssetCapitalization(StockController):
get_link_to_form("Asset Capitalization", self.name)
),
)
else:
asset.set_status("Decapitalized")
add_asset_activity(
asset.name,
_("Asset decapitalized after Asset Capitalization {0} was submitted").format(
get_link_to_form("Asset Capitalization", self.name)
),
)
else:
asset.set_status()
add_asset_activity(
@@ -728,16 +660,12 @@ def get_target_item_details(item_code=None, company=None):
# Set Item Details
out.target_item_name = item.item_name
out.target_stock_uom = item.stock_uom
out.target_is_fixed_asset = cint(item.is_fixed_asset)
out.target_has_batch_no = cint(item.has_batch_no)
out.target_has_serial_no = cint(item.has_serial_no)
if out.target_is_fixed_asset:
out.target_qty = 1
out.target_warehouse = None
else:
out.target_asset = None
if not out.target_has_batch_no:
out.target_batch_no = None

View File

@@ -61,7 +61,6 @@ class TestAssetCapitalization(IntegrationTestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
@@ -76,7 +75,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
@@ -96,6 +94,7 @@ class TestAssetCapitalization(IntegrationTestCase):
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
self.assertEqual(target_asset.status, "Work In Progress")
# Test Consumed Asset values
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
@@ -151,7 +150,6 @@ class TestAssetCapitalization(IntegrationTestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
@@ -166,7 +164,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
@@ -243,7 +240,6 @@ class TestAssetCapitalization(IntegrationTestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
@@ -255,7 +251,6 @@ class TestAssetCapitalization(IntegrationTestCase):
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
self.assertEqual(asset_capitalization.target_qty, 1)
@@ -270,6 +265,7 @@ class TestAssetCapitalization(IntegrationTestCase):
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_amount, total_amount)
self.assertEqual(target_asset.status, "Work In Progress")
# Test General Ledger Entries
expected_gle = {
@@ -295,110 +291,6 @@ class TestAssetCapitalization(IntegrationTestCase):
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_decapitalization_with_depreciation(self):
# Variables
purchase_date = "2020-01-01"
depreciation_start_date = "2020-12-31"
capitalization_date = "2021-06-30"
total_number_of_depreciations = 3
expected_value_after_useful_life = 10_000
consumed_asset_purchase_value = 100_000
consumed_asset_current_value = 70_000
consumed_asset_value_before_disposal = 55_000
target_qty = 10
target_incoming_rate = 5500
depreciation_before_disposal_amount = 15_000
accumulated_depreciation = 45_000
# to accomodate for depreciation on disposal calculation minor difference
consumed_asset_value_before_disposal = 55_123.29
target_incoming_rate = 5512.329
depreciation_before_disposal_amount = 14_876.71
accumulated_depreciation = 44_876.71
# Create assets
consumed_asset = create_depreciation_asset(
asset_name="Asset Capitalization Consumable Asset",
asset_value=consumed_asset_purchase_value,
purchase_date=purchase_date,
depreciation_start_date=depreciation_start_date,
depreciation_method="Straight Line",
total_number_of_depreciations=total_number_of_depreciations,
frequency_of_depreciation=12,
expected_value_after_useful_life=expected_value_after_useful_life,
company="_Test Company with perpetual inventory",
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Active")
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Decapitalization",
posting_date=capitalization_date, # half a year
target_item_code="Capitalization Target Stock Item",
target_qty=target_qty,
consumed_asset=consumed_asset.name,
company="_Test Company with perpetual inventory",
submit=1,
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Decapitalization")
self.assertEqual(
asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value
)
self.assertEqual(
asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal
)
self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal)
self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal)
self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate)
# Test Consumed Asset values
consumed_asset.reload()
self.assertEqual(consumed_asset.status, "Decapitalized")
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEqual(second_asset_depr_schedule.status, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule")
consumed_depreciation_schedule = [
d
for d in depr_schedule_of_consumed_asset
if getdate(d.schedule_date) == getdate(capitalization_date)
]
self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry)
self.assertEqual(
consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount
)
# Test General Ledger Entries
expected_gle = {
"_Test Warehouse - TCP1": consumed_asset_value_before_disposal,
"_Test Accumulated Depreciations - TCP1": accumulated_depreciation,
"_Test Fixed Asset - TCP1": -consumed_asset_purchase_value,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle)
# Cancel Asset Capitalization and make test entries and status are reversed
asset_capitalization.reload()
asset_capitalization.cancel()
self.assertEqual(consumed_asset.db_get("status"), "Partially Depreciated")
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_capitalize_only_service_item(self):
company = "_Test Company"
# Variables
@@ -418,7 +310,6 @@ class TestAssetCapitalization(IntegrationTestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
@@ -466,13 +357,11 @@ def create_asset_capitalization(**args):
target_item_code = target_asset.item_code or args.target_item_code
company = target_asset.company or args.company or "_Test Company"
warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company)
target_warehouse = args.target_warehouse or warehouse
source_warehouse = args.source_warehouse or warehouse
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{
"entry_type": args.entry_type or "Capitalization",
"capitalization_method": args.capitalization_method or None,
"company": company,
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
@@ -480,7 +369,6 @@ def create_asset_capitalization(**args):
"target_item_code": target_item_code,
"target_asset": target_asset.name,
"target_asset_location": "Test Location",
"target_warehouse": target_warehouse,
"target_qty": flt(args.target_qty) or 1,
"target_batch_no": args.target_batch_no,
"target_serial_no": args.target_serial_no,

View File

@@ -34,6 +34,7 @@ frappe.ui.form.on("Depreciation Schedule", {
asset_depr_schedule_name: frm.doc.name,
date: row.schedule_date,
},
debounce: 1000,
callback: function (r) {
frappe.model.sync(r.message);
frm.refresh();

View File

@@ -13,6 +13,7 @@ from frappe.utils import (
flt,
get_first_day,
get_last_day,
get_link_to_form,
getdate,
is_last_day_of_the_month,
month_diff,
@@ -1062,7 +1063,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
if not current_asset_depr_schedule_doc:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
asset_doc.name, row.finance_book
get_link_to_form("Asset", asset_doc.name), row.finance_book
)
)
@@ -1108,7 +1109,7 @@ def get_temp_asset_depr_schedule_doc(
if not current_asset_depr_schedule_doc:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
asset_doc.name, row.finance_book
get_link_to_form("Asset", asset_doc.name), row.finance_book
)
)

View File

@@ -4,6 +4,7 @@
"allow_rename": 1,
"autoname": "field:location_name",
"creation": "2018-05-07 12:49:22.595974",
"default_view": "Tree",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -142,7 +143,7 @@
],
"is_tree": 1,
"links": [],
"modified": "2024-03-27 13:10:02.572355",
"modified": "2025-01-22 10:47:32.581537",
"modified_by": "Administrator",
"module": "Assets",
"name": "Location",

View File

@@ -66,12 +66,12 @@ def get_conditions(filters):
conditions["cost_center"] = filters.get("cost_center")
if status:
# In Store assets are those that are not sold or scrapped or capitalized or decapitalized
# In Store assets are those that are not sold or scrapped or capitalized
operand = "not in"
if status not in "In Location":
operand = "in"
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized", "Decapitalized"])
conditions["status"] = (operand, ["Sold", "Scrapped", "Capitalized"])
return conditions
@@ -272,9 +272,9 @@ def get_asset_depreciation_amount_map(filters, finance_book):
query = query.where(asset.cost_center == filters.cost_center)
if filters.status:
if filters.status == "In Location":
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
query = query.where(asset.status.notin(["Sold", "Scrapped", "Capitalized"]))
else:
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized", "Decapitalized"]))
query = query.where(asset.status.isin(["Sold", "Scrapped", "Capitalized"]))
if finance_book:
query = query.where((gle.finance_book.isin([cstr(finance_book), ""])) | (gle.finance_book.isnull()))
else:

View File

@@ -469,6 +469,9 @@ class PurchaseOrder(BuyingController):
if self.is_against_so():
self.update_status_updater()
if self.is_against_pp():
self.update_status_updater_if_from_pp()
self.update_prevdoc_status()
if not self.is_subcontracted or self.is_old_subcontracting_flow:
self.update_requested_qty()
@@ -550,6 +553,20 @@ class PurchaseOrder(BuyingController):
}
)
def update_status_updater_if_from_pp(self):
self.status_updater.append(
{
"source_dt": "Purchase Order Item",
"target_dt": "Production Plan Sub Assembly Item",
"join_field": "production_plan_sub_assembly_item",
"target_field": "received_qty",
"target_parent_dt": "Production Plan",
"target_parent_field": "",
"target_ref_field": "qty",
"source_field": "fg_item_qty",
}
)
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""
sales_orders_to_update = []
@@ -570,6 +587,9 @@ class PurchaseOrder(BuyingController):
def is_against_so(self):
return any(d.sales_order for d in self.items if d.sales_order)
def is_against_pp(self):
return any(d.production_plan for d in self.items if d.production_plan)
def set_received_qty_for_drop_ship_items(self):
for item in self.items:
if item.delivered_by_supplier == 1:

View File

@@ -47,16 +47,22 @@ frappe.listview_settings["Purchase Order"] = {
listview.call_for_selected_items(method, { status: "Submitted" });
});
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
});
if (frappe.model.can_create("Purchase Invoice")) {
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
});
}
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
});
if (frappe.model.can_create("Purchase Receipt")) {
listview.page.add_action_item(__("Purchase Receipt"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
});
}
listview.page.add_action_item(__("Advance Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
if (frappe.model.can_create("Payment Entry")) {
listview.page.add_action_item(__("Advance Payment"), () => {
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
});
}
},
};

View File

@@ -11,12 +11,20 @@ frappe.listview_settings["Supplier Quotation"] = {
},
onload: function (listview) {
listview.page.add_action_item(__("Purchase Order"), () => {
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
});
if (frappe.model.can_create("Purchase Order")) {
listview.page.add_action_item(__("Purchase Order"), () => {
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
});
}
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
});
if (frappe.model.can_create("Purchase Invoice")) {
listview.page.add_action_item(__("Purchase Invoice"), () => {
erpnext.bulk_transaction_processing.create(
listview,
"Supplier Quotation",
"Purchase Invoice"
);
});
}
},
};

View File

@@ -8,7 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold, qb, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder import Criterion
from frappe.query_builder import Criterion, DocType
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import (
@@ -255,6 +255,8 @@ class AccountsController(TransactionBase):
self.validate_deferred_income_expense_account()
self.set_inter_company_account()
self.set_taxes_and_charges()
if self.doctype == "Purchase Invoice":
self.calculate_paid_amount()
# apply tax withholding only if checked and applicable
@@ -269,6 +271,7 @@ class AccountsController(TransactionBase):
self.set_total_in_words()
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
@@ -407,6 +410,39 @@ class AccountsController(TransactionBase):
for row in batches:
frappe.delete_doc("Batch", row.name)
def validate_company_in_accounting_dimension(self):
doc_field = DocType("DocField")
accounting_dimension = DocType("Accounting Dimension")
dimension_list = (
frappe.qb.from_(accounting_dimension)
.select(accounting_dimension.document_type)
.join(doc_field)
.on(doc_field.parent == accounting_dimension.document_type)
.where(doc_field.fieldname == "company")
).run(as_list=True)
dimension_list = sum(dimension_list, ["Project"])
self.validate_company(dimension_list)
for child in self.get_all_children() or []:
self.validate_company(dimension_list, child)
def validate_company(self, dimension_list, child=None):
for dimension in dimension_list:
if not child:
dimension_value = self.get(frappe.scrub(dimension))
else:
dimension_value = child.get(frappe.scrub(dimension))
if dimension_value:
company = frappe.get_cached_value(dimension, dimension_value, "company")
if company and company != self.company:
frappe.throw(
_("{0}: {1} does not belong to the Company: {2}").format(
dimension, frappe.bold(dimension_value), self.company
)
)
def validate_return_against_account(self):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
@@ -671,21 +707,15 @@ class AccountsController(TransactionBase):
if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
self.due_date = posting_date
elif self.doctype == "Sales Invoice":
if not self.due_date:
frappe.throw(_("Due Date is mandatory"))
elif self.doctype in ["Sales Invoice", "Purchase Invoice"]:
bill_date = self.bill_date if self.doctype == "Purchase Invoice" else None
validate_due_date(
posting_date,
self.due_date,
self.payment_terms_template,
)
elif self.doctype == "Purchase Invoice":
validate_due_date(
posting_date,
self.due_date,
self.bill_date,
self.payment_terms_template,
posting_date=posting_date,
due_date=self.due_date,
bill_date=bill_date,
template_name=self.payment_terms_template,
doctype=self.doctype,
)
def set_price_list_currency(self, buying_or_selling):
@@ -785,7 +815,14 @@ class AccountsController(TransactionBase):
ret = get_item_details(ctx, self, for_validate=for_validate, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
if item.get(fieldname) is None or fieldname in force_item_fields:
if (
item.get(fieldname) is None
or fieldname in force_item_fields
or (
fieldname in ["serial_no", "batch_no"]
and item.get("use_serial_batch_fields")
)
):
item.set(fieldname, value)
elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
@@ -939,6 +976,12 @@ class AccountsController(TransactionBase):
):
return True
def set_taxes_and_charges(self):
if frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
if hasattr(self, "taxes_and_charges") and not self.get("taxes") and not self.get("is_pos"):
if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
self.append_taxes_from_master(tax_master_doctype)
def append_taxes_from_master(self, tax_master_doctype=None):
if self.get("taxes_and_charges"):
if not tax_master_doctype:
@@ -2434,10 +2477,15 @@ class AccountsController(TransactionBase):
)
if (
flt(total, self.precision("grand_total")) - flt(grand_total, self.precision("grand_total"))
abs(
flt(total, self.precision("grand_total"))
- flt(grand_total, self.precision("grand_total"))
)
> 0.1
or flt(base_total, self.precision("base_grand_total"))
- flt(base_grand_total, self.precision("base_grand_total"))
or abs(
flt(base_total, self.precision("base_grand_total"))
- flt(base_grand_total, self.precision("base_grand_total"))
)
> 0.1
):
frappe.throw(
@@ -2992,6 +3040,7 @@ def get_advance_payment_entries(
(payment_ref.allocated_amount).as_("amount"),
(payment_ref.name).as_("reference_row"),
(payment_ref.reference_name).as_("against_order"),
(payment_entry.book_advance_payments_in_separate_party_account),
)
q = q.where(payment_ref.reference_doctype == order_doctype)
@@ -3036,6 +3085,7 @@ def get_common_query(
(payment_entry.name).as_("reference_name"),
payment_entry.posting_date,
(payment_entry.remarks).as_("remarks"),
(payment_entry.book_advance_payments_in_separate_party_account),
)
.where(payment_entry.payment_type == payment_type)
.where(payment_entry.party_type == party_type)
@@ -3711,6 +3761,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
).format(frappe.bold(parent.name))
)
else: # Sales Order
parent.validate_for_duplicate_items()
parent.validate_warehouse()
parent.update_reserved_qty()
parent.update_project()

View File

@@ -110,7 +110,7 @@ def validate_returned_items(doc):
for d in doc.get("items"):
key = d.item_code
raise_exception = False
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice"]:
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Sales Invoice", "POS Invoice"]:
field = frappe.scrub(doc.doctype) + "_item"
if d.get(field):
key = (d.item_code, d.get(field))
@@ -174,7 +174,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
)
for column in fields:
returned_qty = flt(already_returned_data.get(column, 0)) if len(already_returned_data) > 0 else 0
returned_qty = (
flt(already_returned_data.get(column, 0), stock_qty_precision)
if len(already_returned_data) > 0
else 0
)
if column == "stock_qty" and not args.get("return_qty_from_rejected_warehouse"):
reference_qty = ref.get(column)
@@ -186,7 +190,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
max_returnable_qty = flt(reference_qty, stock_qty_precision) - returned_qty
max_returnable_qty = flt(flt(reference_qty, stock_qty_precision) - returned_qty, stock_qty_precision)
label = column.replace("_", " ").title()
if reference_qty:
@@ -259,7 +263,7 @@ def get_already_returned_items(doc):
)
data = frappe.db.sql(
f"""
select {column}, {field}
select {column}, child.{field}
from
`tab{doc.doctype} Item` child, `tab{doc.doctype}` par
where
@@ -370,6 +374,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
if doc.get("is_return"):
if doc.doctype == "Sales Invoice" or doc.doctype == "POS Invoice":
doc.consolidated_invoice = ""
# no copy enabled for party_account_currency
doc.party_account_currency = source.party_account_currency
doc.set("payments", [])
doc.update_billed_amount_in_delivery_note = True
for data in source.payments:
@@ -1020,7 +1026,7 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
if not qty_field:
qty_field = "qty"
qty_field = "stock_qty"
if not warehouse_field:
warehouse_field = "warehouse"
@@ -1109,7 +1115,7 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
warehouse_field = "warehouse"
if not qty_field:
qty_field = "qty"
qty_field = "stock_qty"
warehouse = child_doc.get(warehouse_field)
if parent_doc.get("is_internal_customer"):
@@ -1150,3 +1156,9 @@ def get_available_serial_nos(serial_nos, warehouse):
return frappe.get_all(
"Serial No", filters={"warehouse": warehouse, "name": ("in", serial_nos)}, pluck="name"
)
@frappe.whitelist()
def get_payment_data(invoice):
payment = frappe.db.get_all("Sales Invoice Payment", {"parent": invoice}, ["mode_of_payment", "amount"])
return payment

View File

@@ -330,7 +330,7 @@ class SellingController(StockController):
"batch_no": p.batch_no if self.docstatus == 2 else None,
"uom": p.uom,
"serial_and_batch_bundle": p.serial_and_batch_bundle
or get_serial_and_batch_bundle(p, self),
or get_serial_and_batch_bundle(p, self, d),
"name": d.name,
"target_warehouse": p.target_warehouse,
"company": self.company,
@@ -711,6 +711,16 @@ class SellingController(StockController):
if self.doctype == "POS Invoice":
return
items = [item.item_code for item in self.get("items")]
item_stock_map = frappe._dict(
frappe.get_all(
"Item",
filters={"item_code": ["in", items]},
fields=["item_code", "is_stock_item"],
as_list=True,
)
)
for d in self.get("items"):
if self.doctype == "Sales Invoice":
stock_items = [
@@ -744,7 +754,7 @@ class SellingController(StockController):
frappe.bold(_("Allow Item to Be Added Multiple Times in a Transaction")),
get_link_to_form("Selling Settings", "Selling Settings"),
)
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
if item_stock_map.get(d.item_code):
if stock_items in check_list:
frappe.throw(duplicate_items_msg)
else:
@@ -772,6 +782,12 @@ class SellingController(StockController):
msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype))
frappe.msgprint(msg, title="Internal Transfer", alert=True)
if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items):
msg = _("Target Warehouse set for some items but the customer is not an internal customer.")
msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype))
frappe.msgprint(msg, title="Internal Transfer")
def validate_items(self):
# validate items to see if they have is_sales_item enabled
from erpnext.controllers.buying_controller import validate_item_type
@@ -786,7 +802,7 @@ def set_default_income_account_for_item(obj):
set_item_default(d.item_code, obj.company, "income_account", d.income_account)
def get_serial_and_batch_bundle(child, parent):
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if child.get("use_serial_batch_fields"):
@@ -806,7 +822,7 @@ def get_serial_and_batch_bundle(child, parent):
"warehouse": child.warehouse,
"voucher_type": parent.doctype,
"voucher_no": parent.name if parent.docstatus < 2 else None,
"voucher_detail_no": child.name,
"voucher_detail_no": delivery_note_child.name if delivery_note_child else child.name,
"posting_date": parent.posting_date,
"posting_time": parent.posting_time,
"qty": child.qty,

View File

@@ -205,19 +205,19 @@ class StatusUpdater(Document):
Get the status of the document.
Returns:
dict: A dictionary containing the status. This allows callers to receive
a dictionary for efficient bulk updates, for example when `per_billed`
and other status fields also need to be updated.
dict: A dictionary containing the status. This allows callers to receive
a dictionary for efficient bulk updates, for example when `per_billed`
and other status fields also need to be updated.
Note:
Can be overriden on a doctype to implement more localized status updater logic.
Can be overriden on a doctype to implement more localized status updater logic.
Example:
{
"status": "Draft",
"per_billed": 50,
"billing_status": "Partly Billed"
}
{
"status": "Draft",
"per_billed": 50,
"billing_status": "Partly Billed"
}
"""
if self.doctype not in status_map:
return {"status": self.status}
@@ -279,9 +279,20 @@ class StatusUpdater(Document):
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
args["name"] = d.get(args["join_field"])
is_from_pp = (
hasattr(d, "production_plan_sub_assembly_item")
and frappe.db.get_value(
"Production Plan Sub Assembly Item",
d.production_plan_sub_assembly_item,
"type_of_manufacturing",
)
== "Subcontract"
)
args["item_code"] = "production_item" if is_from_pp else "item_code"
# get all qty where qty > target_field
item = frappe.db.sql(
"""select item_code, `{target_ref_field}`,
"""select `{item_code}` as item_code, `{target_ref_field}`,
`{target_field}`, parenttype, parent from `tab{target_dt}`
where `{target_ref_field}` < `{target_field}`
and name=%s and docstatus=1""".format(**args),

View File

@@ -216,6 +216,10 @@ class StockController(AccountsController):
if self.doctype == "Asset Capitalization":
table_name = "stock_items"
parent_details = frappe._dict()
if table_name == "packed_items":
parent_details = self.get_parent_details_for_packed_items()
for row in self.get(table_name):
if row.serial_and_batch_bundle and (row.serial_no or row.batch_no):
self.validate_serial_nos_and_batches_with_bundle(row)
@@ -246,13 +250,20 @@ class StockController(AccountsController):
}
if row.get("qty") or row.get("consumed_qty") or row.get("stock_qty"):
self.update_bundle_details(bundle_details, table_name, row)
self.update_bundle_details(bundle_details, table_name, row, parent_details=parent_details)
self.create_serial_batch_bundle(bundle_details, row)
if row.get("rejected_qty"):
self.update_bundle_details(bundle_details, table_name, row, is_rejected=True)
self.create_serial_batch_bundle(bundle_details, row)
def get_parent_details_for_packed_items(self):
parent_details = frappe._dict()
for row in self.get("items"):
parent_details[row.name] = row
return parent_details
def make_bundle_for_sales_purchase_return(self, table_name=None):
if not self.get("is_return"):
return
@@ -387,7 +398,7 @@ class StockController(AccountsController):
return False
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False):
def update_bundle_details(self, bundle_details, table_name, row, is_rejected=False, parent_details=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# Since qty field is different for different doctypes
@@ -429,6 +440,11 @@ class StockController(AccountsController):
warehouse = row.get("target_warehouse") or row.get("warehouse")
type_of_transaction = "Outward"
if table_name == "packed_items":
if not warehouse:
warehouse = parent_details[row.parent_detail_docname].warehouse
bundle_details["voucher_detail_no"] = parent_details[row.parent_detail_docname].name
bundle_details.update(
{
"qty": qty,
@@ -924,9 +940,11 @@ class StockController(AccountsController):
row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname])
def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.stock.serial_batch_bundle import update_batch_qty
from erpnext.stock.stock_ledger import make_sl_entries
make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher)
def make_gl_entries_on_cancel(self):
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))

View File

@@ -1263,6 +1263,7 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
"item_code": row.item_details["rm_item_code"],
"subcontracted_item": row.item_details["main_item_code"],
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
"use_serial_batch_fields": 1,
}
)
@@ -1303,10 +1304,13 @@ def make_return_stock_entry_for_subcontract(
if not value.qty:
continue
if item_details := value.get("item_details"):
item_details["serial_and_batch_bundle"] = None
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field, batch_no)
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)

View File

@@ -34,6 +34,11 @@ class calculate_taxes_and_totals:
"Accounts Settings", "round_row_wise_tax"
)
if doc.get("round_off_applicable_accounts_for_tax_withholding"):
frappe.flags.round_off_applicable_accounts.append(
doc.round_off_applicable_accounts_for_tax_withholding
)
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)

View File

@@ -9,6 +9,7 @@ from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests import IntegrationTestCase
from frappe.utils import add_days, getdate, nowdate
from frappe.utils.data import getdate as convert_to_date
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
@@ -16,6 +17,7 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -870,6 +872,69 @@ class TestAccountsController(IntegrationTestCase):
self.assertEqual(pi.items[0].rate, arms_length_price)
self.assertEqual(pi.items[0].valuation_rate, 100)
@IntegrationTestCase.change_settings(
"Accounts Settings", {"exchange_gain_loss_posting_date": "Reconciliation Date"}
)
def test_17_gain_loss_posting_date_for_normal_payment(self):
# Sales Invoice in Foreign Currency
rate = 80
rate_in_account_currency = 1
adv_date = convert_to_date(add_days(nowdate(), -2))
inv_date = convert_to_date(add_days(nowdate(), -1))
si = self.create_sales_invoice(posting_date=inv_date, qty=1, rate=rate_in_account_currency)
# Test payments with different exchange rates
pe = self.create_payment_entry(posting_date=adv_date, amount=1, source_exc_rate=75.1).save().submit()
pr = self.create_payment_reconciliation()
pr.from_invoice_date = add_days(nowdate(), -1)
pr.to_invoice_date = nowdate()
pr.from_payment_date = add_days(nowdate(), -2)
pr.to_payment_date = nowdate()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# Outstanding in both currencies should be '0'
si.reload()
self.assertEqual(si.outstanding_amount, 0)
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
# Exchange Gain/Loss Journal should've been created.
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 1)
self.assertEqual(len(exc_je_for_pe), 1)
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
self.assertEqual(
getdate(nowdate()), frappe.db.get_value("Journal Entry", exc_je_for_pe[0].parent, "posting_date")
)
# Cancel Payment
pe.reload()
pe.cancel()
# outstanding should be same as grand total
si.reload()
self.assertEqual(si.outstanding_amount, rate_in_account_currency)
self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
@@ -1472,32 +1537,32 @@ class TestAccountsController(IntegrationTestCase):
# Invoices
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si1.department = "Management"
si1.department = "Management - _TC"
si1.save().submit()
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si2.department = "Operations"
si2.department = "Operations - _TC"
si2.save().submit()
# Payments
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note1.department = "Management"
cr_note1.department = "Management - _TC"
cr_note1.is_return = 1
cr_note1.save().submit()
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note2.department = "Legal"
cr_note2.department = "Legal - _TC"
cr_note2.is_return = 1
cr_note2.save().submit()
pe1 = get_payment_entry(si1.doctype, si1.name)
pe1.references = []
pe1.department = "Research & Development"
pe1.department = "Research & Development - _TC"
pe1.save().submit()
pe2 = get_payment_entry(si1.doctype, si1.name)
pe2.references = []
pe2.department = "Management"
pe2.department = "Management - _TC"
pe2.save().submit()
je1 = self.create_journal_entry(
@@ -1510,7 +1575,7 @@ class TestAccountsController(IntegrationTestCase):
)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1.accounts[0].department = "Management"
je1.accounts[0].department = "Management - _TC"
je1.save().submit()
# assert dimension filter's result
@@ -1519,17 +1584,17 @@ class TestAccountsController(IntegrationTestCase):
self.assertEqual(len(pr.invoices), 2)
self.assertEqual(len(pr.payments), 5)
pr.department = "Legal"
pr.department = "Legal - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
pr.department = "Management"
pr.department = "Management - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 3)
pr.department = "Research & Development"
pr.department = "Research & Development - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 1)
@@ -1540,17 +1605,17 @@ class TestAccountsController(IntegrationTestCase):
# Invoice
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
si.department = "Management"
si.department = "Management - _TC"
si.save().submit()
# Payment
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
cr_note.department = "Management"
cr_note.department = "Management - _TC"
cr_note.is_return = 1
cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.department = "Management"
pr.department = "Management - _TC"
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
@@ -1582,7 +1647,7 @@ class TestAccountsController(IntegrationTestCase):
# Sales Invoice in Foreign Currency
self.setup_dimensions()
rate_in_account_currency = 1
dpt = "Research & Development"
dpt = "Research & Development - _TC"
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
si.department = dpt
@@ -1617,7 +1682,7 @@ class TestAccountsController(IntegrationTestCase):
def test_93_dimension_inheritance_on_advance(self):
self.setup_dimensions()
dpt = "Research & Development"
dpt = "Research & Development - _TC"
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
adv.department = dpt
@@ -2075,3 +2140,13 @@ class TestAccountsController(IntegrationTestCase):
journal_voucher = frappe.get_doc("Journal Entry", exc_je_for_pi[0].parent)
purchase_invoice = frappe.get_doc("Purchase Invoice", pi.name)
self.assertEqual(purchase_invoice.advances[0].difference_posting_date, journal_voucher.posting_date)
def test_company_validation_in_dimension(self):
si = create_sales_invoice(do_not_submit=True)
project = make_project({"project_name": "_Test Demo Project1", "company": "_Test Company 1"})
si.project = project.name
self.assertRaises(frappe.ValidationError, si.save)
si_1 = create_sales_invoice(do_not_submit=True)
si_1.items[0].project = project.name
self.assertRaises(frappe.ValidationError, si_1.save)

View File

@@ -282,6 +282,79 @@ class TestSubcontractingController(IntegrationTestCase):
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_return_non_consumed_batch_materials(self):
"""
- Set backflush based on Material Transfer.
- Create SCO for item Subcontracted Item SA2.
- Transfer the batched components from Stores to Supplier warehouse with serial nos.
- Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
- Create SCR for full qty against the SCO and change the qty of raw material.
- After that return the non consumed material back to the store from supplier's warehouse.
"""
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
set_backflush_based_on("Material Transferred for Subcontract")
service_item = make_item("Subcontracted Service FG Item A", properties={"is_stock_item": 0}).name
fg_item = make_item(
"Subcontracted FG Item SA2", properties={"is_stock_item": 1, "is_sub_contracted_item": 1}
).name
rm_item = make_item(
"Subcontracted Batch RM Item SA2",
properties={
"is_stock_item": 1,
"create_new_batch": 1,
"has_batch_no": 1,
"batch_number_series": "BATCH-RM-IRM-.####",
},
).name
make_bom(item=fg_item, raw_materials=[rm_item], rate=100, currency="INR")
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": service_item,
"qty": 5,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 5,
},
]
sco = get_subcontracting_order(service_items=service_items)
rm_items = get_rm_items(sco.supplied_items)
rm_items[0]["qty"] += 1
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for item in rm_items:
item["sco_rm_detail"] = sco.items[0].name
make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
scr1 = make_subcontracting_receipt(sco.name)
scr1.save()
scr1.supplied_items[0].consumed_qty = 5
scr1.submit()
for key, value in get_supplied_items(scr1).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 5)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5]))
sco.load_from_db()
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
doc.save()
self.assertEqual(doc.items[0].qty, 1)
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
self.assertTrue(doc.items[0].batch_no)
self.assertTrue(doc.items[0].use_serial_batch_fields)
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
def test_return_non_consumed_materials(self):
"""
- Set backflush based on Material Transfer.

View File

@@ -17,7 +17,9 @@
"quotation_section",
"default_valid_till",
"section_break_13",
"carry_forward_communication_and_comments"
"carry_forward_communication_and_comments",
"column_break_junk",
"update_timestamp_on_new_communication"
],
"fields": [
{
@@ -77,7 +79,7 @@
{
"fieldname": "section_break_13",
"fieldtype": "Section Break",
"label": "Other Settings"
"label": "Activity"
},
{
"default": "0",
@@ -85,13 +87,24 @@
"fieldname": "carry_forward_communication_and_comments",
"fieldtype": "Check",
"label": "Carry Forward Communication and Comments"
},
{
"fieldname": "column_break_junk",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Update the modified timestamp on new communications received in Lead & Opportunity.",
"fieldname": "update_timestamp_on_new_communication",
"fieldtype": "Check",
"label": "Update timestamp on new communication"
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:06:47.508534",
"modified": "2025-01-16 16:12:14.889455",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",

View File

@@ -20,6 +20,7 @@ class CRMSettings(Document):
carry_forward_communication_and_comments: DF.Check
close_opportunity_after_days: DF.Int
default_valid_till: DF.Data | None
update_timestamp_on_new_communication: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -55,12 +55,13 @@ def get_columns():
"options": "Company",
"width": 120,
},
{"fieldname": "address", "label": _("Address"), "fieldtype": "Data", "width": 130},
{"fieldname": "state", "label": _("State"), "fieldtype": "Data", "width": 100},
{"fieldname": "pincode", "label": _("Postal Code"), "fieldtype": "Data", "width": 90},
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
{
"fieldname": "country",
"label": _("Country"),
"fieldname": "country",
"fieldtype": "Link",
"options": "Country",
"width": 100,
@@ -93,8 +94,9 @@ def get_data(filters):
lead.owner,
lead.company,
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
address.state,
address.pincode,
address.city,
address.state,
address.country,
)
.where(lead.company == filters.company)

View File

@@ -84,6 +84,20 @@ def link_communications_with_prospect(communication, method):
row.db_update()
def update_modified_timestamp(communication, method):
if communication.reference_doctype and communication.reference_name:
if communication.sent_or_received == "Received" and frappe.db.get_single_value(
"CRM Settings", "update_timestamp_on_new_communication"
):
frappe.db.set_value(
dt=communication.reference_doctype,
dn=communication.reference_name,
field="modified",
val=now(),
update_modified=False,
)
def get_linked_prospect(reference_doctype, reference_name):
prospect = None
if reference_doctype == "Lead":

View File

@@ -69,23 +69,7 @@ class PlaidConnector:
else:
return response["link_token"]
def auth(self):
try:
self.client.Auth.get(self.access_token)
except ItemError as e:
if e.code == "ITEM_LOGIN_REQUIRED":
pass
except APIError as e:
if e.code == "PLANNED_MAINTENANCE":
pass
except requests.Timeout:
pass
except Exception as e:
frappe.log_error("Plaid: Authentication error")
frappe.throw(_(str(e)), title=_("Authentication Failed"))
def get_transactions(self, start_date, end_date, account_id=None):
self.auth()
kwargs = dict(access_token=self.access_token, start_date=start_date, end_date=end_date)
if account_id:
kwargs.update(dict(account_ids=[account_id]))

View File

@@ -29,7 +29,7 @@ frappe.ui.form.on("Plaid Settings", {
"Bank Transaction",
"",
true,
"Bank Transaction"
__("Bank Transaction")
);
frappe.msgprint({

View File

@@ -1,77 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("QuickBooks Migrator", {
connect: function (frm) {
// OAuth requires user intervention to provide application access permissionsto requested scope
// Here we open a new window and redirect user to the authorization url.
// After user grants us permission to access. We will set authorization details on this doc which will force refresh.
window.open(frm.doc.authorization_url);
},
fetch_data: function (frm) {
frm.call("migrate");
},
onload: function (frm) {
frm.trigger("set_indicator");
var domain = frappe.urllib.get_base_url();
var redirect_url = `${domain}/api/method/erpnext.erpnext_integrations.doctype.quickbooks_migrator.quickbooks_migrator.callback`;
if (frm.doc.redirect_url != redirect_url) {
frm.set_value("redirect_url", redirect_url);
}
// Instead of changing percentage width and message of single progress bar
// Show a different porgress bar for every action after some time remove the finished progress bar
// Former approach causes the progress bar to dance back and forth.
frm.trigger("set_indicator");
frappe.realtime.on("quickbooks_progress_update", function (data) {
frm.dashboard.show_progress(data.message, (data.count / data.total) * 100, data.message);
if (data.count == data.total) {
window.setTimeout(
function (message) {
frm.dashboard.hide_progress(message);
},
1500,
data.messsage
);
}
});
},
refresh: function (frm) {
frm.trigger("set_indicator");
if (!frm.doc.access_token) {
// Unset access_token signifies that we don't have enough information to connect to quickbooks api and fetch data
if (frm.doc.authorization_url) {
frm.add_custom_button(__("Connect to Quickbooks"), function () {
frm.trigger("connect");
});
}
}
if (frm.doc.access_token) {
// If we have access_token that means we also have refresh_token we don't need user intervention anymore
// All we need now is a Company from erpnext
frm.remove_custom_button(__("Connect to Quickbooks"));
frm.toggle_display("company_settings", 1);
frm.set_df_property("company", "reqd", 1);
if (frm.doc.company) {
frm.add_custom_button(__("Fetch Data"), function () {
frm.trigger("fetch_data");
});
}
}
},
set_indicator: function (frm) {
var indicator_map = {
"Connecting to QuickBooks": [__("Connecting to QuickBooks"), "orange"],
"Connected to QuickBooks": [__("Connected to QuickBooks"), "green"],
"In Progress": [__("In Progress"), "orange"],
Complete: [__("Complete"), "green"],
Failed: [__("Failed"), "red"],
};
if (frm.doc.status) {
var indicator = indicator_map[frm.doc.status];
var label = indicator[0];
var color = indicator[1];
frm.page.set_indicator(label, color);
}
},
});

View File

@@ -1,216 +0,0 @@
{
"actions": [],
"beta": 1,
"creation": "2018-07-10 14:48:16.757030",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"application_settings",
"client_id",
"redirect_url",
"token_endpoint",
"application_column_break",
"client_secret",
"scope",
"api_endpoint",
"authorization_settings",
"authorization_endpoint",
"refresh_token",
"code",
"authorization_column_break",
"authorization_url",
"access_token",
"quickbooks_company_id",
"company_settings",
"company",
"default_shipping_account",
"default_warehouse",
"company_column_break",
"default_cost_center",
"undeposited_funds_account"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Connecting to QuickBooks\nConnected to QuickBooks\nIn Progress\nComplete\nFailed"
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.client_id && doc.client_secret && doc.redirect_url",
"fieldname": "application_settings",
"fieldtype": "Section Break",
"label": "Application Settings"
},
{
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID",
"reqd": 1
},
{
"fieldname": "redirect_url",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Redirect URL",
"reqd": 1
},
{
"default": "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer",
"fieldname": "token_endpoint",
"fieldtype": "Data",
"label": "Token Endpoint",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "application_column_break",
"fieldtype": "Column Break"
},
{
"fieldname": "client_secret",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client Secret",
"reqd": 1
},
{
"default": "com.intuit.quickbooks.accounting",
"fieldname": "scope",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Scope",
"read_only": 1,
"reqd": 1
},
{
"default": "https://quickbooks.api.intuit.com/v3",
"fieldname": "api_endpoint",
"fieldtype": "Data",
"label": "API Endpoint",
"read_only": 1,
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "authorization_settings",
"fieldtype": "Section Break",
"label": "Authorization Settings"
},
{
"default": "https://appcenter.intuit.com/connect/oauth2",
"fieldname": "authorization_endpoint",
"fieldtype": "Data",
"label": "Authorization Endpoint",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "refresh_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Refresh Token"
},
{
"fieldname": "code",
"fieldtype": "Data",
"hidden": 1,
"label": "Code"
},
{
"fieldname": "authorization_column_break",
"fieldtype": "Column Break"
},
{
"fieldname": "authorization_url",
"fieldtype": "Data",
"label": "Authorization URL",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "access_token",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Access Token"
},
{
"fieldname": "quickbooks_company_id",
"fieldtype": "Data",
"hidden": 1,
"label": "Quickbooks Company ID"
},
{
"fieldname": "company_settings",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Company Settings"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "default_shipping_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Default Shipping Account",
"options": "Account"
},
{
"fieldname": "default_warehouse",
"fieldtype": "Link",
"hidden": 1,
"label": "Default Warehouse",
"options": "Warehouse"
},
{
"fieldname": "company_column_break",
"fieldtype": "Column Break"
},
{
"fieldname": "default_cost_center",
"fieldtype": "Link",
"hidden": 1,
"label": "Default Cost Center",
"options": "Cost Center"
},
{
"fieldname": "undeposited_funds_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Undeposited Funds Account",
"options": "Account"
}
],
"issingle": 1,
"links": [],
"modified": "2024-03-27 13:10:30.599062",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "QuickBooks Migrator",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from frappe.tests import IntegrationTestCase
class TestQuickBooksMigrator(IntegrationTestCase):
pass

View File

@@ -1,364 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide("erpnext.tally_migration");
frappe.ui.form.on("Tally Migration", {
onload: function (frm) {
let reload_status = true;
frappe.realtime.on("tally_migration_progress_update", function (data) {
if (reload_status) {
frappe.model.with_doc(frm.doc.doctype, frm.doc.name, () => {
frm.refresh_header();
});
reload_status = false;
}
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
let error_occurred = data.count === -1;
if (data.count == data.total || error_occurred) {
window.setTimeout(
(title) => {
frm.dashboard.hide_progress(title);
frm.reload_doc();
if (error_occurred) {
frappe.msgprint({
message: __("An error has occurred during {0}. Check {1} for more details", [
repl(
"<a href='/app/tally-migration/%(tally_document)s' class='variant-click'>%(tally_document)s</a>",
{
tally_document: frm.docname,
}
),
"<a href='/app/error-log' class='variant-click'>Error Log</a>",
]),
title: __("Tally Migration Error"),
indicator: "red",
});
}
},
2000,
data.title
);
}
});
},
refresh: function (frm) {
frm.trigger("show_logs_preview");
erpnext.tally_migration.failed_import_log = JSON.parse(frm.doc.failed_import_log);
erpnext.tally_migration.fixed_errors_log = JSON.parse(frm.doc.fixed_errors_log);
["default_round_off_account", "default_warehouse", "default_cost_center"].forEach((account) => {
frm.toggle_reqd(account, frm.doc.is_master_data_imported === 1);
frm.toggle_enable(account, frm.doc.is_day_book_data_processed != 1);
});
if (frm.doc.master_data && !frm.doc.is_master_data_imported) {
if (frm.doc.is_master_data_processed) {
if (frm.doc.status != "Importing Master Data") {
frm.events.add_button(frm, __("Import Master Data"), "import_master_data");
}
} else {
if (frm.doc.status != "Processing Master Data") {
frm.events.add_button(frm, __("Process Master Data"), "process_master_data");
}
}
}
if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) {
if (frm.doc.is_day_book_data_processed) {
if (frm.doc.status != "Importing Day Book Data") {
frm.events.add_button(frm, __("Import Day Book Data"), "import_day_book_data");
}
} else {
if (frm.doc.status != "Processing Day Book Data") {
frm.events.add_button(frm, __("Process Day Book Data"), "process_day_book_data");
}
}
}
},
erpnext_company: function (frm) {
frappe.db.exists("Company", frm.doc.erpnext_company).then((exists) => {
if (exists) {
frappe.msgprint(
__(
"Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts",
[frm.doc.erpnext_company]
)
);
}
});
},
add_button: function (frm, label, method) {
frm.add_custom_button(label, () => {
frm.call({
doc: frm.doc,
method: method,
freeze: true,
});
frm.reload_doc();
});
},
render_html_table(frm, shown_logs, hidden_logs, field) {
if (shown_logs && shown_logs.length > 0) {
frm.toggle_display(field, true);
} else {
frm.toggle_display(field, false);
return;
}
let rows = erpnext.tally_migration.get_html_rows(shown_logs, field);
let rows_head, table_caption;
let table_footer =
hidden_logs && hidden_logs.length > 0
? `<tr class="text-muted">
<td colspan="4">And ${hidden_logs.length} more others</td>
</tr>`
: "";
if (field === "fixed_error_log_preview") {
rows_head = `<th width="75%">${__("Meta Data")}</th>
<th width="10%">${__("Unresolve")}</th>`;
table_caption = "Resolved Issues";
} else {
rows_head = `<th width="75%">${__("Error Message")}</th>
<th width="10%">${__("Create")}</th>`;
table_caption = "Error Log";
}
frm.get_field(field).$wrapper.html(`
<table class="table table-bordered">
<caption>${table_caption}</caption>
<tr class="text-muted">
<th width="5%">${__("#")}</th>
<th width="10%">${__("DocType")}</th>
${rows_head}
</tr>
${rows}
${table_footer}
</table>
`);
},
show_error_summary(frm) {
let summary = erpnext.tally_migration.failed_import_log.reduce((summary, row) => {
if (row.doc) {
if (summary[row.doc.doctype]) {
summary[row.doc.doctype] += 1;
} else {
summary[row.doc.doctype] = 1;
}
}
return summary;
}, {});
console.table(summary);
},
show_logs_preview(frm) {
let empty = "[]";
let import_log = frm.doc.failed_import_log || empty;
let completed_log = frm.doc.fixed_errors_log || empty;
let render_section = !(import_log === completed_log && import_log === empty);
frm.toggle_display("import_log_section", render_section);
if (render_section) {
frm.trigger("show_error_summary");
frm.trigger("show_errored_import_log");
frm.trigger("show_fixed_errors_log");
}
},
show_errored_import_log(frm) {
let import_log = erpnext.tally_migration.failed_import_log;
let logs = import_log.slice(0, 20);
let hidden_logs = import_log.slice(20);
frm.events.render_html_table(frm, logs, hidden_logs, "failed_import_preview");
},
show_fixed_errors_log(frm) {
let completed_log = erpnext.tally_migration.fixed_errors_log;
let logs = completed_log.slice(0, 20);
let hidden_logs = completed_log.slice(20);
frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview");
},
});
erpnext.tally_migration.getError = (traceback) => {
/* Extracts the Error Message from the Python Traceback or Solved error */
let is_multiline = traceback.trim().indexOf("\n") != -1;
let message;
if (is_multiline) {
let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1;
let error_line = traceback.substr(exc_error_idx);
let split_str_idx = error_line.indexOf(":") > 0 ? error_line.indexOf(":") + 1 : 0;
message = error_line.slice(split_str_idx).trim();
} else {
message = traceback;
}
return message;
};
erpnext.tally_migration.cleanDoc = (obj) => {
/* Strips all null and empty values of your JSON object */
let temp = obj;
$.each(temp, function (key, value) {
if (value === "" || value === null) {
delete obj[key];
} else if (Object.prototype.toString.call(value) === "[object Object]") {
erpnext.tally_migration.cleanDoc(value);
} else if ($.isArray(value)) {
$.each(value, function (k, v) {
erpnext.tally_migration.cleanDoc(v);
});
}
});
return temp;
};
erpnext.tally_migration.unresolve = (document) => {
/* Mark document migration as unresolved ie. move to failed error log */
let frm = cur_frm;
let failed_log = erpnext.tally_migration.failed_import_log;
let fixed_log = erpnext.tally_migration.fixed_errors_log;
let modified_fixed_log = fixed_log.filter((row) => {
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
return row;
}
});
failed_log.push({ doc: document, exc: `Marked unresolved on ${Date()}` });
frm.doc.failed_import_log = JSON.stringify(failed_log);
frm.doc.fixed_errors_log = JSON.stringify(modified_fixed_log);
frm.dirty();
frm.save();
};
erpnext.tally_migration.resolve = (document) => {
/* Mark document migration as resolved ie. move to fixed error log */
let frm = cur_frm;
let failed_log = erpnext.tally_migration.failed_import_log;
let fixed_log = erpnext.tally_migration.fixed_errors_log;
let modified_failed_log = failed_log.filter((row) => {
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
return row;
}
});
fixed_log.push({ doc: document, exc: `Solved on ${Date()}` });
frm.doc.failed_import_log = JSON.stringify(modified_failed_log);
frm.doc.fixed_errors_log = JSON.stringify(fixed_log);
frm.dirty();
frm.save();
};
erpnext.tally_migration.create_new_doc = (document) => {
/* Mark as resolved and create new document */
erpnext.tally_migration.resolve(document);
return frappe.call({
type: "POST",
method: "erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc",
args: {
document,
},
freeze: true,
callback: function (r) {
if (!r.exc) {
frappe.model.sync(r.message);
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true;
frappe.set_route("Form", r.message.doctype, r.message.name);
}
},
});
};
erpnext.tally_migration.get_html_rows = (logs, field) => {
let index = 0;
let rows = logs
.map(({ doc, exc }) => {
let id = frappe.dom.get_unique_id();
let traceback = exc;
let error_message = erpnext.tally_migration.getError(traceback);
index++;
let show_traceback = `
<button class="btn btn-default btn-xs m-3" type="button" data-toggle="collapse" data-target="#${id}-traceback" aria-expanded="false" aria-controls="${id}-traceback">
${__("Show Traceback")}
</button>
<div class="collapse margin-top" id="${id}-traceback">
<div class="well">
<pre style="font-size: smaller;">${traceback}</pre>
</div>
</div>`;
let show_doc = `
<button class='btn btn-default btn-xs m-3' type='button' data-toggle='collapse' data-target='#${id}-doc' aria-expanded='false' aria-controls='${id}-doc'>
${__("Show Document")}
</button>
<div class="collapse margin-top" id="${id}-doc">
<div class="well">
<pre style="font-size: smaller;">${JSON.stringify(erpnext.tally_migration.cleanDoc(doc), null, 1)}</pre>
</div>
</div>`;
let create_button = `
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.create_new_doc(${JSON.stringify(
doc
)})'>
${__("Create Document")}
</button>`;
let mark_as_unresolved = `
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.unresolve(${JSON.stringify(
doc
)})'>
${__("Mark as unresolved")}
</button>`;
if (field === "fixed_error_log_preview") {
return `<tr>
<td>${index}</td>
<td>
<div>${doc.doctype}</div>
</td>
<td>
<div>${error_message}</div>
<div>${show_doc}</div>
</td>
<td>
<div>${mark_as_unresolved}</div>
</td>
</tr>`;
} else {
return `<tr>
<td>${index}</td>
<td>
<div>${doc.doctype}</div>
</td>
<td>
<div>${error_message}</div>
<div>${show_traceback}</div>
<div>${show_doc}</div>
</td>
<td>
<div>${create_button}</div>
</td>
</tr>`;
}
})
.join("");
return rows;
};

View File

@@ -1,280 +0,0 @@
{
"actions": [],
"beta": 1,
"creation": "2019-02-01 14:27:09.485238",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"status",
"master_data",
"is_master_data_processed",
"is_master_data_imported",
"column_break_2",
"tally_creditors_account",
"tally_debtors_account",
"company_section",
"tally_company",
"default_uom",
"column_break_8",
"erpnext_company",
"processed_files_section",
"chart_of_accounts",
"parties",
"addresses",
"column_break_17",
"uoms",
"items",
"vouchers",
"accounts_section",
"default_warehouse",
"default_round_off_account",
"column_break_21",
"default_cost_center",
"day_book_section",
"day_book_data",
"column_break_27",
"is_day_book_data_processed",
"is_day_book_data_imported",
"import_log_section",
"failed_import_log",
"fixed_errors_log",
"failed_import_preview",
"fixed_error_log_preview"
],
"fields": [
{
"fieldname": "status",
"fieldtype": "Data",
"hidden": 1,
"label": "Status"
},
{
"description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs",
"fieldname": "master_data",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Master Data"
},
{
"default": "Sundry Creditors",
"description": "Creditors Account set in Tally",
"fieldname": "tally_creditors_account",
"fieldtype": "Data",
"label": "Tally Creditors Account",
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "Sundry Debtors",
"description": "Debtors Account set in Tally",
"fieldname": "tally_debtors_account",
"fieldtype": "Data",
"label": "Tally Debtors Account",
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
"reqd": 1
},
{
"depends_on": "is_master_data_processed",
"fieldname": "company_section",
"fieldtype": "Section Break"
},
{
"description": "Company Name as per Imported Tally Data",
"fieldname": "tally_company",
"fieldtype": "Data",
"label": "Tally Company",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"description": "Your Company set in ERPNext",
"fieldname": "erpnext_company",
"fieldtype": "Data",
"label": "ERPNext Company",
"read_only_depends_on": "eval:doc.is_master_data_processed==1"
},
{
"fieldname": "processed_files_section",
"fieldtype": "Section Break",
"hidden": 1,
"label": "Processed Files"
},
{
"fieldname": "chart_of_accounts",
"fieldtype": "Attach",
"label": "Chart of Accounts"
},
{
"fieldname": "parties",
"fieldtype": "Attach",
"label": "Parties"
},
{
"fieldname": "addresses",
"fieldtype": "Attach",
"label": "Addresses"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "uoms",
"fieldtype": "Attach",
"label": "UOMs"
},
{
"fieldname": "items",
"fieldtype": "Attach",
"label": "Items"
},
{
"fieldname": "vouchers",
"fieldtype": "Attach",
"label": "Vouchers"
},
{
"depends_on": "is_master_data_imported",
"description": "The accounts are set by the system automatically but do confirm these defaults",
"fieldname": "accounts_section",
"fieldtype": "Section Break",
"label": "Accounts"
},
{
"fieldname": "default_warehouse",
"fieldtype": "Link",
"label": "Default Warehouse",
"options": "Warehouse"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "default_cost_center",
"fieldtype": "Link",
"label": "Default Cost Center",
"options": "Cost Center"
},
{
"default": "0",
"fieldname": "is_master_data_processed",
"fieldtype": "Check",
"label": "Is Master Data Processed",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_day_book_data_processed",
"fieldtype": "Check",
"label": "Is Day Book Data Processed",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_day_book_data_imported",
"fieldtype": "Check",
"label": "Is Day Book Data Imported",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_master_data_imported",
"fieldtype": "Check",
"label": "Is Master Data Imported",
"read_only": 1
},
{
"depends_on": "is_master_data_imported",
"fieldname": "day_book_section",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"description": "Day Book Data exported from Tally that consists of all historic transactions",
"fieldname": "day_book_data",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Day Book Data"
},
{
"default": "Unit",
"description": "UOM in case unspecified in imported data",
"fieldname": "default_uom",
"fieldtype": "Link",
"label": "Default UOM",
"options": "UOM",
"read_only_depends_on": "eval:doc.is_master_data_imported==1"
},
{
"default": "[]",
"fieldname": "failed_import_log",
"fieldtype": "Code",
"hidden": 1,
"options": "JSON"
},
{
"fieldname": "failed_import_preview",
"fieldtype": "HTML",
"label": "Failed Import Log"
},
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
"label": "Import Log"
},
{
"fieldname": "default_round_off_account",
"fieldtype": "Link",
"label": "Default Round Off Account",
"options": "Account"
},
{
"default": "[]",
"fieldname": "fixed_errors_log",
"fieldtype": "Code",
"hidden": 1,
"options": "JSON"
},
{
"fieldname": "fixed_error_log_preview",
"fieldtype": "HTML",
"label": "Fixed Error Log"
}
],
"links": [],
"modified": "2024-03-27 13:10:51.146772",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Tally Migration",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

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