Compare commits

..

616 Commits

Author SHA1 Message Date
Frappe PR Bot
3967773fbe chore(release): Bumped to Version 14.5.1
## [14.5.1](https://github.com/frappe/erpnext/compare/v14.5.0...v14.5.1) (2022-11-03)

### Bug Fixes

* not able to select customer / supplier ([dd4dbd4](dd4dbd4b00))
2022-11-03 08:07:36 +00:00
Deepesh Garg
074c75252a Merge pull request #32835 from frappe/mergify/bp/version-14/pr-32833
fix: not able to select customer / supplier (backport #32832) (backport #32833)
2022-11-03 13:35:52 +05:30
Rohit Waghchaure
dd4dbd4b00 fix: not able to select customer / supplier
(cherry picked from commit b0fc568c80)
(cherry picked from commit 6989cdf4f2)
2022-11-03 06:53:41 +00:00
Frappe PR Bot
ce5dbf891a chore(release): Bumped to Version 14.5.0
# [14.5.0](https://github.com/frappe/erpnext/compare/v14.4.0...v14.5.0) (2022-11-01)

### Bug Fixes

* add `Sales Order` reference in Material Request Dashboard ([fc63892](fc6389280c))
* Add condition for discount section collapse ([953f78d](953f78d6a9))
* Budget validation for main cost center ([89a1c83](89a1c83431))
* Clear invoice table post importing invoices ([6eafff8](6eafff8694))
* Company bank account filter in Bank Clearance ([797512c](797512ca13))
* Curreny in SOA print for multi-currency party ([195500c](195500cb32))
* duplicate custom fields for inventory dimension ([1152ac3](1152ac3ff1))
* Filter fixes in Accounts Payable report ([29197dc](29197dcd7f))
* Issues while cancel/amending Purchase Invoice with TDS enabled ([74a6479](74a6479f70))
* key error in filter access ([6114241](6114241ff2))
* Mode of payment for returns in POS Sales Invoice ([a260426](a260426dd4))
* Pass project to stock entry items ([4035873](4035873295))
* pro_rata_amount calculation in assets tests ([d1b2786](d1b2786f24))
* Reference due date field type in Journal Entry Accounts table ([faf25c0](faf25c0b95))
* Reset advance paid amount on Oreder cancel and amend ([34bd783](34bd7837e2))
* Total Sales amount update in project via Sales Order ([d742e6d](d742e6d56b))

### Features

* additional filters on Payment terms report ([a03ec0a](a03ec0afb3))
* **pricing rule:** free qty rounding and recursion qty ([#32577](https://github.com/frappe/erpnext/issues/32577)) ([9b66020](9b66020fc7))
2022-11-01 17:19:41 +00:00
Deepesh Garg
fd36ee0fda Merge pull request #32793 from frappe/version-14-hotfix
chore: release v14
2022-11-01 22:48:17 +05:30
Deepesh Garg
d68fa0eff2 Merge pull request #32807 from frappe/mergify/bp/version-14-hotfix/pr-32779
fix: Mode of payment for returns in POS Sales Invoice (backport #32779)
2022-11-01 22:12:11 +05:30
Deepesh Garg
3f72156ea9 chore: Update tests
(cherry picked from commit 5b74161195)
2022-11-01 16:00:18 +00:00
Deepesh Garg
a260426dd4 fix: Mode of payment for returns in POS Sales Invoice
(cherry picked from commit 06e8e28531)
2022-11-01 16:00:18 +00:00
Deepesh Garg
148dc37d92 Merge pull request #32804 from frappe/mergify/bp/version-14-hotfix/pr-32801
fix: Issues while cancel/amending Purchase Invoice with TDS enabled (backport #32801)
2022-11-01 21:29:17 +05:30
Deepesh Garg
74a6479f70 fix: Issues while cancel/amending Purchase Invoice with TDS enabled
(cherry picked from commit f7c9258770)
2022-11-01 15:19:10 +00:00
ruthra kumar
f2f1c160f8 Merge pull request #32797 from frappe/mergify/bp/version-14-hotfix/pr-32577
feat(pricing rule): free qty rounding and recursion qty (backport #32577)
2022-11-01 17:09:46 +05:30
Dany Robert
9b66020fc7 feat(pricing rule): free qty rounding and recursion qty (#32577)
Option to specify recursion start qty and repeating qty

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit 1d83fb20d6)
2022-11-01 11:12:44 +00:00
Deepesh Garg
6af43ba40d Merge pull request #32795 from frappe/mergify/bp/version-14-hotfix/pr-32790
fix: pro_rata_amount calculation in assets tests (backport #32790)
2022-11-01 16:19:49 +05:30
Anand Baburajan
bd531eb898 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32790 2022-11-01 15:21:29 +05:30
Deepesh Garg
15f4812108 Merge pull request #32782 from frappe/mergify/bp/version-14-hotfix/pr-32768
fix: Budget validation for main cost center (backport #32768)
2022-11-01 15:21:16 +05:30
anandbaburajan
4c5b688e0d chore: empty commit to try fixing stuck test
(cherry picked from commit 672fbd3849)
2022-11-01 09:48:04 +00:00
anandbaburajan
d1b2786f24 fix: pro_rata_amount calculation in assets tests
(cherry picked from commit 65e855bfff)
2022-11-01 09:48:04 +00:00
rohitwaghchaure
2eb01aae27 Merge pull request #32783 from frappe/mergify/bp/version-14-hotfix/pr-32730
fix: duplicate custom fields for inventory dimension (backport #32730)
2022-10-31 23:06:29 +05:30
Rohit Waghchaure
1152ac3ff1 fix: duplicate custom fields for inventory dimension
(cherry picked from commit 45ededbed5)
2022-10-31 16:56:17 +00:00
Deepesh Garg
89a1c83431 fix: Budget validation for main cost center
(cherry picked from commit 4e26d42d17)
2022-10-31 16:49:31 +00:00
Deepesh Garg
2a1a61238a Merge pull request #32780 from frappe/mergify/bp/version-14-hotfix/pr-32777
fix: Reset advance paid amount on Order cancel and amend (backport #32777)
2022-10-31 22:07:54 +05:30
Deepesh Garg
34bd7837e2 fix: Reset advance paid amount on Oreder cancel and amend
(cherry picked from commit 92f37ca111)
2022-10-31 16:09:27 +00:00
Deepesh Garg
f66a6693f4 Merge pull request #32761 from frappe/mergify/bp/version-14-hotfix/pr-32759
Pass project to stock entry items (backport #32759)
2022-10-30 09:29:03 +05:30
Hossein Yousefian
4035873295 fix: Pass project to stock entry items
fix: Pass project to stock entry items
(cherry picked from commit 54c2ffc36b)
2022-10-29 16:55:25 +00:00
Deepesh Garg
4a86efedfc Merge pull request #32752 from frappe/mergify/bp/version-14-hotfix/pr-32744
refactor: additional filters on Payment Terms status report (backport #32744)
2022-10-29 17:12:17 +05:30
Sagar Sharma
b7cbc804b2 Merge pull request #32756 from frappe/mergify/bp/version-14-hotfix/pr-32754
fix: add `Sales Order` reference in Material Request Dashboard (backport #32754)
2022-10-29 13:49:16 +05:30
Sagar Sharma
fc6389280c fix: add Sales Order reference in Material Request Dashboard
(cherry picked from commit 15ebf4a0cf)
2022-10-29 07:07:25 +00:00
ruthra kumar
6dbb4a3b0d test: due date filter on Payment Terms report
(cherry picked from commit fed39a53cb)
2022-10-29 05:40:16 +00:00
ruthra kumar
6114241ff2 fix: key error in filter access
(cherry picked from commit 4765f937ea)
2022-10-29 05:40:16 +00:00
ruthra kumar
a03ec0afb3 feat: additional filters on Payment terms report
Filter on Status and Due dates

(cherry picked from commit aadb6b1772)
2022-10-29 05:40:15 +00:00
Deepesh Garg
db5d74b7dc Merge pull request #32748 from frappe/mergify/bp/version-14-hotfix/pr-32722
fix: Reference due date field type in Journal Entry Accounts table (backport #32722)
2022-10-29 11:00:01 +05:30
Deepesh Garg
ad6e0f3af7 Merge pull request #32746 from frappe/mergify/bp/version-14-hotfix/pr-32699
fix: Curreny in SOA print for multi-currency party (backport #32699)
2022-10-29 10:59:49 +05:30
Deepesh Garg
abcf30aaa3 Merge pull request #32745 from frappe/mergify/bp/version-14-hotfix/pr-32692
fix: Clear invoice table post importing invoices (backport #32692)
2022-10-29 10:59:35 +05:30
Deepesh Garg
cc9c876007 Merge pull request #32747 from frappe/mergify/bp/version-14-hotfix/pr-32718
fix: Total Sales amount update in project via Sales Order (backport #32718)
2022-10-29 10:59:10 +05:30
Sagar Sharma
199e77faef Merge pull request #32750 from frappe/mergify/bp/version-14-hotfix/pr-32662
refactor: rewrite stock reports queries in qb (backport #32662)
2022-10-28 22:48:03 +05:30
Sagar Sharma
98428f0bce refactor: rewrite Itemwise Recommended Reorder Level Report queries in QB
(cherry picked from commit 40bd121593)
2022-10-28 15:36:01 +00:00
Sagar Sharma
a4b99a34cb refactor: rewrite Product Bundle Balance Report queries in QB
(cherry picked from commit cde785f1bb)
2022-10-28 15:36:01 +00:00
Sagar Sharma
c4587d8caa refactor: rewrite Stock Ledger Report queries in QB
(cherry picked from commit feaa2dbba8)
2022-10-28 15:36:01 +00:00
Deepesh Garg
faf25c0b95 fix: Reference due date field type in Journal Entry Accounts table
(cherry picked from commit e7caa48e2f)
2022-10-28 15:26:29 +00:00
Deepesh Garg
d742e6d56b fix: Total Sales amount update in project via Sales Order
(cherry picked from commit 6063c4e3c0)
2022-10-28 15:16:42 +00:00
Deepesh Garg
3f0b03c0a4 chore: Use account currency as fallback
(cherry picked from commit a18a715bb4)
2022-10-28 15:12:34 +00:00
Deepesh Garg
195500cb32 fix: Curreny in SOA print for multi-currency party
(cherry picked from commit 49ee873655)
2022-10-28 15:12:34 +00:00
Deepesh Garg
6eafff8694 fix: Clear invoice table post importing invoices
(cherry picked from commit 267e7c3a90)
2022-10-28 15:11:41 +00:00
Deepesh Garg
36f2b65401 Merge pull request #32739 from frappe/mergify/bp/version-14-hotfix/pr-32716
fix: Company bank account filter in Bank Clearance (backport #32716)
2022-10-28 17:19:49 +05:30
Sagar Sharma
f082c9e797 Merge pull request #32740 from frappe/mergify/bp/version-14-hotfix/pr-32738
fix: Added Material Request Reference in Purchase Recipt Dashboard for Tracking (backport #32738)
2022-10-28 16:06:44 +05:30
Vishal
0ab69c0e32 chore: minor linting issue fixed
(cherry picked from commit e8c0157017)
2022-10-28 08:54:44 +00:00
Vishal
de3996e411 chore: Added Material Request Reference in Purchase Recipt Dashboard for Tracking
(cherry picked from commit a04c44fe34)
2022-10-28 08:54:44 +00:00
Deepesh Garg
797512ca13 fix: Company bank account filter in Bank Clearance
(cherry picked from commit f9f78c1086)
2022-10-28 07:24:35 +00:00
Deepesh Garg
d39ec5ef5f Merge pull request #32737 from frappe/mergify/bp/version-14-hotfix/pr-32717
fix: Add condition for discount section collapse (backport #32717)
2022-10-28 11:55:37 +05:30
Deepesh Garg
2dae93c0a1 Merge pull request #32736 from frappe/mergify/bp/version-14-hotfix/pr-32724
fix: Filter fixes in Accounts Payable report (backport #32724)
2022-10-28 11:55:24 +05:30
Deepesh Garg
953f78d6a9 fix: Add condition for discount section collapse
(cherry picked from commit 4cd65027c4)
2022-10-28 05:55:06 +00:00
Deepesh Garg
29197dcd7f fix: Filter fixes in Accounts Payable report
(cherry picked from commit a5a73ba857)
2022-10-28 05:54:25 +00:00
Frappe PR Bot
123d720616 chore(release): Bumped to Version 14.4.0
# [14.4.0](https://github.com/frappe/erpnext/compare/v14.3.1...v14.4.0) (2022-10-26)

### Bug Fixes

* Advance paid amount in orders (backport [#32642](https://github.com/frappe/erpnext/issues/32642)) ([#32648](https://github.com/frappe/erpnext/issues/32648)) ([8a88105](8a88105aed))
* allow to create Sales Order from expired Quotation ([#32641](https://github.com/frappe/erpnext/issues/32641)) ([ccc58f4](ccc58f48e3))
* Billing Address for inter-company purchase docs ([f8934fa](f8934faa73))
* BOM cost update message ([e539579](e539579fb4))
* dont update item info twice ([8876904](887690449d))
* incorrect qty in material request ([da538a3](da538a37ad))
* number of months subscription plan ([fff9e76](fff9e76718))
* overlap error not raised for job card in case of workstation with production capacity ([ed2a093](ed2a093e49))
* party type and party mandatory on updating outstanding ([9a5e238](9a5e238702))
* searchfield not working for cuctsomer, supplier as per customize form ([fb1c307](fb1c30718b))
* unset contact details ([d7a65b1](d7a65b1d41))

### Features

* Basic Payment Ledger report ([5cb9f7b](5cb9f7b3a4))
* Repayment schedule types for term loans ([6ce32fd](6ce32fd5a9))

### Performance Improvements

* cache barcode scan result (backport [#32629](https://github.com/frappe/erpnext/issues/32629)) ([#32672](https://github.com/frappe/erpnext/issues/32672)) ([3300856](3300856fb4))
2022-10-26 05:09:19 +00:00
Deepesh Garg
1c95c67f58 Merge pull request #32707 from frappe/version-14-hotfix
chore: release v14
2022-10-26 10:37:46 +05:30
rohitwaghchaure
753b2edebf Merge pull request #32698 from frappe/mergify/bp/version-14-hotfix/pr-32693
fix: Search field not working for customer, supplier (backport #32693)
2022-10-24 23:14:37 +05:30
Rohit Waghchaure
f50b485225 test: added test case to validate seachfields for customer, supplier
(cherry picked from commit 5f84993bae)
2022-10-24 11:42:16 +00:00
Rohit Waghchaure
fb1c30718b fix: searchfield not working for cuctsomer, supplier as per customize form
(cherry picked from commit 46d148defd)
2022-10-24 11:42:15 +00:00
Deepesh Garg
2705de084f Merge pull request #32689 from frappe/mergify/bp/version-14-hotfix/pr-32424
feat: Repayment schedule types for term loans (backport #32424)
2022-10-23 23:06:58 +05:30
Deepesh Garg
a0ace27f56 chore: resolve conflicts 2022-10-23 19:43:02 +05:30
Deepesh Garg
2348c42c79 chore: Update tests
(cherry picked from commit e59b147a62)
2022-10-23 14:08:36 +00:00
Deepesh Garg
1b3a0be0f1 chore: Add repayment date on option
(cherry picked from commit ef0cb17faf)
2022-10-23 14:08:35 +00:00
Deepesh Garg
b9bf5666bc chore: label post save
(cherry picked from commit bf7a51791a)
2022-10-23 14:08:34 +00:00
Deepesh Garg
25e87641b6 chore: Add patch to update repayment schedule type in loan documents
(cherry picked from commit 679b5ed551)

# Conflicts:
#	erpnext/patches.txt
2022-10-23 14:08:34 +00:00
Deepesh Garg
8550bbde27 chore: Update labels as per repayment type
(cherry picked from commit 2ddee50f27)
2022-10-23 14:08:32 +00:00
Deepesh Garg
7398cbdf12 chore: Remove print statements
(cherry picked from commit 3466461eb3)
2022-10-23 14:08:32 +00:00
Deepesh Garg
6ce32fd5a9 feat: Repayment schedule types for term loans
(cherry picked from commit 76c6ccab5d)
2022-10-23 14:08:32 +00:00
Deepesh Garg
18904456f3 Merge pull request #32686 from frappe/mergify/bp/version-14-hotfix/pr-32650
fix: unset contact details (backport #32650)
2022-10-23 18:32:04 +05:30
barredterra
d7a65b1d41 fix: unset contact details
(cherry picked from commit 23f0bb45b0)
2022-10-23 12:51:49 +00:00
ruthra kumar
eb6db59adf Merge pull request #32679 from frappe/mergify/bp/version-14-hotfix/pr-32635
fix: Integrity Error on PLE while submitting sales invoice (backport #32635)
2022-10-21 12:05:46 +05:30
ruthra kumar
fb2b2edf7e Merge pull request #32678 from frappe/mergify/bp/version-14-hotfix/pr-32651
feat: Payment Ledger report (backport #32651)
2022-10-21 11:56:09 +05:30
ruthra kumar
c2f8f64d1e test: use payable account in tax and to trigger party validation
(cherry picked from commit 8f42e7f703)
2022-10-21 06:07:09 +00:00
ruthra kumar
9a5e238702 fix: party type and party mandatory on updating outstanding
(cherry picked from commit 43b80683eb)
2022-10-21 06:07:09 +00:00
ruthra kumar
77f4c666ee test: invoice outstandings and payments
(cherry picked from commit 6e55b419a6)
2022-10-21 05:56:17 +00:00
ruthra kumar
5cb9f7b3a4 feat: Basic Payment Ledger report
(cherry picked from commit 8f60f0a0cf)
2022-10-21 05:56:17 +00:00
Ankush Menat
e1fc67c730 chore: disable coverage reporting on stable branches 2022-10-20 19:15:50 +05:30
Deepesh Garg
99d410ab75 Merge pull request #32673 from frappe/mergify/bp/version-14-hotfix/pr-32551
fix: number of months subscription plan (backport #32551)
2022-10-20 19:06:54 +05:30
Deepesh Garg
54de569251 Merge pull request #32655 from frappe/mergify/bp/version-14-hotfix/pr-32641
fix: allow to create Sales Order from expired Quotation (backport #32641)
2022-10-20 17:34:04 +05:30
Deepesh Garg
cab3f39f95 chore: Linting issues
(cherry picked from commit 1ca472cc8a)
2022-10-20 11:55:04 +00:00
codezart
fff9e76718 fix: number of months subscription plan
(cherry picked from commit 2d30b36cca)
2022-10-20 11:55:04 +00:00
Deepesh Garg
dbbebd2a73 Merge pull request #32663 from frappe/mergify/bp/version-14-hotfix/pr-32659
fix: Billing Address for inter-company purchase docs (backport #32659)
2022-10-20 17:21:24 +05:30
rohitwaghchaure
9bf101a234 Merge pull request #32669 from frappe/mergify/bp/version-14-hotfix/pr-32667
fix: BOM cost update message (backport #32667)
2022-10-20 16:53:09 +05:30
Ankush Menat
887690449d fix: dont update item info twice
[skip ci]
2022-10-20 16:19:40 +05:30
mergify[bot]
3300856fb4 perf: cache barcode scan result (backport #32629) (#32672)
perf: cache barcode scan result (#32629)

* perf: cache barcode scan result

* feat: BarcodeScanResult type

* fix: use safe `get_value` `set_value`

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
(cherry picked from commit b88e850d55)

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
2022-10-20 15:57:21 +05:30
Rohit Waghchaure
e539579fb4 fix: BOM cost update message
(cherry picked from commit 9cfe527492)
2022-10-20 09:08:31 +00:00
Deepesh Garg
f8934faa73 fix: Billing Address for inter-company purchase docs
(cherry picked from commit 796f2d3c09)
2022-10-20 06:29:32 +00:00
rohitwaghchaure
2c9845f8cb Merge pull request #32660 from frappe/mergify/bp/version-14-hotfix/pr-32654
fix: incorrect qty in material request created from PP (backport #32654)
2022-10-20 11:58:57 +05:30
Rohit Waghchaure
92b9d3dc6d test: validate qty and purchase uom in material request which is created from PP
(cherry picked from commit 4d5ef721f7)
2022-10-20 04:30:08 +00:00
Rohit Waghchaure
da538a37ad fix: incorrect qty in material request
(cherry picked from commit ad278b2007)
2022-10-20 04:30:08 +00:00
rohitwaghchaure
d6bf85f8bb Merge pull request #32652 from frappe/mergify/bp/version-14-hotfix/pr-32645
fix: overlap error not raised for job card in case of workstation with production capacity (backport #32645)
2022-10-20 09:58:25 +05:30
mergify[bot]
c3024af296 feat(report):added account wise redirection (backport #32529) (#32657)
feat(report): added account wise redirection

* feat(report):added account wise redirection

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit c52b41d311)

Co-authored-by: FinByz Tech Pvt. Ltd <info@finbyz.tech>
2022-10-20 09:39:11 +05:30
Raffael Meyer
ccc58f48e3 fix: allow to create Sales Order from expired Quotation (#32641)
(cherry picked from commit 4ad3002861)
2022-10-19 16:37:53 +00:00
Rohit Waghchaure
ed2a093e49 fix: overlap error not raised for job card in case of workstation with production capacity
(cherry picked from commit 8b2165e0d1)
2022-10-19 11:35:38 +00:00
mergify[bot]
8a88105aed fix: Advance paid amount in orders (backport #32642) (#32648)
fix: Advance paid amount in orders (#32642)

(cherry picked from commit 430492152f)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2022-10-19 15:41:57 +05:30
Frappe PR Bot
9703771b75 chore(release): Bumped to Version 14.3.1
## [14.3.1](https://github.com/frappe/erpnext/compare/v14.3.0...v14.3.1) (2022-10-18)

### Bug Fixes

* `Brand Defaults` filters ([cb0c4b5](cb0c4b5664))
* add project settings to projects workspace (backport [#32568](https://github.com/frappe/erpnext/issues/32568)) ([#32600](https://github.com/frappe/erpnext/issues/32600)) ([af4dafd](af4dafdc64))
* delete old ple's on item value repost ([62cabdf](62cabdf792))
* don't try to update youtube data if disabled in settings (backport [#32588](https://github.com/frappe/erpnext/issues/32588)) ([#32589](https://github.com/frappe/erpnext/issues/32589)) ([ed85683](ed85683fea))
* group warehouse filter not working for Batchwise Balance history report ([faedd85](faedd85b66))
* Ignore linked purchase invoice on cancel ([cc938fb](cc938fb028))
* linter ([831f60f](831f60f439))
* Party account for multi-order invoices ([eaea846](eaea8469fc))
* pricing rule item code UOM apply & conversions (backport [#32566](https://github.com/frappe/erpnext/issues/32566)) ([#32637](https://github.com/frappe/erpnext/issues/32637)) ([ffd82f3](ffd82f3302))
* Renamed Dashboard tab label to Connections ([dac5989](dac5989d72))
* Renamed Notes section to Comments ([9e94dd9](9e94dd9203))
* type-cast while saving an item (backport [#32549](https://github.com/frappe/erpnext/issues/32549)) ([#32578](https://github.com/frappe/erpnext/issues/32578)) ([829a0ff](829a0ff827))
2022-10-18 18:03:17 +00:00
Deepesh Garg
bd62a1d2d3 Merge pull request #32633 from frappe/version-14-hotfix
chore: release v14
2022-10-18 23:31:52 +05:30
mergify[bot]
ffd82f3302 fix: pricing rule item code UOM apply & conversions (backport #32566) (#32637)
fix: pricing rule for non stock UOM and conversions

* fix: pricing rule for non stock UOM and conversions

(cherry picked from commit 96b4211ea1)

Co-authored-by: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com>
2022-10-18 23:01:08 +05:30
Deepesh Garg
8c658638b4 Merge pull request #32621 from frappe/mergify/bp/version-14-hotfix/pr-32598
refactor: split ple creation function into two (backport #32598)
2022-10-18 15:11:51 +05:30
Sagar Sharma
064e364b4f Merge pull request #32632 from frappe/mergify/bp/version-14-hotfix/pr-32520
refactor: rewrite stock reports queries in qb (backport #32520)
2022-10-18 14:59:18 +05:30
Sagar Sharma
831f60f439 fix: linter 2022-10-18 14:27:56 +05:30
Sagar Sharma
469813d3e1 chore: conflicts 2022-10-18 14:18:44 +05:30
Sagar Sharma
656008dd66 refactor: rewrite Warehouse wise Item Balance Age and Value Report queries in QB
(cherry picked from commit 8103856a41)
2022-10-18 08:22:56 +00:00
Sagar Sharma
362bf5fc19 refactor: rewrite Total Stock Summary Report queries in QB
(cherry picked from commit 7c759b193c)
2022-10-18 08:22:56 +00:00
Sagar Sharma
8af90b2421 refactor: rewrite Supplier-Wise Sales Analytics Report queries in QB
(cherry picked from commit d3c073dc25)
2022-10-18 08:22:55 +00:00
Sagar Sharma
6903fb68d3 refactor: rewrite Stock Projected Qty Report queries in QB
(cherry picked from commit c18f13a45b)
2022-10-18 08:22:55 +00:00
Sagar Sharma
907a41a8da refactor: rewrite Item Prices Report queries in QB
(cherry picked from commit e312d17eae)
2022-10-18 08:22:55 +00:00
Sagar Sharma
e62fdf4af0 refactor: rewrite Delayed Item Report queries in QB
(cherry picked from commit bb59346651)
2022-10-18 08:22:55 +00:00
Sagar Sharma
720370537f refactor: rewrite Batch-Wise Balance History Report queries in QB
(cherry picked from commit 1c1f991d2f)

# Conflicts:
#	erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py
2022-10-18 08:22:54 +00:00
Sagar Sharma
5a85c2d661 refactor: rewrite Batch Item Expiry Status Report queries in QB
(cherry picked from commit 2c9d9577e3)
2022-10-18 08:22:54 +00:00
Sagar Sharma
64e9a25347 Merge pull request #32630 from frappe/mergify/bp/version-14-hotfix/pr-32527
refactor: rewrite buying reports queries in qb (backport #32527)
2022-10-18 10:47:47 +05:30
Sagar Sharma
2191f8a488 refactor: rewrite Procurement Tracker Report queries in QB
(cherry picked from commit e78a706994)
2022-10-18 04:37:03 +00:00
Sagar Sharma
1fbbaa8fcf refactor: rewrite Supplier Quotation Comparison Report queries in QB
(cherry picked from commit a14b9c7bac)
2022-10-18 04:37:02 +00:00
Sagar Sharma
fbb641936e refactor: rewrite Purchase Order Analysis Report queries in QB
(cherry picked from commit a5b3f8cae9)
2022-10-18 04:37:02 +00:00
Deepesh Garg
891a4afb5f Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32598 2022-10-18 10:00:46 +05:30
Deepesh Garg
01c5fbdbfb Merge pull request #32623 from frappe/mergify/bp/version-14-hotfix/pr-32622
fix: Ignore linked purchase invoice on cancel (backport #32622)
2022-10-18 09:12:48 +05:30
Deepesh Garg
cc938fb028 fix: Ignore linked purchase invoice on cancel
(cherry picked from commit faadf78332)
2022-10-17 14:08:50 +00:00
Deepesh Garg
9bcec15ca4 Merge pull request #32619 from frappe/mergify/bp/version-14-hotfix/pr-32618
test: lead creation and deletion restricted to dummy company (backport #32618)
2022-10-17 19:37:36 +05:30
ruthra kumar
a88ac4016a refactor: split ple creation function into two
refactor create_payment_ledger_entry function into 2.
one for generating ple map and one for DB entry creation

(cherry picked from commit 9b50221bf0)
2022-10-17 13:16:20 +00:00
rohitwaghchaure
0654f303bb Merge pull request #32615 from frappe/mergify/bp/version-14-hotfix/pr-32613
fix: group warehouse filter not working for Batch-wise Balance history report (backport #32613)
2022-10-17 18:04:09 +05:30
ruthra kumar
98e7b2dd25 test: lead creation and deletion restricted to dummy company
(cherry picked from commit ced8d2a537)
2022-10-17 11:42:33 +00:00
Rohit Waghchaure
1ee0d990e5 chore: seperate function to apply filter for warehouse in case of QB
(cherry picked from commit 2481574a28)
2022-10-17 09:43:30 +00:00
Rohit Waghchaure
faedd85b66 fix: group warehouse filter not working for Batchwise Balance history report
(cherry picked from commit 6381e75fa5)
2022-10-17 09:43:29 +00:00
Deepesh Garg
2db1da0c8a Merge pull request #32612 from frappe/mergify/bp/version-14-hotfix/pr-32607
chore: Remove HRMS related code (backport #32607)
2022-10-17 14:53:39 +05:30
Deepesh Garg
d9519f24aa chore: Remove HRMS related code (#32607)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
(cherry picked from commit dc3fe85921)
2022-10-17 06:09:25 +00:00
Deepesh Garg
a260c594c1 Merge pull request #32608 from frappe/mergify/bp/version-14-hotfix/pr-32591
fix: Renamed Notes section to Comments (backport #32591)
2022-10-15 18:26:00 +05:30
Nabin Hait
dac5989d72 fix: Renamed Dashboard tab label to Connections
(cherry picked from commit f561d8f689)
2022-10-15 12:13:35 +00:00
Nabin Hait
9e94dd9203 fix: Renamed Notes section to Comments
(cherry picked from commit 56a4a77398)
2022-10-15 12:13:35 +00:00
Deepesh Garg
794167aa36 Merge pull request #32606 from frappe/mergify/bp/version-14-hotfix/pr-32594
fix: Party account for multi-order invoices (backport #32594)
2022-10-15 17:41:14 +05:30
Deepesh Garg
eaea8469fc fix: Party account for multi-order invoices
(cherry picked from commit fd49503ba2)
2022-10-15 11:29:42 +00:00
mergify[bot]
af4dafdc64 fix: add project settings to projects workspace (backport #32568) (#32600)
Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2022-10-14 18:01:43 +05:30
ruthra kumar
a012c55c81 Merge pull request #32592 from frappe/mergify/bp/version-14-hotfix/pr-32567
fix: deduplicate Payment Ledger on Item value repost (backport #32567)
2022-10-13 15:28:57 +05:30
mergify[bot]
6a89cb57b0 chore: drop dead code (backport #32595) (#32596)
chore: drop dead code (#32595)

[skip ci]

(cherry picked from commit 50e9698932)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-13 15:27:17 +05:30
ruthra kumar
3c457e2638 test: dedeplication on payment ledger upon repost
(cherry picked from commit eb819368aa)
2022-10-13 08:32:51 +00:00
ruthra kumar
62cabdf792 fix: delete old ple's on item value repost
(cherry picked from commit 65992304bc)
2022-10-13 08:32:51 +00:00
mergify[bot]
ed85683fea fix: don't try to update youtube data if disabled in settings (backport #32588) (#32589)
fix: don't try to update youtube data if disabled in settings (#32588)

fix: cast value from db

[skip ci]

(cherry picked from commit e543dca6a0)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2022-10-13 11:55:36 +05:30
mergify[bot]
829a0ff827 fix: type-cast while saving an item (backport #32549) (#32578)
fix: type-cast while saving an item (#32549)

(cherry picked from commit 43037d893d)

Co-authored-by: Rohan <Alchez@users.noreply.github.com>
2022-10-13 11:54:22 +05:30
Sagar Sharma
0674d42c96 Merge pull request #32586 from frappe/mergify/bp/version-14-hotfix/pr-32576
fix: `Brand Defaults` filters (backport #32576)
2022-10-12 19:25:32 +05:30
Sagar Sharma
cb0c4b5664 fix: Brand Defaults filters
(cherry picked from commit 7da32c7db3)
2022-10-12 13:53:35 +00:00
Frappe PR Bot
36b9926527 chore(release): Bumped to Version 14.3.0
# [14.3.0](https://github.com/frappe/erpnext/compare/v14.2.3...v14.3.0) (2022-10-12)

### Bug Fixes

* consider outgoingrate while valuation rate calculate ([423c019](423c0190e2))
* consider sales rate as incoming rate for transit warehouse in purchase flow ([948b231](948b231e92))
* Do not add tax withheld vouchers post tax withheding in one document ([bd8e61b](bd8e61b2dc))
* Explicitly update modified (backport [#32519](https://github.com/frappe/erpnext/issues/32519)) ([#32575](https://github.com/frappe/erpnext/issues/32575)) ([154904e](154904e960))
* Hanlde rounding loss for internal transfer ([8eda2db](8eda2dbdf8))
* Incoming rate precision fixes for intra company transfer ([b653f43](b653f43b70))
* make readings status mandatory in Quality Inspection ([50d790c](50d790c919))
* minor cleanup ([236a5f3](236a5f37c7))
* more fields reordering related to Tab Break ([db4fbc9](db4fbc9e58))
* PO cancel post advance payment cancel against PO ([bda25c4](bda25c4d5d))
* removed unnecessary imports ([31782d6](31782d6f71))
* set Quality Inspection status based on readings status ([e25db8b](e25db8b1b5))
* single column indexes (backport [#32425](https://github.com/frappe/erpnext/issues/32425)) ([#32513](https://github.com/frappe/erpnext/issues/32513)) ([2f1b8af](2f1b8af13c))
* Tax withholding related fixes ([d5f6938](d5f693806b))
* test case ([0b26131](0b26131b65))
* **test:** `test_rejected_qi_validation` ([943e619](943e6192cc))
* TooManyWritesError during reposting of stock ([73742ff](73742ff565))
* unlink payment on invoice cancellation ([8ba503b](8ba503bf0e))
* use naming_series in budget ([0a24859](0a24859c9c))
* value error on pos submit ([4b65dd7](4b65dd7f2a))

### Features

* **JE:** trigger account field when fetched from template ([17dd1ab](17dd1abb34)), closes [#32409](https://github.com/frappe/erpnext/issues/32409)
* provision to return non consumed components against the work order ([d0f3818](d0f3818060))
* Tab Break in Material Request ([79151be](79151be8ce))
* Tab Break in Purchase Invoice ([89314d4](89314d4171))
* Tab Break in Purchase Order ([5a8329a](5a8329add1))
* Tab Break in Purchase Receipt ([b092791](b092791ac8))
* Tab Break in Quotation ([b0a9f79](b0a9f79e18))
* Tab Break in Sales Order, Delivery Note, Sales Invoice and Purchase Order ([31cd96b](31cd96bc89))
* Tab Break in Supplier Quotation ([22ad3ad](22ad3ad6f3))
2022-10-12 12:21:48 +00:00
Deepesh Garg
d38071eb20 Merge pull request #32560 from frappe/version-14-hotfix
chore: release v14
2022-10-12 17:50:12 +05:30
Deepesh Garg
c4b39c7c1c Merge branch 'version-14' into version-14-hotfix 2022-10-12 17:19:33 +05:30
Deepesh Garg
c83c645ab0 Merge pull request #32579 from frappe/mergify/bp/version-14-hotfix/pr-32563
fix: consider sales rate as incoming rate for transit warehouse in purchase flow (backport #32563)
2022-10-12 17:16:36 +05:30
Deepesh Garg
eab24b8366 Merge pull request #32582 from frappe/mergify/bp/version-14-hotfix/pr-32272
fix: Incoming rate precision fixes for intra company transfer (backport #32272)
2022-10-12 17:05:18 +05:30
rohitwaghchaure
31782d6f71 fix: removed unnecessary imports 2022-10-12 16:34:48 +05:30
Deepesh Garg
2494ccc0a2 chore: check only for inter-company transfers
(cherry picked from commit 9aa5e20ef7)
2022-10-12 10:54:33 +00:00
Deepesh Garg
108720b9c3 chore: fix precision condition
(cherry picked from commit 49601558c6)
2022-10-12 10:54:33 +00:00
Deepesh Garg
4f3e209009 chore: Use proper accounts
(cherry picked from commit 1c05c004cd)
2022-10-12 10:54:33 +00:00
Deepesh Garg
23b18a5e38 chore: Increase precision for other doc fields
(cherry picked from commit c8d2181498)
2022-10-12 10:54:33 +00:00
Deepesh Garg
b386b8c634 chore: GL Entries for SLE diff
(cherry picked from commit df2a0e265b)
2022-10-12 10:54:32 +00:00
Deepesh Garg
49221d47c2 test: Internal tranfer precision loss test
(cherry picked from commit dc20b21fb5)
2022-10-12 10:54:32 +00:00
Deepesh Garg
09e9675653 chore: Increase incoming_rate field precision to 6
(cherry picked from commit b31c3bd35d)
2022-10-12 10:54:32 +00:00
Deepesh Garg
8eda2dbdf8 fix: Hanlde rounding loss for internal transfer
(cherry picked from commit 6e47fd54a0)
2022-10-12 10:54:32 +00:00
Deepesh Garg
b653f43b70 fix: Incoming rate precision fixes for intra company transfer
(cherry picked from commit 083309c056)
2022-10-12 10:54:31 +00:00
Rohit Waghchaure
0b26131b65 fix: test case
(cherry picked from commit 98bf8e1304)
2022-10-12 10:51:00 +00:00
Rohit Waghchaure
423c0190e2 fix: consider outgoingrate while valuation rate calculate
(cherry picked from commit 3266e54e33)
2022-10-12 10:51:00 +00:00
Rohit Waghchaure
948b231e92 fix: consider sales rate as incoming rate for transit warehouse in purchase flow
(cherry picked from commit 683a47f7a1)
2022-10-12 10:50:59 +00:00
Deepesh Garg
23aca6663f Merge pull request #32565 from frappe/mergify/bp/version-14-hotfix/pr-32408
feat: Tab Breaks in all Sales and Purchase cycle transactions (backport #32408)
2022-10-12 14:13:38 +05:30
Deepesh Garg
6c85bdd735 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32408 2022-10-12 13:04:01 +05:30
mergify[bot]
154904e960 fix: Explicitly update modified (backport #32519) (#32575)
fix: Explicitly update modified (#32519)

* fix: Explicitly update modified

required after https://github.com/frappe/frappe/pull/18301

* chore: fix broken translations

(cherry picked from commit 8376fbc982)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-12 12:59:06 +05:30
mergify[bot]
03db8b2f5f ci: disable orchestrator (backport #32571) (#32572)
* ci: disable orchestrator (#32571)

(cherry picked from commit 6ce3ce758c)

* chore: correct build count

* chore: no cov

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-12 12:14:48 +05:30
Nabin Hait
236a5f37c7 fix: minor cleanup
(cherry picked from commit e71c417f7e)
2022-10-11 10:33:30 +00:00
Nabin Hait
db4fbc9e58 fix: more fields reordering related to Tab Break
(cherry picked from commit 5389a35798)
2022-10-11 10:33:30 +00:00
Nabin Hait
89314d4171 feat: Tab Break in Purchase Invoice
(cherry picked from commit aaf3c2b329)
2022-10-11 10:33:30 +00:00
Nabin Hait
b092791ac8 feat: Tab Break in Purchase Receipt
(cherry picked from commit 35f836c4b7)
2022-10-11 10:33:30 +00:00
Nabin Hait
22ad3ad6f3 feat: Tab Break in Supplier Quotation
(cherry picked from commit 3d9263bf86)
2022-10-11 10:33:29 +00:00
Nabin Hait
79151be8ce feat: Tab Break in Material Request
(cherry picked from commit 2172c5034a)
2022-10-11 10:33:29 +00:00
Nabin Hait
5a8329add1 feat: Tab Break in Purchase Order
(cherry picked from commit 10a25603ac)
2022-10-11 10:33:29 +00:00
Nabin Hait
b0a9f79e18 feat: Tab Break in Quotation
(cherry picked from commit 7546562139)
2022-10-11 10:33:28 +00:00
Nabin Hait
31cd96bc89 feat: Tab Break in Sales Order, Delivery Note, Sales Invoice and Purchase Order
(cherry picked from commit f6613e1e4c)
2022-10-11 10:33:27 +00:00
ruthra kumar
db563483cb Merge pull request #32558 from frappe/mergify/bp/version-14-hotfix/pr-32557
fix: Value error on validation of POS invoices with Serial Nos (backport #32557)
2022-10-11 15:36:36 +05:30
ruthra kumar
6fa1b892f7 test: value error on serial no validation on pos
(cherry picked from commit 9e2bd10d03)
2022-10-11 09:13:36 +00:00
ruthra kumar
4b65dd7f2a fix: value error on pos submit
(cherry picked from commit 4b908ebcd6)
2022-10-11 09:13:35 +00:00
Deepesh Garg
62296c85cd Merge pull request #32543 from frappe/mergify/bp/version-14-hotfix/pr-32497
chore: set `Quality Inspection` status based on readings status (backport #32497)
2022-10-11 14:02:58 +05:30
ruthra kumar
a71be4e99a Merge pull request #32550 from frappe/mergify/bp/version-14-hotfix/pr-32548
fix: allow deletion of cancelled Invoices once payments are unlinked (backport #32548)
2022-10-11 13:47:57 +05:30
Ankush Menat
c9002526b9 chore: update codeowners 2022-10-11 12:33:51 +05:30
Deepesh Garg
b9b08e4d5e Merge pull request #32555 from frappe/mergify/bp/version-14-hotfix/pr-32554
fix: use naming_series in budget (backport #32554)
2022-10-11 11:51:51 +05:30
Sagar Sharma
3c0bca3d74 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32497 2022-10-11 11:45:19 +05:30
Anand Baburajan
27509e2141 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32554 2022-10-11 11:06:44 +05:30
rohitwaghchaure
52dd967b0d Merge pull request #32542 from frappe/mergify/bp/version-14-hotfix/pr-32393
feat: provision to return non consumed components against the work order (backport #32393)
2022-10-11 10:39:18 +05:30
Deepesh Garg
9601e47ae4 Merge pull request #32540 from frappe/mergify/bp/version-14-hotfix/pr-32536
fix: PO cancel post advance payment cancel against PO (backport #32536)
2022-10-11 10:17:10 +05:30
anandbaburajan
0a24859c9c fix: use naming_series in budget
(cherry picked from commit e4d7d8c42d)
2022-10-11 04:46:10 +00:00
ruthra kumar
998367cedc test: update ple on payment unlink for SI's and SO's
(cherry picked from commit 143f905838)
2022-10-10 11:12:38 +00:00
ruthra kumar
8ba503bf0e fix: unlink payment on invoice cancellation
(cherry picked from commit 537d953f4c)
2022-10-10 11:12:38 +00:00
Sagar Sharma
943e6192cc fix(test): test_rejected_qi_validation
(cherry picked from commit 4992e4a2b8)
2022-10-10 05:20:45 +00:00
Sagar Sharma
91b0e7ddbd test: add test cases for Quality Inspection status
(cherry picked from commit fcc1272d42)
2022-10-10 05:20:45 +00:00
Sagar Sharma
e25db8b1b5 fix: set Quality Inspection status based on readings status
(cherry picked from commit 2657ece2cd)
2022-10-10 05:20:44 +00:00
Sagar Sharma
50d790c919 fix: make readings status mandatory in Quality Inspection
(cherry picked from commit d7c3b7633a)
2022-10-10 05:20:44 +00:00
Sagar Sharma
8a4c07a7e8 chore: add Manual Inspection field in Quality Inspection DocType
(cherry picked from commit 39707757a6)
2022-10-10 05:20:44 +00:00
Rohit Waghchaure
d0f3818060 feat: provision to return non consumed components against the work order
(cherry picked from commit d59ed24e6c)
2022-10-10 03:39:25 +00:00
Deepesh Garg
bda25c4d5d fix: PO cancel post advance payment cancel against PO
(cherry picked from commit d806e32030)
2022-10-09 13:06:06 +00:00
Frappe PR Bot
f70103dac6 chore(release): Bumped to Version 14.2.3
## [14.2.3](https://github.com/frappe/erpnext/compare/v14.2.2...v14.2.3) (2022-10-07)

### Bug Fixes

* Do not add tax withheld vouchers post tax withheding in one document ([6060072](6060072d22))
* Tax withholding related fixes ([a226fcf](a226fcf898))
2022-10-07 12:40:22 +00:00
Deepesh Garg
a519d68826 Merge pull request #32525 from frappe/mergify/bp/version-14/pr-32522
fix: Tax withholding related fixes (backport #32522)
2022-10-07 18:08:51 +05:30
Deepesh Garg
49cedca8de Merge pull request #32524 from frappe/mergify/bp/version-14-hotfix/pr-32522
fix: Tax withholding related fixes (backport #32522)
2022-10-07 18:08:39 +05:30
Deepesh Garg
6060072d22 fix: Do not add tax withheld vouchers post tax withheding in one document
(cherry picked from commit 781d160c68)
2022-10-07 12:03:29 +00:00
Deepesh Garg
a226fcf898 fix: Tax withholding related fixes
(cherry picked from commit abf5b6be3e)
2022-10-07 12:03:29 +00:00
Deepesh Garg
bd8e61b2dc fix: Do not add tax withheld vouchers post tax withheding in one document
(cherry picked from commit 781d160c68)
2022-10-07 11:03:40 +00:00
Deepesh Garg
d5f693806b fix: Tax withholding related fixes
(cherry picked from commit abf5b6be3e)
2022-10-07 11:03:40 +00:00
mergify[bot]
5f6bbd4929 chore: drop stale demo page (backport #32515) (#32516)
chore: drop stale demo page (#32515)

(cherry picked from commit 07c4a74838)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-06 13:03:29 +05:30
mergify[bot]
2f1b8af13c fix: single column indexes (backport #32425) (#32513)
fix: single column indexes (#32425)

refactor: move single column indexes to doctypes
(cherry picked from commit 8d1db0ea3d)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-10-06 11:37:43 +05:30
Deepesh Garg
3d6a37ea04 Merge pull request #32510 from frappe/mergify/bp/version-14-hotfix/pr-32385
chore: update fr translation (backport #32385)
2022-10-06 09:03:20 +05:30
HENRY Florian
3caf905019 chore: update fr translation (#32385)
(cherry picked from commit 0e4017cbe5)
2022-10-05 17:20:04 +00:00
Deepesh Garg
4941b6e280 Merge pull request #32501 from frappe/mergify/bp/version-14-hotfix/pr-32491
refactor: remove duplicate entries on remarks migration patch (backport #32491)
2022-10-05 18:16:51 +05:30
Deepesh Garg
a02ddc92ef Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32491 2022-10-05 17:30:25 +05:30
Deepesh Garg
6017f6790e Merge pull request #32503 from frappe/mergify/bp/version-14-hotfix/pr-32499
fix: TooManyWritesError during reposting of stock (backport #32499)
2022-10-05 17:29:56 +05:30
Deepesh Garg
3daff104c3 Merge pull request #32505 from frappe/mergify/bp/version-14-hotfix/pr-32478
feat(JE): trigger account field when fetched from template (backport #32478)
2022-10-05 17:28:49 +05:30
Dany Robert
17dd1abb34 feat(JE): trigger account field when fetched from template
Closes #32409

(cherry picked from commit c35adcf5a1)
2022-10-05 10:38:41 +00:00
Rohit Waghchaure
73742ff565 fix: TooManyWritesError during reposting of stock
(cherry picked from commit aaabba9b1e)
2022-10-05 10:36:18 +00:00
ruthra kumar
70e990c4f3 refactor: remove duplicate entries on remarks migration patch
(cherry picked from commit 518ab93e03)
2022-10-05 10:35:06 +00:00
Frappe PR Bot
217e407371 chore(release): Bumped to Version 14.2.2
## [14.2.2](https://github.com/frappe/erpnext/compare/v14.2.1...v14.2.2) (2022-10-04)

### Bug Fixes

* (bulk transaction) key error and better error logging (backport [#32445](https://github.com/frappe/erpnext/issues/32445)) ([#32463](https://github.com/frappe/erpnext/issues/32463)) ([7a80385](7a8038500d))
* add non-stock item filter for `fg_item` in PO ([11f9279](11f92797b9))
* add validation for non-stock item in SCR ([793fa18](793fa18225))
* asset requiring maintenance sold status and added test [v14-hotfix] ([c0a141b](c0a141bfcd))
* asset tests ([6710337](671033755f))
* calculate depreciation properly on asset sale entry and scrap entry ([f5f13c4](f5f13c4611))
* Create accounting dimension fields in asset capitalization ([9ec0d10](9ec0d104a5))
* Disbursement Account in patch to update old loans ([0973989](097398914e))
* don't allow to update `Maintain Stock` if the item has a `BOM` ([e5d1f59](e5d1f59d96))
* fetch swift_number in payment_request ([f09222a](f09222a9e5))
* fix restore asset value after depreciation ([8605245](86052450a5))
* fixes query builder order by issue in postgres (backport [#32441](https://github.com/frappe/erpnext/issues/32441)) ([#32465](https://github.com/frappe/erpnext/issues/32465)) ([8851826](8851826a57))
* Incorrect TCS amount deducted in Sales Invoice ([4d51d73](4d51d73797))
* Item details fetching on making transaction from item dashboard ([db60299](db60299cf3))
* not able to return sold expired batches ([9c56a5f](9c56a5ff7c))
* payment request make bank field Link instead of Read Only ([c8c0f34](c8c0f345d7))
* pick list picked-qty for batch item ([32107c4](32107c4a79))
* refactor asset capitilization ([bad3b33](bad3b330f4))
* searchfields as per customize form not working for Item ([f167abb](f167abbbfd))
* show `Make Purchase Invoice` button based on permission ([fd86876](fd86876a0e))
* supplied items added twice in Stock Entry ([d91ac5e](d91ac5e549))
* test_scrap_asset ([fd232d1](fd232d1db7))
* typo in sales_register's filter mode_of_payment (backport [#32371](https://github.com/frappe/erpnext/issues/32371)) ([#32446](https://github.com/frappe/erpnext/issues/32446)) ([1918b5c](1918b5c4b0))
* update with new Frappe color. fix [#32455](https://github.com/frappe/erpnext/issues/32455) ([#32456](https://github.com/frappe/erpnext/issues/32456)) ([3e7582f](3e7582f55b))

### Reverts

* Revert "fix: fetch swift_number in payment_request" ([d3c151f](d3c151f32b))
2022-10-04 12:13:08 +00:00
Deepesh Garg
90607becb8 Merge pull request #32493 from frappe/version-14-hotfix
chore: release v14
2022-10-04 17:41:32 +05:30
mergify[bot]
d6888bc8c8 refactor(UI): clean up portal pages (backport #29018) (#32489)
refactor(UI): clean up portal pages (#29018)

* refactor: list view for portal pages

* refactor: add align center class

* refactor: change footer btn class(primary)

* refactor: add order style page

* refactor: import order page in website

* refactor: remove table elements

* fix: preview image height

* fix: make string translatable

* refactor: change font variables

* refactor: order preview bg white

* refactor: center align items

* fix: breadcrumb padding

* refactor: make preview image rounded

* refactor: add condition for image container

* refactor: change alignment for mobile view

* fix: make footer button secondary

* refactor: clean code

* refactor: code indentantion

* refactor: remove space

* fix: Payment section cleanup

Co-authored-by: Summayya <frappe@Summayyas-MacBook-Air.local>
Co-authored-by: Marica <maricadsouza221197@gmail.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit da43a5e371)

Co-authored-by: Summayya Hashmani <58825865+sumaiya2908@users.noreply.github.com>
2022-10-04 10:21:50 +05:30
rohitwaghchaure
063f206b9b Merge pull request #32469 from frappe/mergify/bp/version-14-hotfix/pr-32466
fix: not able to return sold expired batches (backport #32466)
2022-10-04 07:34:09 +05:30
mergify[bot]
7a8038500d fix: (bulk transaction) key error and better error logging (backport #32445) (#32463)
fix: (bulk transaction) key error and better error logging (#32445)

* fix: (bulk transaction) key error and better error logging

* chore: pre-commit

* chore: linter - missing comma

(cherry picked from commit 91055151ce)

Co-authored-by: Dany Robert <danyrt@wahni.com>
2022-10-03 17:45:51 +05:30
mergify[bot]
8851826a57 fix: fixes query builder order by issue in postgres (backport #32441) (#32465)
fix: fixes query builder order by issue in postgres (#32441)

* fix: fixes query builder order by issue in postgres

* fix: linter

(cherry picked from commit 69efd2ee24)

Co-authored-by: Shridhar Patil <shridhar.p@zerodha.com>
2022-10-03 16:25:23 +05:30
Sagar Sharma
2a82f16680 Merge pull request #32473 from frappe/mergify/bp/version-14-hotfix/pr-32472
fix: pick list picked-qty for batch item (backport #32472)
2022-10-03 15:24:14 +05:30
Sagar Sharma
32107c4a79 fix: pick list picked-qty for batch item
(cherry picked from commit ba02209f1d)
2022-10-03 09:16:37 +00:00
Deepesh Garg
bbc7559380 Merge pull request #32468 from frappe/mergify/bp/version-14-hotfix/pr-32394
fix: fetch swift number in payment request from bank doctype (backport #32394)
2022-10-03 14:29:06 +05:30
Rohit Waghchaure
9c56a5ff7c fix: not able to return sold expired batches
(cherry picked from commit 0b1727cf79)
2022-10-03 07:45:46 +00:00
Maharshi Patel
c8c0f345d7 fix: payment request make bank field Link instead of Read Only
(cherry picked from commit dc8d49260c)
2022-10-03 07:26:38 +00:00
Maharshi Patel
d3c151f32b Revert "fix: fetch swift_number in payment_request"
This reverts commit f42a8e4e03.

(cherry picked from commit 9245d3b5cd)
2022-10-03 07:26:38 +00:00
Maharshi Patel
f09222a9e5 fix: fetch swift_number in payment_request
There isn't direct link between payment_request and bank so swift_number wasn't fetched using Fetch form. I fixed it by fetching swift_number on_change of bank_account.

(cherry picked from commit f42a8e4e03)
2022-10-03 07:26:38 +00:00
Deepesh Garg
ba80ba07f1 Merge pull request #32461 from frappe/mergify/bp/version-14-hotfix/pr-32456
fix: update with new Frappe color. fix #32455 (backport #32456)
2022-10-03 09:14:57 +05:30
Deepesh Garg
34845b7630 Merge pull request #32459 from frappe/mergify/bp/version-14-hotfix/pr-32454
fix: test_scrap_asset not working on end of month (backport #32454)
2022-10-03 09:04:01 +05:30
Deepesh Garg
ecfa64f22e Merge pull request #32460 from AnandBaburajan/fix_asset_sold_status_v14
fix: status of assets with maintenance_required changing back to 'Partially Depreciated' some time after being sold (backport #32405)
2022-10-03 08:58:40 +05:30
Muvuk
3e7582f55b fix: update with new Frappe color. fix #32455 (#32456)
* Update with new Frappe color.

* refactor: use css variables

Co-authored-by: Ankush Menat <ankushmenat@gmail.com>
(cherry picked from commit 73e5a7d671)
2022-10-02 17:02:02 +00:00
Anand Baburajan
6f571218bf Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32454 2022-10-02 19:14:00 +05:30
Anand Baburajan
8ab2f2f925 Merge branch 'version-14-hotfix' into fix_asset_sold_status_v14 2022-10-02 18:31:04 +05:30
Deepesh Garg
2d34761057 Merge pull request #32429 from frappe/mergify/bp/version-14-hotfix/pr-32420
patch: Create accounting dimension fields in asset capitalization (backport #32420)
2022-10-02 18:28:06 +05:30
anandbaburajan
c0a141bfcd fix: asset requiring maintenance sold status and added test [v14-hotfix] 2022-10-02 18:27:37 +05:30
anandbaburajan
fd232d1db7 fix: test_scrap_asset
(cherry picked from commit 47bd4be71b)
2022-10-02 12:15:55 +00:00
Deepesh Garg
df9147f2e5 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32420 2022-10-02 17:45:30 +05:30
Sagar Sharma
8e68219c5b Merge pull request #32440 from frappe/mergify/bp/version-14-hotfix/pr-32428
fix: don't allow to update `Maintain Stock` if the item has a `BOM` (backport #32428)
2022-10-02 12:07:28 +05:30
Sagar Sharma
060ce45572 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32428 2022-10-01 14:32:18 +05:30
Sagar Sharma
432da7d1d1 Merge pull request #32444 from frappe/mergify/bp/version-14-hotfix/pr-32439
fix: add non-stock item filter for `fg_item` in PO (backport #32439)
2022-10-01 14:32:02 +05:30
Sagar Sharma
9318d9a7fc Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32439 2022-10-01 14:31:49 +05:30
Deepesh Garg
faca06d6c3 Merge pull request #32433 from frappe/mergify/bp/version-14-hotfix/pr-32368
chore: remove discount accounting logic from PI and PI Item (backport #32368)
2022-10-01 12:05:10 +05:30
mergify[bot]
1918b5c4b0 fix: typo in sales_register's filter mode_of_payment (backport #32371) (#32446)
fix: typo in sales_register's filter mode_of_payment (#32371)
2022-10-01 12:04:16 +05:30
Deepesh Garg
444f3b5397 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32368 2022-10-01 11:10:03 +05:30
Deepesh Garg
3736f4ecb8 Merge pull request #32430 from frappe/mergify/bp/version-14-hotfix/pr-32402
fix: Item details fetching on making transaction from item dashboard (backport #32402)
2022-10-01 11:03:08 +05:30
Deepesh Garg
5fe2079dcc Merge pull request #32434 from frappe/mergify/bp/version-14-hotfix/pr-32412
fix: Incorrect TCS amount deducted in Sales Invoice (backport #32412)
2022-10-01 11:01:28 +05:30
Sagar Sharma
11f92797b9 fix: add non-stock item filter for fg_item in PO
(cherry picked from commit f95ed7dca2)
2022-09-30 18:29:54 +00:00
Sagar Sharma
17996efbc7 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32428 2022-09-30 23:57:49 +05:30
Sagar Sharma
c2bed24c31 Merge pull request #32438 from frappe/mergify/bp/version-14-hotfix/pr-32436
fix: add validation for non-stock item in SCR (backport #32436)
2022-09-30 23:57:30 +05:30
Sagar Sharma
e5d1f59d96 fix: don't allow to update Maintain Stock if the item has a BOM
(cherry picked from commit 6d0842465b)
2022-09-30 13:39:11 +00:00
Sagar Sharma
793fa18225 fix: add validation for non-stock item in SCR
(cherry picked from commit fa2290657a)
2022-09-30 11:23:50 +00:00
Deepesh Garg
f7ec00ef49 chore: Remove print statements
(cherry picked from commit bff3cd9068)
2022-09-30 10:35:58 +00:00
Deepesh Garg
4d51d73797 fix: Incorrect TCS amount deducted in Sales Invoice
(cherry picked from commit 08443c6421)
2022-09-30 10:35:57 +00:00
ruthra kumar
7a096231fb chore: clean up purchase invoice and purchase invoice item
1. remove discount accounting related fields from purchase invoice and
purchase invoice item
2. clean buying settings doctype

(cherry picked from commit 21095502b9)
2022-09-30 10:29:17 +00:00
Deepesh Garg
db60299cf3 fix: Item details fetching on making transaction from item dashboard
(cherry picked from commit 0439e41a44)
2022-09-30 10:27:15 +00:00
Nabin Hait
9ec0d104a5 fix: Create accounting dimension fields in asset capitalization
(cherry picked from commit fe891aa488)
2022-09-30 10:26:33 +00:00
Sagar Sharma
7243089027 Merge pull request #32418 from frappe/mergify/bp/version-14-hotfix/pr-32404
fix(ux): show `Make Purchase Invoice` button based on permission (backport #32404)
2022-09-29 17:16:25 +05:30
rohitwaghchaure
5aa9c4fd07 Merge pull request #32417 from frappe/mergify/bp/version-14-hotfix/pr-32414
fix: searchfields as per customize form not working for Item (backport #32414)
2022-09-29 15:45:10 +05:30
Sagar Sharma
fd86876a0e fix: show Make Purchase Invoice button based on permission
(cherry picked from commit 80080a3d7b)
2022-09-29 09:26:36 +00:00
Rohit Waghchaure
3f0a3b702d test: added test case
(cherry picked from commit 3656f7d06f)
2022-09-29 09:22:44 +00:00
Rohit Waghchaure
f167abbbfd fix: searchfields as per customize form not working for Item
(cherry picked from commit fd889fd29a)
2022-09-29 09:22:44 +00:00
Nabin Hait
5015f38a8d Merge pull request #32398 from frappe/mergify/bp/version-14-hotfix/pr-32226
fix: calculate depreciation properly on asset sale entry and scrap entry (backport #32226)
2022-09-29 12:34:42 +05:30
anandbaburajan
381231257b chore: fix merge conflicts 2022-09-29 11:59:53 +05:30
Anand Baburajan
f4648ed610 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32226 2022-09-28 22:24:03 +05:30
Sagar Sharma
51fcb3c666 Merge pull request #32407 from frappe/mergify/bp/version-14-hotfix/pr-32406
fix: supplied items added twice in Stock Entry (backport #32406)
2022-09-28 21:44:59 +05:30
Sagar Sharma
7b11da2373 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32406 2022-09-28 21:44:50 +05:30
Deepesh Garg
5c0446d9fc Merge pull request #32411 from frappe/mergify/bp/version-14-hotfix/pr-32403
fix: Disbursement Account in patch to update old loans (backport #32403)
2022-09-28 20:14:01 +05:30
Deepesh Garg
097398914e fix: Disbursement Account in patch to update old loans
(cherry picked from commit be623ce8e8)
2022-09-28 14:07:00 +00:00
Sagar Sharma
d91ac5e549 fix: supplied items added twice in Stock Entry
(cherry picked from commit ac7409bbf3)
2022-09-28 12:20:55 +00:00
Frappe PR Bot
9c915ceeb2 chore(release): Bumped to Version 14.2.1
## [14.2.1](https://github.com/frappe/erpnext/compare/v14.2.0...v14.2.1) (2022-09-28)

### Bug Fixes

* `For Quantity` error msg in `Stock Entry` ([4b6e1f4](4b6e1f4892))
* Add return against indexes for POS Invoice ([3abe4a1](3abe4a16af))
* Add return against indexes for POS Invoice ([32ad36a](32ad36ad0f))
* Add strings to translate function ([2eeff8a](2eeff8a00b))
* add translate function to strings ([f5a3f8b](f5a3f8b486))
* add translate function to strings ([e464de3](e464de3c5d))
* allow to return expired batches using purchase return ([305eb02](305eb02bfc))
* consider overproduction percentage for WO finish button ([4a29c7f](4a29c7fafb))
* difference amount calculation on payment reconciliation ([b389740](b3897406b0))
* get amount in words for debit note ([7554ac9](7554ac9569))
* item_code key error in production plan ([4e31448](4e31448e3d))
* Move subscription process to hourly long quque ([b2f280d](b2f280dee6))
* opening entry causing discepancy between stock and trial balance ([2fd0291](2fd0291afb))
* POS only validate QTY if is_stock_item ([29c4a32](29c4a32c33))
* POS properly validate stock for bundle products ([6b55a01](6b55a01834))
* Reduce font size for Process Statement of accounts print/pdf ([bcfa9b8](bcfa9b8593))
* report chart field type ([931aa62](931aa62072))
* Scrap Asset Accounting Dimensions (backport [#31949](https://github.com/frappe/erpnext/issues/31949)) ([#32376](https://github.com/frappe/erpnext/issues/32376)) ([a28ae4d](a28ae4d653))
* total value in all keys ([24234d5](24234d584d))
* typo on opportunity summary by sales stage report ([a403e72](a403e729d6))
2022-09-28 09:49:04 +00:00
Deepesh Garg
4b8dd1fab6 Merge pull request #32375 from frappe/version-14-hotfix
chore: release v14
2022-09-28 15:17:26 +05:30
Deepesh Garg
fc581464b8 Merge pull request #32396 from frappe/mergify/bp/version-14-hotfix/pr-32379
fix: POS only validate QTY if is_stock_item (backport #32379)
2022-09-28 14:47:41 +05:30
anandbaburajan
b3b7650ac8 chore: add blank lines
(cherry picked from commit c6a7de0e54)

# Conflicts:
#	erpnext/assets/doctype/asset/depreciation.py
2022-09-28 08:59:43 +00:00
anandbaburajan
86052450a5 fix: fix restore asset value after depreciation
(cherry picked from commit 43a3400221)
2022-09-28 08:59:42 +00:00
anandbaburajan
bad3b330f4 fix: refactor asset capitilization
(cherry picked from commit 5a8b28c194)
2022-09-28 08:59:42 +00:00
anandbaburajan
671033755f fix: asset tests
(cherry picked from commit 11ac20e5ee)
2022-09-28 08:59:41 +00:00
anandbaburajan
f5f13c4611 fix: calculate depreciation properly on asset sale entry and scrap entry
(cherry picked from commit ff5cad1cd6)

# Conflicts:
#	erpnext/assets/doctype/asset/depreciation.py
2022-09-28 08:59:41 +00:00
Maharshi Patel
6b55a01834 fix: POS properly validate stock for bundle products
Stock availability was not calculated properly for Product Bundle with non stock item so i have added logic to properly calculate that as well.

(cherry picked from commit e392ea1104)
2022-09-28 08:16:23 +00:00
Maharshi Patel
29c4a32c33 fix: POS only validate QTY if is_stock_item
POS invoice raised " Item not available " validation error even though item is non_stock.

(cherry picked from commit e39e088f18)
2022-09-28 08:16:22 +00:00
Deepesh Garg
c9c203a5eb Merge pull request #32383 from frappe/mergify/bp/version-14-hotfix/pr-32378
fix: Add return against indexes for POS Invoice (backport #32378)
2022-09-28 10:54:44 +05:30
Deepesh Garg
261aac8e26 Merge pull request #32387 from frappe/mergify/bp/version-14-hotfix/pr-32382
fix: Move subscription process to hourly long queue (backport #32382)
2022-09-28 08:09:26 +05:30
Deepesh Garg
b2f280dee6 fix: Move subscription process to hourly long quque
(cherry picked from commit 82a2f31ada)
2022-09-27 18:09:50 +00:00
Deepesh Garg
3abe4a16af fix: Add return against indexes for POS Invoice
(cherry picked from commit cbfe28286a)
2022-09-27 16:52:39 +00:00
Deepesh Garg
32ad36ad0f fix: Add return against indexes for POS Invoice
(cherry picked from commit 1f6205e1ea)
2022-09-27 16:52:38 +00:00
Sagar Sharma
e98c0ab924 Merge pull request #32380 from frappe/mergify/bp/version-14-hotfix/pr-32377
fix: consider overproduction percentage for WO finish button (backport #32377)
2022-09-27 20:18:25 +05:30
Sagar Sharma
4a29c7fafb fix: consider overproduction percentage for WO finish button
(cherry picked from commit 05392e0918)
2022-09-27 12:15:20 +00:00
Sagar Sharma
4b6e1f4892 fix: For Quantity error msg in Stock Entry
(cherry picked from commit 9049db41ae)
2022-09-27 12:15:19 +00:00
mergify[bot]
a28ae4d653 fix: Scrap Asset Accounting Dimensions (backport #31949) (#32376)
fix: Scrap Asset Accounting Dimensions (#31949)

* fix: Scrap Asset Accounting Dimensions

(cherry picked from commit c760ca2323)

Co-authored-by: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com>
2022-09-27 17:07:38 +05:30
rohitwaghchaure
11be30d68e Merge pull request #32372 from frappe/mergify/bp/version-14-hotfix/pr-32370
fix: Not allowing to return expired batches using purchase return (backport #32370)
2022-09-27 14:58:51 +05:30
Rohit Waghchaure
305eb02bfc fix: allow to return expired batches using purchase return
(cherry picked from commit a4a86ee23f)
2022-09-27 08:51:01 +00:00
rohitwaghchaure
1184fa3313 Merge pull request #32365 from frappe/mergify/bp/version-14-hotfix/pr-32339
fix: opening entry causing discrepancy between stock and trial balance (backport #32339)
2022-09-27 10:56:04 +05:30
Rohit Waghchaure
2fd0291afb fix: opening entry causing discepancy between stock and trial balance
(cherry picked from commit bc3ab45af2)
2022-09-26 15:03:25 +00:00
rohitwaghchaure
959fdcd841 Merge pull request #32364 from frappe/mergify/bp/version-14-hotfix/pr-32049
Allow Item Templates in Work Order Items (backport #32049)
2022-09-26 20:33:21 +05:30
Dhananjay Palshikar
a83e36b801 Allow Item Templates in Work Order Items
Item Variants can be a part of the items tables, however, when BOM items are fetched to be a part of the Work Order items, item variants were being filtered out.
The filtering out does not serve a purpose. Having Item variants in BOMs allows for template like behaviour.

(cherry picked from commit 75396c02d2)
2022-09-26 14:58:05 +00:00
rohitwaghchaure
37d0721f36 Merge pull request #32359 from frappe/mergify/bp/version-14-hotfix/pr-32347
refactor: rewrite `Incorrect Stock Value Report` queries in `QB` (backport #32347)
2022-09-26 20:24:58 +05:30
Deepesh Garg
408f2bda82 Merge pull request #32361 from frappe/mergify/bp/version-14-hotfix/pr-32146
fix: report chart fieldtype (backport #32146)
2022-09-26 18:36:33 +05:30
Deepesh Garg
ddf4d02c7d Merge pull request #32356 from frappe/mergify/bp/version-14-hotfix/pr-32276
fix: add some strings to translate function (backport #32276)
2022-09-26 18:33:32 +05:30
Sagar Sharma
2f5276b794 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32347 2022-09-26 18:18:29 +05:30
Sagar Sharma
e708a33684 Merge pull request #32357 from frappe/mergify/bp/version-14-hotfix/pr-32346
refactor: rewrite `Item Price Stock Report` queries in `QB` (backport #32346)
2022-09-26 18:18:12 +05:30
hendrik
931aa62072 fix: report chart field type
* fix: report chart field type

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit f41d1500b0)
2022-09-26 12:18:17 +00:00
Sagar Sharma
8c6ddb6721 refactor: rewrite Incorrect Stock Value Report queries in QB
(cherry picked from commit b93331e844)
2022-09-26 11:40:00 +00:00
Sagar Sharma
f1d774034c refactor: rewrite Item Price Stock Report queries in QB
(cherry picked from commit 22299d2382)
2022-09-26 11:39:04 +00:00
Deepesh Garg
4a1de87822 Merge pull request #32354 from frappe/mergify/bp/version-14-hotfix/pr-32336
fix: typo on opportunity summary by sales stage report (backport #32336)
2022-09-26 16:58:58 +05:30
Ernesto Ruiz
f5a3f8b486 fix: add translate function to strings
(cherry picked from commit 94199b7867)
2022-09-26 11:09:49 +00:00
Ernesto Ruiz
e464de3c5d fix: add translate function to strings
(cherry picked from commit aa49ec815a)
2022-09-26 11:09:48 +00:00
Ernesto Ruiz
2eeff8a00b fix: Add strings to translate function
(cherry picked from commit 9decebe6e1)
2022-09-26 11:09:48 +00:00
Ernesto Ruiz
a403e729d6 fix: typo on opportunity summary by sales stage report
(cherry picked from commit c4919cf5ec)
2022-09-26 11:08:18 +00:00
ruthra kumar
1c32bf23d7 Merge pull request #32351 from frappe/mergify/bp/version-14-hotfix/pr-32310
fix: total value in all keys (backport #32310)
2022-09-26 12:05:20 +05:30
Ankush Menat
e599b61ac5 chore: drop 1 runner from stable branch 2022-09-26 11:37:14 +05:30
nishibakabeer
24234d584d fix: total value in all keys
Gross and net profit report showing wrong values in monthly quarterly and half yearly filters which is the total value
@ruthra-kumar added in develop branch as suggested ( https://github.com/frappe/erpnext/pull/32020)

(cherry picked from commit 6919f389aa)
2022-09-26 06:03:33 +00:00
ruthra kumar
a69881e8e5 Merge pull request #32349 from frappe/mergify/bp/version-14-hotfix/pr-32303
fix: difference amount calculation and popup on payment reconciliation (backport #32303)
2022-09-26 11:15:43 +05:30
ruthra kumar
b3897406b0 fix: difference amount calculation on payment reconciliation
(cherry picked from commit 122d5f2729)
2022-09-26 04:57:57 +00:00
Deepesh Garg
0136d58d59 Merge pull request #32334 from frappe/mergify/bp/version-14-hotfix/pr-32117
fix: Reduce font size for Process Statement of accounts print/pdf (backport #32117)
2022-09-25 17:21:10 +05:30
Sagar Sharma
3a74ac9194 Merge pull request #32337 from frappe/mergify/bp/version-14-hotfix/pr-32324
refactor: rewrite `Production Planning Report` queries in `QB` (backport #32324)
2022-09-23 10:37:54 +05:30
Sagar Sharma
21a4556f99 refactor: rewrite Production Planning Report queries in QB
(cherry picked from commit 8417b9b99c)
2022-09-23 04:20:22 +00:00
Deepesh Garg
bcfa9b8593 fix: Reduce font size for Process Statement of accounts print/pdf
(cherry picked from commit 6bfd193b0d)
2022-09-22 19:39:17 +00:00
Sagar Sharma
f2dd547451 Merge pull request #32327 from s-aga-r/backport/v14-h/32309
fix: item_code key error in production plan (backport #32309)
2022-09-22 18:32:21 +05:30
Rohit Waghchaure
4e31448e3d fix: item_code key error in production plan 2022-09-22 18:07:20 +05:30
Sagar Sharma
d5d31060c4 Merge pull request #32317 from s-aga-r/backport/v14-h/32304
refactor: rewrite `Exponential Smoothing Forecasting` queries in `QB` (backport #32304)
2022-09-22 01:32:10 +05:30
Sagar Sharma
f5c517d08d refactor: rewrite Exponential Smoothing Forecasting queries in QB 2022-09-21 21:31:37 +05:30
Deepesh Garg
8d5131ef8f Merge pull request #32300 from frappe/mergify/bp/version-14-hotfix/pr-32296
fix: get amount in words for debit note (backport #32296)
2022-09-21 13:07:25 +05:30
Sagar Sharma
0e7c0144cb Merge pull request #32306 from s-aga-r/backport/14/refactor/report/exponential-smoothing-forecasting
refactor: rewrite `BOM Variance Report` queries in `QB` (backport #32297)
2022-09-21 11:51:22 +05:30
Sagar Sharma
d54bf6d91c refactor: rewrite BOM Variance Report queries in QB 2022-09-21 11:16:45 +05:30
Frappe PR Bot
c50d6c5e74 chore(release): Bumped to Version 14.2.0
# [14.2.0](https://github.com/frappe/erpnext/compare/v14.1.2...v14.2.0) (2022-09-20)

### Bug Fixes

* `po_detail` or `sco_rm_detail` not getting set while while mapping SE ([ff210c7](ff210c73eb))
* `sco_rm_detail` in Stock Entry ([a15f0d4](a15f0d427c))
* abbreviation issue on renaming cost center ([f752822](f752822bb3))
* Add child table for tax withheld vouchers ([e807662](e8076629fa))
* always set default expense account in company ([3b79e24](3b79e24c7e))
* **Asset Capitalization:** Hide source items section if table is empty ([003cfe2](003cfe2717))
* **Asset Capitalization:** Reverse depreciation on cancel ([85d1a23](85d1a237ce))
* **Asset Capitalization:** update code for changes in depreciation logic ([dc3c27f](dc3c27fd1b))
* **Asset Capitalization:** validation edge cases ([c311b8e](c311b8ea4f))
* **Asset Captalization:** run_serially on posting_date changed ([132b517](132b517584))
* **Asset:** On Depreciation reversal, remove Journal Entry reference ([e832944](e832944dfe))
* consider posting time for internal transfer PO ([f370c7b](f370c7b50b))
* correct sql output format in CRM patch (backport [#32213](https://github.com/frappe/erpnext/issues/32213)) ([#32215](https://github.com/frappe/erpnext/issues/32215)) ([64aad88](64aad88684))
* create dunning from sales invoice ([17ea6cc](17ea6cc5a5))
* Depreciation posting date only when calculate depreciation is checked ([4939153](4939153f8c))
* disable cwip in asset repair tests ([5cab0aa](5cab0aa1d7))
* fetch description only if empty on the payment schedule ([8876631](887663129e))
* Fetch vouchers to show in Invoice ([f759c29](f759c29d55))
* incorrect gl if tax on multi currency payment entry ([dcfc11d](dcfc11df7a))
* make `po_detail` or `sco_rm_detail` mandatory for SE `Send to Subcontractor` ([908944b](908944b68b))
* merge conflict ([cd613c6](cd613c6033))
* No permission to read doctype ([442f54a](442f54a988))
* Parent Level project linkning on creating PO from project ([cbaffb4](cbaffb4858))
* pending accrual entries ([b9a2499](b9a249918a))
* production plan pending-qty ([5f3caf6](5f3caf6975))
* remove no_copy for ignore_pricing_rule ([c000319](c0003195b8))
* suggestion threshold label and rule was not working for other items with min and max amount ([436b7e3](436b7e3b70))
* TDS deduction via journal entry ([eba46da](eba46dae6c))
* test cases ([f126e88](f126e88e5e))
* test cases ([153ef5f](153ef5f164))
* unknown column error while updating value of maintain-stock in item master ([38488c1](38488c13e6))
* use default supplier currency if default supplier is enabled ([c98413c](c98413c981))
* warehouse filter in `BOM Stock Calculated Report` ([ed9a896](ed9a896f72))

### Features

* Asset Capitalization ([38c3107](38c31077c9))
* Asset Capitalization Form ([6c74896](6c748966e7))
* **Asset Capitalization:** Accounting Fields ([702b5c3](702b5c32c1))
* **Asset Capitalization:** Finance Book field in Asset Row ([3b9bc8e](3b9bc8e4ef))
* **Asset Capitalization:** Submission and Cancellation ([7a5d75b](7a5d75b68d))
2022-09-20 18:31:07 +00:00
Deepesh Garg
08daa237f2 Merge pull request #32292 from frappe/version-14-hotfix
chore: release v14
2022-09-20 23:59:35 +05:30
Sagar Sharma
85cda4a995 Merge pull request #32301 from frappe/mergify/bp/version-14-hotfix/pr-32295
refactor: rewrite `BOM Stock Report` queries in `QB` (backport #32295)
2022-09-20 23:09:25 +05:30
Sagar Sharma
ed9a896f72 fix: warehouse filter in BOM Stock Calculated Report
(cherry picked from commit 390ce5719d)
2022-09-20 17:37:19 +00:00
Sagar Sharma
181dccd8d8 refactor: rewrite BOM Stock Report queries in QB
(cherry picked from commit 8fd7c04920)
2022-09-20 17:37:18 +00:00
ruthra kumar
7554ac9569 fix: get amount in words for debit note
(cherry picked from commit 70f6484d9d)
2022-09-20 12:44:03 +00:00
Deepesh Garg
fcd46aa243 Merge pull request #32294 from frappe/mergify/bp/version-14-hotfix/pr-32284
fix: remove no_copy for ignore_pricing_rule (backport #32284)
2022-09-20 17:16:12 +05:30
Deepesh Garg
d62b983057 chore: Resolve conflicts 2022-09-20 16:45:23 +05:30
Deepesh Garg
b0131a8a80 Merge pull request #32288 from frappe/mergify/bp/version-14-hotfix/pr-32204
fix(UX): More predictable tax withholding application in invoices (backport #32204)
2022-09-20 16:38:44 +05:30
Maharshi Patel
c0003195b8 fix: remove no_copy for ignore_pricing_rule
(cherry picked from commit 8c5b420aea)
2022-09-20 10:50:22 +00:00
Deepesh Garg
957fc440fd Merge pull request #32290 from frappe/mergify/bp/version-14-hotfix/pr-32264
fix: Depreciation posting date only when calculate depreciation is checked (backport #32264)
2022-09-20 15:00:19 +05:30
Deepesh Garg
4939153f8c fix: Depreciation posting date only when calculate depreciation is checked
(cherry picked from commit fac82cf69b)
2022-09-20 09:18:22 +00:00
Deepesh Garg
302f2d10e2 chore: fix tests
(cherry picked from commit 9aa1f84d45)
2022-09-20 09:06:40 +00:00
Deepesh Garg
eba46dae6c fix: TDS deduction via journal entry
(cherry picked from commit 36d0906ea2)
2022-09-20 09:06:40 +00:00
Deepesh Garg
27fdd41a6e test: Add tests
(cherry picked from commit b6184ce471)
2022-09-20 09:06:40 +00:00
Deepesh Garg
f759c29d55 fix: Fetch vouchers to show in Invoice
(cherry picked from commit 3fb1595a4e)
2022-09-20 09:06:40 +00:00
Deepesh Garg
e8076629fa fix: Add child table for tax withheld vouchers
(cherry picked from commit 246c1a9380)
2022-09-20 09:06:39 +00:00
Sagar Sharma
77678ecfeb Merge pull request #32285 from frappe/mergify/bp/version-14-hotfix/pr-32280
refactor: rewrite `Item Shortage Report` queries in QB (backport #32280)
2022-09-20 11:43:55 +05:30
Sagar Sharma
273ed40cfb test: add test cases for Item Shortage Report
(cherry picked from commit 3dc754cac2)
2022-09-20 05:45:04 +00:00
Sagar Sharma
a56b5ed8e5 refactor: rewrite Item Shortage Report queries in QB
(cherry picked from commit f0a78aa559)
2022-09-20 05:45:03 +00:00
ruthra kumar
d7d75be0d7 Merge pull request #32283 from frappe/mergify/bp/version-14-hotfix/pr-32217
fix: incorrect gl if tax on multi currency payment entry (backport #32217)
2022-09-20 11:10:25 +05:30
ruthra kumar
5ba5b7bf51 test: gl entries of payments with advance tax
(cherry picked from commit 5bd5dd7262)
2022-09-20 04:31:10 +00:00
ruthra kumar
dcfc11df7a fix: incorrect gl if tax on multi currency payment entry
(cherry picked from commit f0ae77b23b)
2022-09-20 04:31:10 +00:00
Deepesh Garg
f14b577766 Merge pull request #32278 from frappe/mergify/bp/version-14-hotfix/pr-32235
fix: fetch description only if empty on the payment schedule (backport #32235)
2022-09-20 09:54:17 +05:30
Maharshi Patel
887663129e fix: fetch description only if empty on the payment schedule
added fetch_if_empty on description field of payment_schedule.

(cherry picked from commit f4b64686ae)
2022-09-20 03:44:29 +00:00
Sagar Sharma
327256a516 Merge pull request #32275 from frappe/mergify/bp/version-14-hotfix/pr-32274
fix: `po_detail` or `sco_rm_detail` not getting set while while mapping SE (backport #32274)
2022-09-19 19:46:43 +05:30
Sagar Sharma
ff210c73eb fix: po_detail or sco_rm_detail not getting set while while mapping SE
(cherry picked from commit 3a9c08e7c9)
2022-09-19 13:49:43 +00:00
ruthra kumar
a85dd86832 Merge pull request #32270 from frappe/mergify/bp/version-14-hotfix/pr-32251
fix: use default supplier currency if default supplier is enabled (backport #32251)
2022-09-19 14:04:16 +05:30
ruthra kumar
c98413c981 fix: use default supplier currency if default supplier is enabled
(cherry picked from commit 77fdc37cb7)
2022-09-19 07:33:18 +00:00
Deepesh Garg
198d19b920 Merge pull request #32246 from frappe/mergify/bp/version-14-hotfix/pr-32238
fix: suggestion threshold label and rule was not working for other item with min and max amount (backport #32238)
2022-09-19 10:10:55 +05:30
Deepesh Garg
5a2e286de6 Merge pull request #32255 from frappe/mergify/bp/version-14-hotfix/pr-32244
fix: Parent Level project linkning on creating PO from project (backport #32244)
2022-09-18 19:41:45 +05:30
Sagar Sharma
24ccebc633 Merge pull request #32260 from frappe/mergify/bp/version-14-hotfix/pr-32250
fix: make `po_detail` or `sco_rm_detail` mandatory for SE Send to Subcontractor (backport #32250)
2022-09-18 15:58:48 +05:30
Sagar Sharma
908944b68b fix: make po_detail or sco_rm_detail mandatory for SE Send to Subcontractor
(cherry picked from commit b90875575c)
2022-09-18 09:24:48 +00:00
Deepesh Garg
cbaffb4858 fix: Parent Level project linkning on creating PO from project
(cherry picked from commit 93e134aab0)
2022-09-17 15:00:33 +00:00
Sagar Sharma
7dcb8bb674 Merge pull request #32248 from frappe/mergify/bp/version-14-hotfix/pr-32247
fix: add SCO supplied item reference while mapping the Stock Entry (backport #32247)
2022-09-17 16:09:15 +05:30
Sagar Sharma
a15f0d427c fix: sco_rm_detail in Stock Entry
(cherry picked from commit 2f97370b8e)
2022-09-17 10:12:48 +00:00
Rohit Waghchaure
436b7e3b70 fix: suggestion threshold label and rule was not working for other items with min and max amount
(cherry picked from commit f5bd3fa952)
2022-09-17 06:42:22 +00:00
Sagar Sharma
075d456417 Merge pull request #32240 from frappe/mergify/bp/version-14-hotfix/pr-32236
fix: production plan pending-qty (backport #32236)
2022-09-16 21:31:57 +05:30
Sagar Sharma
ff78906361 test: update test case for production plan pending-qty
(cherry picked from commit bd6af7c613)
2022-09-16 13:00:32 +00:00
Sagar Sharma
5f3caf6975 fix: production plan pending-qty
(cherry picked from commit 5be7d42dfd)
2022-09-16 13:00:31 +00:00
Sagar Sharma
5c09b28d62 Merge pull request #32234 from frappe/mergify/bp/version-14-hotfix/pr-32233
refactor: rewrite Production Plan queries in QB (backport #32233)
2022-09-16 13:57:26 +05:30
Sagar Sharma
b700a0be1f refactor: rewrite Production Plan queries in QB
(cherry picked from commit b8cf3b4c77)
2022-09-16 08:00:08 +00:00
Nabin Hait
cceadcec33 Merge pull request #32231 from frappe/mergify/bp/version-14-hotfix/pr-32225
fix: create dunning from sales invoice (backport #32225)
2022-09-16 13:06:45 +05:30
Nabin Hait
22372e69eb Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-32225 2022-09-16 12:17:14 +05:30
Nabin Hait
88c446b0bf Merge pull request #32232 from nabinhait/asset-repair-test-fix1
fix: disable cwip in asset repair tests
2022-09-16 12:17:01 +05:30
Nabin Hait
5cab0aa1d7 fix: disable cwip in asset repair tests 2022-09-16 11:44:25 +05:30
Nabin Hait
569b4c256a Merge pull request #32230 from frappe/mergify/bp/version-14-hotfix/pr-32219
fix: No permission to read doctype (backport #32219)
2022-09-16 11:37:09 +05:30
Nabin Hait
17ea6cc5a5 fix: create dunning from sales invoice
(cherry picked from commit 29db084dc3)
2022-09-16 05:35:51 +00:00
Nabin Hait
442f54a988 fix: No permission to read doctype
(cherry picked from commit c0da948a4e)
2022-09-16 05:35:29 +00:00
Deepesh Garg
c07e61953a Merge pull request #32228 from frappe/mergify/bp/version-14-hotfix/pr-32222
fix: abbreviation issue on renaming cost center (backport #32222)
2022-09-16 11:03:14 +05:30
Nabin Hait
2102b83bd9 Merge pull request #32229 from frappe/mergify/bp/version-14-hotfix/pr-32221
fix: consider posting time for internal transfer PO (backport #32221)
2022-09-16 09:39:33 +05:30
Rohit Waghchaure
f370c7b50b fix: consider posting time for internal transfer PO
(cherry picked from commit cb763938dc)
2022-09-16 04:08:04 +00:00
Nabin Hait
f752822bb3 fix: abbreviation issue on renaming cost center
(cherry picked from commit af21a11e1e)
2022-09-16 04:07:44 +00:00
Nabin Hait
af5fb78fe2 Merge pull request #32201 from nabinhait/SaiFi0102-asset-capitalization-v14
feat: Asset Capitalization
2022-09-16 09:34:52 +05:30
Sagar Sharma
85cf30cfa8 Merge branch 'version-14-hotfix' into SaiFi0102-asset-capitalization-v14 2022-09-15 08:17:37 +05:30
Frappe PR Bot
14e5f239ac chore(release): Bumped to Version 14.1.2
## [14.1.2](https://github.com/frappe/erpnext/compare/v14.1.1...v14.1.2) (2022-09-14)

### Bug Fixes

* correct sql output format in CRM patch (backport [#32213](https://github.com/frappe/erpnext/issues/32213)) ([#32214](https://github.com/frappe/erpnext/issues/32214)) ([fed2353](fed23536dc))
2022-09-14 13:52:36 +00:00
mergify[bot]
64aad88684 fix: correct sql output format in CRM patch (backport #32213) (#32215)
fix: correct sql output format in CRM patch (#32213)

(cherry picked from commit 97977cdb4b)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-09-14 19:21:40 +05:30
mergify[bot]
fed23536dc fix: correct sql output format in CRM patch (backport #32213) (#32214)
fix: correct sql output format in CRM patch (#32213)

(cherry picked from commit 97977cdb4b)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-09-14 19:21:04 +05:30
Nabin Hait
f126e88e5e fix: test cases 2022-09-14 18:31:15 +05:30
Nabin Hait
3f8c701083 Merge branch 'SaiFi0102-asset-capitalization-v14' of https://github.com/nabinhait/erpnext into SaiFi0102-asset-capitalization-v14 2022-09-14 17:21:57 +05:30
Nabin Hait
153ef5f164 fix: test cases 2022-09-14 17:20:19 +05:30
Deepesh Garg
cbdc46b89e Merge pull request #32210 from frappe/mergify/bp/version-14-hotfix/pr-32208
fix: Loans pending accrual entries (backport #32208)
2022-09-14 15:34:22 +05:30
Abhinav Raut
b9a249918a fix: pending accrual entries
(cherry picked from commit f2209045f8)
2022-09-14 08:45:47 +00:00
Nabin Hait
fb99d3f1b9 Merge branch 'version-14-hotfix' into SaiFi0102-asset-capitalization-v14 2022-09-14 12:56:47 +05:30
Nabin Hait
3b79e24c7e fix: always set default expense account in company 2022-09-14 12:55:49 +05:30
Sagar Sharma
97b462538a Merge pull request #32206 from frappe/mergify/bp/version-14-hotfix/pr-32205
fix: unknown column error while updating value of maintain-stock in item master (backport #32205)
2022-09-14 11:09:05 +05:30
Sagar Sharma
2fe72af359 test: add test case for item master maintain-stock
(cherry picked from commit bf1fa014f4)
2022-09-14 02:44:55 +00:00
Sagar Sharma
38488c13e6 fix: unknown column error while updating value of maintain-stock in item master
(cherry picked from commit 7b878ea3d8)
2022-09-14 02:44:54 +00:00
Frappe PR Bot
740c17c231 chore(release): Bumped to Version 14.1.1
## [14.1.1](https://github.com/frappe/erpnext/compare/v14.1.0...v14.1.1) (2022-09-13)

### Bug Fixes

* AD not getting copied from SCO while creating a Material Transfer ([#32109](https://github.com/frappe/erpnext/issues/32109)) ([09d8fa4](09d8fa4b5d)), closes [#32106](https://github.com/frappe/erpnext/issues/32106)
* add missing warehouse filter in BOM Stock Calculated report ([4e09203](4e09203ddc))
* conflict ([a408722](a408722983))
* consider Stock Entry purpose while getting total supplied qty ([5af0062](5af006278e))
* customer code max characters limit issue ([#32177](https://github.com/frappe/erpnext/issues/32177)) ([93e5100](93e510023a))
* delete linked payment ledger entries no source doc deletion ([ccd0449](ccd0449006))
* drop old notes column from lead and prospect ([ded6e7f](ded6e7f544))
* **Employee:** shorter tab titles (backport [#32192](https://github.com/frappe/erpnext/issues/32192)) ([#32193](https://github.com/frappe/erpnext/issues/32193)) ([027e66f](027e66f637))
* hide "Return of Components" button in closed SCO (backport [#32130](https://github.com/frappe/erpnext/issues/32130)) ([#32131](https://github.com/frappe/erpnext/issues/32131)) ([7840fed](7840fed7a5))
* import error on bank statement import ([467ee97](467ee97938))
* internal transfer flow ([4bac0da](4bac0daf9b))
* inventory dimension filter's label not showing in the reort ([2d2eef6](2d2eef6b29))
* item wise sales register taxes and charges ([53f235b](53f235b630))
* Migrate old lead notes as per the new format ([28b1d4e](28b1d4e2a3))
* option to start reposting from repost item valuation ([6483195](64831952d5))
* pick_list - picked qty getting set to 1 ([3c3ab89](3c3ab897d5))
* promotional scheme min and max amount configuration ([702c16e](702c16eac4))
* QR Code multi currency issue ([d14a9a1](d14a9a1e89))
* Rate for internal PI have non stock UOM items ([c06e241](c06e241fbc))
* remove EmployeeBoardingController (backport [#32139](https://github.com/frappe/erpnext/issues/32139)) ([#32191](https://github.com/frappe/erpnext/issues/32191)) ([944b8a0](944b8a0ed7))
* remove multiple call to ple creation ([a2285d5](a2285d5e5e))
* reposting not working for internal transferred purchase receipt ([7ce6729](7ce6729100))
* require barcode item barcode ([#32111](https://github.com/frappe/erpnext/issues/32111)) ([404668f](404668fcc9)), closes [#31957](https://github.com/frappe/erpnext/issues/31957)
* required_qty in BOM Stock Calculated report ([af883be](af883be065))
* SCO Supplied Items returned-qty ([4568648](4568648d8c))
* Set filter condition and spell in AR ([174c750](174c7503c9))
* status filter for Subcontracting Order in Stock Entry ([844f120](844f120a56))
* Subcontracting Receipt GL Entries (backport [#31918](https://github.com/frappe/erpnext/issues/31918)) ([#32124](https://github.com/frappe/erpnext/issues/32124)) ([f2ab220](f2ab220ce3))
* **UX:** make Item attachments public by default (backport [#32196](https://github.com/frappe/erpnext/issues/32196)) ([#32197](https://github.com/frappe/erpnext/issues/32197)) ([2c4867c](2c4867cc98))
* validate Subcontracting Order in Stock Entry ([d3cc9d4](d3cc9d4fa6))

### Performance Improvements

* lesser SQL queries and no validation ([7bb5f6e](7bb5f6eb23))
2022-09-13 12:13:00 +00:00
Deepesh Garg
5f4b214217 Merge pull request #32199 from frappe/version-14-hotfix
chore: release v14
2022-09-13 17:41:02 +05:30
Deepesh Garg
5b04bebc0a Merge pull request #32198 from frappe/mergify/bp/version-14-hotfix/pr-32190
fix: item wise sales register taxes and charges (backport #32190)
2022-09-13 16:30:59 +05:30
Nabin Hait
38c31077c9 feat: Asset Capitalization
- manual selection of entry type
- GLE cleanup with smaller functions
- GLE considering periodical inventory
- test cases
2022-09-13 16:21:06 +05:30
Nabin Hait
cd613c6033 fix: merge conflict 2022-09-13 16:20:42 +05:30
Deepesh Garg
2e1fb5255e Merge pull request #32195 from frappe/mergify/bp/version-14-hotfix/pr-32091
fix: Migrate old lead notes as per the new format (backport #32091)
2022-09-13 14:41:35 +05:30
Deepesh Garg
9fd8fffb28 Merge pull request #32166 from frappe/mergify/bp/version-14-hotfix/pr-32144
fix: Rate for internal PI have non stock UOM items (backport #32144)
2022-09-13 14:40:31 +05:30
Maharshi Patel
53f235b630 fix: item wise sales register taxes and charges
i have added separate column for other charges. Instead of adding all values to tax_total, it checks if account_type is tax, and then only it adds to total_tax otherwise it adds to the total_other_charges.

(cherry picked from commit 62163ab3d3)
2022-09-13 08:26:13 +00:00
mergify[bot]
2c4867cc98 fix(UX): make Item attachments public by default (backport #32196) (#32197)
fix(UX): make Item attachments public by default (#32196)

(cherry picked from commit fffc245922)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-09-13 13:41:58 +05:30
Deepesh Garg
dda057d257 chore: Resolve conflicts 2022-09-13 13:40:36 +05:30
Nabin Hait
ded6e7f544 fix: drop old notes column from lead and prospect
(cherry picked from commit 4b13452022)
2022-09-13 06:51:27 +00:00
Nabin Hait
7bb5f6eb23 perf: lesser SQL queries and no validation
Co-authored-by: Sagar Vora <sagar@resilient.tech>
(cherry picked from commit 2a100abef1)
2022-09-13 06:51:27 +00:00
Nabin Hait
28b1d4e2a3 fix: Migrate old lead notes as per the new format
(cherry picked from commit 3abd00f3bb)
2022-09-13 06:51:26 +00:00
mergify[bot]
027e66f637 fix(Employee): shorter tab titles (backport #32192) (#32193)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
2022-09-13 10:36:12 +05:30
mergify[bot]
944b8a0ed7 fix: remove EmployeeBoardingController (backport #32139) (#32191)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2022-09-13 09:50:36 +05:30
rohitwaghchaure
95da66a201 Merge pull request #32183 from frappe/mergify/bp/version-14-hotfix/pr-32181
fix: promotional scheme min and max amount configuration (backport #32181)
2022-09-13 08:36:54 +05:30
Sagar Sharma
9fbf507877 Merge pull request #32189 from frappe/mergify/bp/version-14-hotfix/pr-32179
fix: pick_list - picked qty getting set to 1 (backport #32179)
2022-09-12 22:42:58 +05:30
Ahmad
3c3ab897d5 fix: pick_list - picked qty getting set to 1
(cherry picked from commit 3256e2b8b7)
2022-09-12 16:50:28 +00:00
Sagar Sharma
60b914b2a6 Merge pull request #32185 from frappe/mergify/bp/version-14-hotfix/pr-32150
refactor: BOM Stock Calculated report, fix required-qty (backport #32150)
2022-09-12 19:49:01 +05:30
Sagar Sharma
581c5cbc38 test: add test cases for BOM Stock Calculated report
(cherry picked from commit e1a98c1ff7)
2022-09-12 13:52:05 +00:00
Sagar Sharma
4e09203ddc fix: add missing warehouse filter in BOM Stock Calculated report
(cherry picked from commit 7a968a5f0d)
2022-09-12 13:52:05 +00:00
Sagar Sharma
af883be065 fix: required_qty in BOM Stock Calculated report
(cherry picked from commit 56192daabf)
2022-09-12 13:52:05 +00:00
Sagar Sharma
08f5f81fa8 refactor: BOM Stock Calculated report
(cherry picked from commit 723fa9eebc)
2022-09-12 13:52:05 +00:00
Rohit Waghchaure
702c16eac4 fix: promotional scheme min and max amount configuration
(cherry picked from commit a8fd92ddc1)
2022-09-12 13:01:02 +00:00
rohitwaghchaure
82440e3124 Merge pull request #32180 from frappe/mergify/bp/version-14-hotfix/pr-32177
fix: customer code max characters limit issue (backport #32177)
2022-09-12 18:28:39 +05:30
rohitwaghchaure
93e510023a fix: customer code max characters limit issue (#32177)
(cherry picked from commit 8f787c08a2)
2022-09-12 11:19:42 +00:00
ruthra kumar
0bc61b3b16 Merge pull request #32176 from frappe/mergify/bp/version-14-hotfix/pr-32175
fix: Set filter condition and spell in AR (backport #32175)
2022-09-12 16:01:41 +05:30
Nihantra C. Patel
174c7503c9 fix: Set filter condition and spell in AR
(cherry picked from commit e2b4ae13fa)
2022-09-12 09:31:27 +00:00
mergify[bot]
3083b4c218 chore: correct license text for GPLv3 (backport #32170) (#32171)
chore: correct license text for GPLv3 (#32170)

[skip ci]

(cherry picked from commit a30f38481d)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-09-12 13:57:49 +05:30
Deepesh Garg
c06e241fbc fix: Rate for internal PI have non stock UOM items
(cherry picked from commit 0f655e4430)

# Conflicts:
#	erpnext/controllers/buying_controller.py
2022-09-12 03:40:43 +00:00
Sagar Sharma
a9e9dad66a Merge pull request #32162 from frappe/mergify/bp/version-14-hotfix/pr-32161
refactor: rewrite Process Loss Report queries in QB (backport #32161)
2022-09-11 20:06:52 +05:30
Sagar Sharma
86e5f362e3 refactor: rewrite Process Loss Report queries in QB
(cherry picked from commit 5245928648)
2022-09-11 14:18:22 +00:00
ruthra kumar
7cc98181ec Merge pull request #32157 from frappe/mergify/bp/version-14-hotfix/pr-32137
refactor(minor): delete linked payment ledger entries on source doc deletion (backport #32137)
2022-09-11 13:41:12 +05:30
ruthra kumar
1ea778a77b Merge pull request #32158 from frappe/mergify/bp/version-14-hotfix/pr-32156
fix: remove duplicate call to ple creation (backport #32156)
2022-09-11 12:43:45 +05:30
ruthra kumar
a2285d5e5e fix: remove multiple call to ple creation
(cherry picked from commit 4adc372f9a)
2022-09-11 06:51:50 +00:00
ruthra kumar
ccd0449006 fix: delete linked payment ledger entries no source doc deletion
(cherry picked from commit 70313df531)
2022-09-11 06:37:22 +00:00
Sagar Sharma
793ee83f39 Merge pull request #32154 from frappe/mergify/bp/version-14-hotfix/pr-32153
refactor: rewrite Work Order Stock Report queries in QB (backport #32153)
2022-09-10 17:16:44 +05:30
Sagar Sharma
9203217678 refactor: rewrite Work Order Stock Report queries in QB
(cherry picked from commit d4c4dddfc3)
2022-09-10 11:21:27 +00:00
rohitwaghchaure
3c1c50f171 Merge pull request #32141 from frappe/mergify/bp/version-14-hotfix/pr-32135
fix: reposting not working for internal transferred purchase receipt (backport #32135)
2022-09-10 16:40:51 +05:30
rohitwaghchaure
a408722983 fix: conflict 2022-09-09 22:44:10 +05:30
Deepesh Garg
16502b30a0 Merge pull request #32148 from frappe/mergify/bp/version-14-hotfix/pr-32110
fix: error on bank statement import (backport #32110)
2022-09-09 17:16:58 +05:30
Sagar Sharma
61a1b2b42b Merge pull request #32147 from frappe/mergify/bp/version-14-hotfix/pr-32145
fix: validate Subcontracting Order Status in Stock Entry (backport #32145)
2022-09-09 17:06:28 +05:30
ruthra kumar
467ee97938 fix: import error on bank statement import
Name collision between frameworks file importer's internal log and
doctype fieldname - import_log.

Frameworks internal log overrode, doctypes field which caused failure
in basic fieldtype validation.

(cherry picked from commit a6fbb80b94)
2022-09-09 09:31:25 +00:00
Sagar Sharma
d3cc9d4fa6 fix: validate Subcontracting Order in Stock Entry
(cherry picked from commit 9a3dcb9ad1)
2022-09-09 09:26:08 +00:00
Sagar Sharma
844f120a56 fix: status filter for Subcontracting Order in Stock Entry
(cherry picked from commit 30909a9b79)
2022-09-09 09:26:08 +00:00
Sagar Sharma
8be12f0858 Merge pull request #32143 from frappe/mergify/bp/version-14-hotfix/pr-32102
fix: consider Stock Entry purpose while getting total supplied qty (backport #32102)
2022-09-09 12:35:47 +05:30
Sagar Sharma
5af006278e fix: consider Stock Entry purpose while getting total supplied qty
(cherry picked from commit 2f00413864)
2022-09-09 06:44:26 +00:00
Rohit Waghchaure
7ce6729100 fix: reposting not working for internal transferred purchase receipt
(cherry picked from commit a03b4ce213)

# Conflicts:
#	erpnext/stock/stock_ledger.py
2022-09-09 04:01:23 +00:00
Sagar Sharma
c4c43bc550 Merge pull request #32134 from frappe/mergify/bp/version-14-hotfix/pr-32128
fix: SCO Supplied Items returned-qty (backport #32128)
2022-09-08 19:59:23 +05:30
Sagar Sharma
62741bf744 test: add test case for returned-qty
(cherry picked from commit aea7188304)
2022-09-08 13:31:38 +00:00
Sagar Sharma
4568648d8c fix: SCO Supplied Items returned-qty
(cherry picked from commit ccb2889cac)
2022-09-08 13:31:38 +00:00
mergify[bot]
7840fed7a5 fix: hide "Return of Components" button in closed SCO (backport #32130) (#32131)
fix: hide "Return of Components" button in closed SCO (#32130)

(cherry picked from commit 3585daab95)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-09-08 18:25:27 +05:30
Sagar Sharma
f2ab220ce3 fix: Subcontracting Receipt GL Entries (backport #31918) (#32124)
fix: Subcontracting Receipt GL Entries
2022-09-08 11:18:00 +05:30
rohitwaghchaure
9b6dd7e71f Merge pull request #32120 from rohitwaghchaure/backport-provision-to-manual-reposting
fix: option to start reposting from repost item valuation
2022-09-07 18:05:38 +05:30
Rohit Waghchaure
64831952d5 fix: option to start reposting from repost item valuation 2022-09-07 18:04:43 +05:30
rohitwaghchaure
d53fd8c592 Merge pull request #32119 from rohitwaghchaure/backport-fixed-label-not-showing-for-filter
fix: inventory dimension filter's label not showing in the reort
2022-09-07 18:03:51 +05:30
Rohit Waghchaure
2d2eef6b29 fix: inventory dimension filter's label not showing in the reort 2022-09-07 18:02:41 +05:30
rohitwaghchaure
c313c37006 Merge pull request #32114 from rohitwaghchaure/backport-fixed-internal-transfer-flow
fix: backport fixed internal transfer flow
2022-09-07 15:44:01 +05:30
Rohit Waghchaure
a00bc2967e test: added test case for internal transfer 2022-09-07 14:29:21 +05:30
Rohit Waghchaure
4bac0daf9b fix: internal transfer flow 2022-09-07 14:29:15 +05:30
Sagar Sharma
404668fcc9 fix: require barcode item barcode (#32111)
fix: require barcode item barcode. (#31957)

* fix: require barcode item barcode.

* fix: make supplier mandatory in Item Supplier DocType

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
2022-09-07 14:24:12 +05:30
Sagar Sharma
09d8fa4b5d fix: AD not getting copied from SCO while creating a Material Transfer (#32109)
fix: AD not getting copied from SCO while creating a Material Transfer (#32106)
2022-09-07 12:42:12 +05:30
Deepesh Garg
354aaae587 Merge pull request #32088 from frappe/mergify/bp/version-14-hotfix/pr-32086
fix: QR Code multi currency issue (backport #32086)
2022-09-07 07:46:41 +05:30
Frappe PR Bot
7235c3f88f chore(release): Bumped to Version 14.1.0
# [14.1.0](https://github.com/frappe/erpnext/compare/v14.0.3...v14.1.0) (2022-09-06)

### Bug Fixes

* **Appointment:** create lead notes as child table ([00a73c7](00a73c7a57))
* fetch from parent not working for custom field ([76ae4d8](76ae4d87ca))
* force delete old report docs (backport [#32026](https://github.com/frappe/erpnext/issues/32026)) ([#32027](https://github.com/frappe/erpnext/issues/32027)) ([0e9a1fb](0e9a1fb40e))
* include payment against PO in AR/AP report ([4bbd0ec](4bbd0ec985))
* incorrect import parameter for cancel PDA ([f55881a](f55881aef8))
* key error on consolidated financial report ([4409f11](4409f11282))
* KSA VAT report multi currency amount issue ([b96526e](b96526eefd))
* Loan Interest accruals for 0 rated loans ([1b9082e](1b9082e07b))
* Naming series in Journal Entry Template ([5520c6b](5520c6b2f3))
* not able to make variant item ([f871dd4](f871dd4ef6))
* **pos:** error while consolidating pos invoices ([319ee41](319ee41403))
* remove spaces and order import ([055556b](055556b7f1))
* upgrade process to version-14 when currency opportunity wass not set ([4cb685a](4cb685a326))
* validate available qty for consumption in SCR ([26536da](26536da74b))

### Features

* better Item Price list view ([#31954](https://github.com/frappe/erpnext/issues/31954)) ([784fb47](784fb47197))
* tabbed view for Employee form ([#31940](https://github.com/frappe/erpnext/issues/31940)) ([#32095](https://github.com/frappe/erpnext/issues/32095)) ([3c688df](3c688dfa6d))
* two new filters for gross profit ([60fa421](60fa421409))
2022-09-06 16:04:45 +00:00
Deepesh Garg
15733c14e8 Merge pull request #32101 from frappe/version-14-hotfix
chore: version-14 weekly release
2022-09-06 21:31:39 +05:30
rohitwaghchaure
22b2386aa1 Merge pull request #32097 from rohitwaghchaure/backport-fixed-fetch-from-parent
fix: fetch from parent
2022-09-06 12:59:37 +05:30
Rohit Waghchaure
6ab0637b0b test: test cases for PI and DN 2022-09-06 11:54:56 +05:30
Rohit Waghchaure
76ae4d87ca fix: fetch from parent not working for custom field 2022-09-06 11:54:48 +05:30
Rucha Mahabal
3c688dfa6d feat: tabbed view for Employee form (#31940) (#32095) 2022-09-06 11:04:13 +05:30
Sagar Sharma
8b8d054ded Merge pull request #32090 from frappe/mergify/bp/version-14-hotfix/pr-32082
fix: validate available qty for consumption in SCR (backport #32082)
2022-09-05 14:49:35 +05:30
Sagar Sharma
121ec83562 refactor(test): test_update_reserved_qty_for_subcontracting
(cherry picked from commit a349b58306)
2022-09-05 08:50:29 +00:00
Sagar Sharma
26536da74b fix: validate available qty for consumption in SCR
(cherry picked from commit 4a7add2169)
2022-09-05 08:50:28 +00:00
Nabin Hait
f605564094 Merge pull request #32087 from frappe/mergify/bp/version-14-hotfix/pr-32061
fix(Appointment): create lead notes as child table (backport #32061)
2022-09-05 12:56:00 +05:30
hamzaali15
d14a9a1e89 fix: QR Code multi currency issue
When try to scan qr code on app it is showing correct values for multi currencies because it is not getting base amount

(cherry picked from commit b10a2b87b6)
2022-09-05 07:13:08 +00:00
Sagar Vora
b7e8fbe43f test: dont create lead manually, add coverage for notes
(cherry picked from commit 875ff15109)
2022-09-05 06:56:48 +00:00
Sagar Vora
00a73c7a57 fix(Appointment): create lead notes as child table
(cherry picked from commit 58e553151e)
2022-09-05 06:56:47 +00:00
Deepesh Garg
edb100274b Merge pull request #32080 from frappe/mergify/bp/version-14-hotfix/pr-31982
fix: upgrade process to version-14 when currency opportunity was not set (backport #31982)
2022-09-04 22:39:01 +05:30
Deepesh Garg
6976316ada chore: fix message
(cherry picked from commit 118b0c0f86)
2022-09-04 13:34:32 +00:00
Florian HENRY
357f74a580 chore: better text
(cherry picked from commit d19b664ba9)
2022-09-04 13:34:32 +00:00
Florian HENRY
aedd0397b8 chore: remove debug
(cherry picked from commit ac66538651)
2022-09-04 13:34:32 +00:00
Florian HENRY
4cb685a326 fix: upgrade process to version-14 when currency opportunity wass not set
(cherry picked from commit 9d02fbadb4)
2022-09-04 13:34:31 +00:00
Deepesh Garg
afcc0cbccd Merge pull request #32079 from frappe/mergify/bp/version-14-hotfix/pr-31954
feat: better Item Price list view (backport #31954)
2022-09-04 18:15:25 +05:30
Deepesh Garg
29bca45b1e Merge pull request #32077 from frappe/mergify/bp/version-14-hotfix/pr-32045
fix: Naming series in Journal Entry Template (backport #32045)
2022-09-04 18:14:56 +05:30
HENRY Florian
784fb47197 feat: better Item Price list view (#31954)
* feat: better Item Price list view

(cherry picked from commit 86395c6adb)
2022-09-04 12:39:04 +00:00
Solufyin
5520c6b2f3 fix: Naming series in Journal Entry Template
(cherry picked from commit 2085626390)
2022-09-04 11:05:07 +00:00
Deepesh Garg
f873547447 Merge pull request #32074 from frappe/mergify/bp/version-14-hotfix/pr-31850
feat: two new filters for gross profit (backport #31850)
2022-09-04 16:21:54 +05:30
Deepesh Garg
d65d2f617d Merge pull request #32071 from frappe/mergify/bp/version-14-hotfix/pr-32053
fix: KSA VAT report multi currency amount issue (backport #32053)
2022-09-04 15:58:50 +05:30
Deepesh Garg
0438433a4f chore: Linting Issues
(cherry picked from commit ad8d0efa29)
2022-09-04 10:27:23 +00:00
hrzzz
055556b7f1 fix: remove spaces and order import
(cherry picked from commit 3ef551872a)
2022-09-04 10:27:23 +00:00
hrzzz
60fa421409 feat: two new filters for gross profit
(cherry picked from commit 27891ecb77)
2022-09-04 10:27:23 +00:00
Deepesh Garg
dd2fd12d5f Merge pull request #32073 from frappe/mergify/bp/version-14-hotfix/pr-31822
fix(pos): error while consolidating pos invoices (backport #31822)
2022-09-04 15:57:09 +05:30
Saqib Ansari
319ee41403 fix(pos): error while consolidating pos invoices
(cherry picked from commit 33762dbbac)
2022-09-04 07:56:31 +00:00
hamzaali15
b96526eefd fix: KSA VAT report multi currency amount issue
In KSA VAT report amount is not showing correctly for multi currencies because net_amount field is fetched instead of base_net_amount

(cherry picked from commit 56d8962e40)
2022-09-04 07:43:32 +00:00
rohitwaghchaure
78a992c086 Merge pull request #32066 from frappe/mergify/bp/version-14-hotfix/pr-32065
fix: not able to make variant item (backport #32065)
2022-09-03 11:53:48 +05:30
Rohit Waghchaure
f871dd4ef6 fix: not able to make variant item
(cherry picked from commit 92b0f9cd7e)
2022-09-03 06:23:27 +00:00
ruthra kumar
79ecf7751f Merge pull request #32060 from frappe/mergify/bp/version-14-hotfix/pr-32054
fix: type error on cancellation of Process Deferred Accounting (backport #32054)
2022-09-02 17:10:34 +05:30
ruthra kumar
4698dba402 test: pda document submission and cancellation
(cherry picked from commit 1c385541fa)
2022-09-02 11:15:32 +00:00
ruthra kumar
f55881aef8 fix: incorrect import parameter for cancel PDA
(cherry picked from commit 08f2e4edc3)
2022-09-02 11:15:31 +00:00
ruthra kumar
f1b2ba5a84 Merge pull request #32058 from frappe/mergify/bp/version-14-hotfix/pr-32052
fix: key error on consolidated financial report (backport #32052)
2022-09-02 16:41:08 +05:30
ruthra kumar
4409f11282 fix: key error on consolidated financial report
accounts with same name but different account number will throw key
error on consolidated report

(cherry picked from commit 6e8395cccd)
2022-09-02 10:46:42 +00:00
ruthra kumar
84865a8421 Merge pull request #32055 from frappe/mergify/bp/version-14-hotfix/pr-31942
fix: include payment against PO in AR/AP report (backport #31942)
2022-09-02 14:58:25 +05:30
ruthra kumar
d9632e8138 test: payments against so/po will show up as outstanding amount
1. Class will use FrappeTestCase fixture
2. setup and teardown methods are introduced
3. test for payments against SO

(cherry picked from commit 36f5883dda)
2022-09-02 08:15:36 +00:00
ruthra kumar
4bbd0ec985 fix: include payment against PO in AR/AP report
(cherry picked from commit fdd167cac1)
2022-09-02 08:15:36 +00:00
mergify[bot]
92f8f0ec74 chore: set BOM as default value for Backflush Raw Materials of Subcontract Based On (backport #32048) (#32050)
chore: set BOM as default value for Backflush Raw Materials of Subcontract Based On (#32048)

chore: set BOM as default value for Backflush Raw Materials of Subcontract Based On in Buying Settings
(cherry picked from commit 68907ca783)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-09-02 09:10:17 +05:30
Deepesh Garg
aea484be1f Merge pull request #32032 from frappe/mergify/bp/version-14-hotfix/pr-32030
fix: Loan Interest accruals for 0 rated loans (backport #32030)
2022-08-31 21:04:38 +05:30
Deepesh Garg
52fc10d00c chore: Add check for principal amount
(cherry picked from commit a76d3827ec)
2022-08-30 15:47:11 +00:00
Deepesh Garg
1b9082e07b fix: Loan Interest accruals for 0 rated loans
(cherry picked from commit eefc9b7172)
2022-08-30 15:47:11 +00:00
mergify[bot]
0e9a1fb40e fix: force delete old report docs (backport #32026) (#32027)
fix: force delete old report docs (#32026)

(cherry picked from commit ffa3071d36)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-08-30 15:46:08 +05:30
Frappe PR Bot
32abf67c80 chore(release): Bumped to Version 14.0.3
## [14.0.3](https://github.com/frappe/erpnext/compare/v14.0.2...v14.0.3) (2022-08-30)

### Bug Fixes

* AD not getting copied from SCO while creating a SE (backport [#32004](https://github.com/frappe/erpnext/issues/32004)) ([#32007](https://github.com/frappe/erpnext/issues/32007)) ([cee867f](cee867f941))
* Add docstatus filter for voucher_no in Repost Item Valuation ([309da96](309da96442))
* add validation for PO in Stock Entry (backport [#31974](https://github.com/frappe/erpnext/issues/31974)) ([#31975](https://github.com/frappe/erpnext/issues/31975)) ([4d8ced6](4d8ced6c87))
* default supplier not set in the PP ([4cf9fb0](4cf9fb08e1))
* display amount in account currency if party is supplied ([15915d7](15915d7053))
* Explicitly commit "log_error" since its getting called during GET request (backport [#31952](https://github.com/frappe/erpnext/issues/31952)) ([#31953](https://github.com/frappe/erpnext/issues/31953)) ([362976f](362976fa42))
* gl entries for asset repair ([f0f7afa](f0f7afa669))
* lost quotation not to expired ([8717235](8717235a34))
* material request connection on work order ([193502c](193502ce03))
* **patch:** update sla doctype directly (backport [#32014](https://github.com/frappe/erpnext/issues/32014)) ([#32015](https://github.com/frappe/erpnext/issues/32015)) ([1b5a1cb](1b5a1cbaad))
* permissions for Task Type ([#32016](https://github.com/frappe/erpnext/issues/32016)) ([7ca1beb](7ca1beb15d))
* Purchase Order creation from Sales Order ([39ff0cc](39ff0cc6d8))
* Purposes not set ([94ebfa7](94ebfa765c))
* restrict party types to Supplier/Customer for AR/AP report ([c407d1e](c407d1e51a))
* Rounded total for cash and non trade discount invoices ([6c4fcd8](6c4fcd80c6))
* Route condition set for stock ledger (backport [#31935](https://github.com/frappe/erpnext/issues/31935)) ([#31945](https://github.com/frappe/erpnext/issues/31945)) ([a7d23ab](a7d23abc2f))
2022-08-30 09:24:00 +00:00
Deepesh Garg
52b42e9492 Merge pull request #32025 from frappe/version-14-hotfix
chore: weekly version-14 release
2022-08-30 14:52:11 +05:30
Deepesh Garg
77ac7f06d4 Merge pull request #32021 from frappe/mergify/bp/version-14-hotfix/pr-32005
fix: lost quotation not to expired (backport #32005)
2022-08-30 11:08:40 +05:30
MOHAMMED NIYAS
8717235a34 fix: lost quotation not to expired
(cherry picked from commit 69ffef8c0e)
2022-08-30 05:03:44 +00:00
Deepesh Garg
670426f428 Merge pull request #32017 from frappe/mergify/bp/version-14-hotfix/pr-32016
fix: permissions for Task Type (backport #32016)
2022-08-30 08:38:14 +05:30
mergify[bot]
1b5a1cbaad fix(patch): update sla doctype directly (backport #32014) (#32015)
fix(patch): update sla doctype directly (#32014)

fix: update sla doctype directly
(cherry picked from commit 2d41704424)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-08-29 22:17:39 +05:30
Raffael Meyer
7ca1beb15d fix: permissions for Task Type (#32016)
(cherry picked from commit 73f4d5931d)
2022-08-29 16:27:00 +00:00
ruthra kumar
c660db145b Merge pull request #32013 from frappe/mergify/bp/version-14-hotfix/pr-32009
refactor: readd remarks field to payment ledger (backport #32009)
2022-08-29 20:21:43 +05:30
ruthra kumar
0aaf9c4f05 chore: add remarks migration to patches.txt
(cherry picked from commit d522f13d55)
2022-08-29 19:52:33 +05:30
ruthra kumar
b3b0272ec9 chore: patch for migrating remarks to payment ledger
(cherry picked from commit 3a6b095ed4)
2022-08-29 14:19:46 +00:00
ruthra kumar
f7898b4954 refactor: re-add remarks field to payment ledger and AR/AP report
(cherry picked from commit 5782c4469a)
2022-08-29 14:19:46 +00:00
Deepesh Garg
b067eae38c Merge pull request #32008 from frappe/mergify/bp/version-14-hotfix/pr-32006
fix: Rounded total for cash and non trade discount invoices (backport #32006)
2022-08-29 17:46:14 +05:30
mergify[bot]
cee867f941 fix: AD not getting copied from SCO while creating a SE (backport #32004) (#32007)
fix: AD not getting copied from SCO while creating a SE (#32004)

(cherry picked from commit 9dbaaa33f5)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-29 15:50:22 +05:30
Deepesh Garg
6c4fcd80c6 fix: Rounded total for cash and non trade discount invoices
(cherry picked from commit 318da16b99)
2022-08-29 10:06:08 +00:00
Deepesh Garg
11657effa5 Merge pull request #31970 from frappe/mergify/bp/version-14-hotfix/pr-31955
chore: update french translation (backport #31955)
2022-08-28 11:32:54 +05:30
Deepesh Garg
d3b7942f32 Merge pull request #31979 from frappe/mergify/bp/version-14-hotfix/pr-31977
fix: restrict party types to Supplier/Customer for AR/AP report (backport #31977)
2022-08-28 11:25:00 +05:30
mergify[bot]
f407c972d1 chore: allow return of components in SCO (backport #31994) (#31995)
chore: allow return of components in SCO (#31994)

chore: allow return of components in sco
(cherry picked from commit af5cbc881f)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-26 23:15:20 +05:30
Deepesh Garg
0b72295fa2 Merge pull request #31990 from frappe/mergify/bp/version-14-hotfix/pr-31988
chore: remove precision on discount_percentage of Sales Invoice Item (backport #31988)
2022-08-26 18:09:26 +05:30
Deepesh Garg
204b6c0272 Merge pull request #31973 from frappe/mergify/bp/version-14-hotfix/pr-31943
fix: gl entries for asset repair (backport #31943)
2022-08-26 17:34:57 +05:30
Deepesh Garg
1118e25b6d Merge pull request #31985 from frappe/mergify/bp/version-14-hotfix/pr-31984
fix: Purchase Order creation from Sales Order (backport #31984)
2022-08-26 16:05:01 +05:30
ruthra kumar
9b9df70632 chore: remove precision on discount_percentage of Sales Invoice Item
(cherry picked from commit c42fef541a)
2022-08-26 10:34:50 +00:00
Solufyin
39ff0cc6d8 fix: Purchase Order creation from Sales Order
(cherry picked from commit bd4b4ddd8b)
2022-08-26 06:31:51 +00:00
ruthra kumar
c407d1e51a fix: restrict party types to Supplier/Customer for AR/AP report
(cherry picked from commit 6aa8fd0f7b)
2022-08-25 10:50:46 +00:00
mergify[bot]
4d8ced6c87 fix: add validation for PO in Stock Entry (backport #31974) (#31975)
fix: add validation for PO in Stock Entry (#31974)

(cherry picked from commit 8566832dd5)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-25 15:29:48 +05:30
Deepesh Garg
4f832678cf chore: fix against account
(cherry picked from commit c1f6dd46d1)
2022-08-25 07:55:08 +00:00
Nabin Hait
f0f7afa669 fix: gl entries for asset repair
(cherry picked from commit b4a2eb2e65)
2022-08-25 07:55:08 +00:00
Florian HENRY
cb6cbf7818 chore: update fr translation
(cherry picked from commit 299da5d596)
2022-08-25 07:37:06 +00:00
Florian HENRY
3aec1175df chore: update fr translation
(cherry picked from commit 1f6f2747d4)
2022-08-25 07:37:06 +00:00
Florian HENRY
c645995ae3 chore: update french translation
(cherry picked from commit 264f98af14)
2022-08-25 07:37:05 +00:00
rohitwaghchaure
f772c17f3f Merge pull request #31969 from frappe/mergify/bp/version-14-hotfix/pr-31967
fix: material request connection on work order (backport #31967)
2022-08-25 12:30:25 +05:30
Rohit Waghchaure
193502ce03 fix: material request connection on work order
(cherry picked from commit 9ab10def49)
2022-08-25 06:59:56 +00:00
rohitwaghchaure
abd637a238 Merge pull request #31968 from frappe/mergify/bp/version-14-hotfix/pr-31966
fix: default supplier not set in the PP (backport #31966)
2022-08-25 12:26:47 +05:30
Rohit Waghchaure
4cf9fb08e1 fix: default supplier not set in the PP
(cherry picked from commit 5fd468d9ec)
2022-08-25 06:55:46 +00:00
Deepesh Garg
e4ca20654f Merge pull request #31964 from frappe/mergify/bp/version-14-hotfix/pr-31909
fix: Add docstatus filter for voucher_no in Repost Item Valuation (backport #31909)
2022-08-25 11:25:02 +05:30
Deepesh Garg
bc873939eb Merge pull request #31959 from frappe/mergify/bp/version-14-hotfix/pr-31934
refactor: disable discount accounting on Buying module(PI) (backport #31934)
2022-08-25 11:06:58 +05:30
Deepesh Garg
c6d3e9f432 Merge pull request #31958 from frappe/mergify/bp/version-14-hotfix/pr-31950
fix: display amount in account currency if party is supplied (backport #31950)
2022-08-25 11:06:42 +05:30
rohitwaghchaure
54f4504df6 Merge pull request #31963 from frappe/mergify/bp/version-14-hotfix/pr-31951
fix: Purposes not set in Maintenance Visit (backport #31951)
2022-08-25 10:50:50 +05:30
Sagar Sharma
309da96442 fix: Add docstatus filter for voucher_no in Repost Item Valuation
(cherry picked from commit 520306dc87)
2022-08-25 05:20:26 +00:00
Rohit Waghchaure
94ebfa765c fix: Purposes not set
(cherry picked from commit f9a7b31b5b)
2022-08-25 05:19:53 +00:00
ruthra kumar
2577747c5c test: remove discount accounting tests
(cherry picked from commit 277ef04b60)
2022-08-25 05:04:46 +00:00
ruthra kumar
fb387426d6 refactor: disable discount accounting on Buying module(PI)
(cherry picked from commit a956e20f29)
2022-08-25 05:04:46 +00:00
ruthra kumar
15915d7053 fix: display amount in account currency if party is supplied
(cherry picked from commit e5b04d54ff)
2022-08-25 05:04:27 +00:00
mergify[bot]
362976fa42 fix: Explicitly commit "log_error" since its getting called during GET request (backport #31952) (#31953)
fix: Explicitly commit "log_error" since its getting called during GET request (#31952)

(cherry picked from commit 122f1c0ced)

Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
2022-08-24 18:26:08 +05:30
mergify[bot]
a7d23abc2f fix: Route condition set for stock ledger (backport #31935) (#31945)
fix: Route condition set for stock ledger (#31935)

(cherry picked from commit 0e26df331c)

Co-authored-by: Solufyin <34390782+Solufyin@users.noreply.github.com>
2022-08-24 13:30:25 +05:30
mergify[bot]
4b609322ba chore: add Work Order test dependencies (backport #31936) (#31937)
chore: add Work Order test dependencies (#31936)

(cherry picked from commit fe73d55f70)

Co-authored-by: HENRY Florian <florian.henry@open-concept.pro>
2022-08-23 16:09:26 +05:30
Frappe PR Bot
5778f227ee chore(release): Bumped to Version 14.0.2
## [14.0.2](https://github.com/frappe/erpnext/compare/v14.0.1...v14.0.2) (2022-08-23)

### Bug Fixes

* additional-cost in items table ([da69cc5](da69cc5477))
* base_amount and exchange_rate in additional-cost table ([d48487a](d48487ada2))
* Cash and non trade discount calculation ([b6d2de2](b6d2de2cc1))
* don't allow to create SCR directly (backport [#31924](https://github.com/frappe/erpnext/issues/31924)) ([#31926](https://github.com/frappe/erpnext/issues/31926)) ([0bfb774](0bfb774bdf))
* incorrect buying amount in Gross Profit rpt ([981add9](981add9b6f))
* incorrect tax amt due to different exchange rate in PR and PI ([a767326](a76732613e))
* limited options for no-of-employees in crm ([735a608](735a60807a))
* make rate field read-only in subcontracting receipt item (backport [#31905](https://github.com/frappe/erpnext/issues/31905)) ([#31906](https://github.com/frappe/erpnext/issues/31906)) ([6be77d5](6be77d5729))
* map old data as per new options of no-of-employees ([35e9bfc](35e9bfca38))
* **pos:** edge case while closing pos ([#31892](https://github.com/frappe/erpnext/issues/31892)) ([200a971](200a971743))
* process loan interest accrual ([66e5202](66e5202642))
* recalculate rate of items based on "Recalculate Rate" checkbox ([9e60dd3](9e60dd32e8))
* TDS calculation for advance payment ([78b39d6](78b39d6ca4))
* term loan interest calculation ([8fdbbf3](8fdbbf374d))
* test "test_pending_and_received_qty" ([e099e10](e099e10c8e))
* Test cases ([3afb625](3afb625ff8))
* Unable to make payment entry against Fees using education app ([1aa96de](1aa96defda))

### Performance Improvements

* use `create_custom_fields` ([#31853](https://github.com/frappe/erpnext/issues/31853)) ([6656d23](6656d23e45))
2022-08-23 05:28:37 +00:00
Deepesh Garg
7ac75aab1a Merge pull request #31932 from frappe/version-14-hotfix
chore: weekly version 14 release
2022-08-23 10:57:10 +05:30
Deepesh Garg
e831b6e054 Merge pull request #31930 from frappe/mergify/bp/version-14-hotfix/pr-31910
fix: Cash and non trade discount calculation (backport #31910)
2022-08-23 10:36:15 +05:30
Deepesh Garg
856a64b77c chore: Linting issues
(cherry picked from commit 1cb7ae16ab)
2022-08-23 04:49:57 +00:00
Deepesh Garg
3afb625ff8 fix: Test cases
(cherry picked from commit ae3dce0cbd)
2022-08-23 04:49:57 +00:00
Deepesh Garg
b6d2de2cc1 fix: Cash and non trade discount calculation
(cherry picked from commit 3b15966cc9)
2022-08-23 04:49:57 +00:00
mergify[bot]
0bfb774bdf fix: don't allow to create SCR directly (backport #31924) (#31926)
fix: don't allow to create SCR directly (#31924)

(cherry picked from commit bf5c43322a)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-22 18:38:21 +05:30
Deepesh Garg
d7a8db04a1 Merge pull request #31916 from frappe/mergify/bp/version-14-hotfix/pr-31856
fix: incorrect tax amt due to different exchange rate in PR and PI (backport #31856)
2022-08-22 10:57:53 +05:30
Deepesh Garg
2d5ae811d2 Merge pull request #31917 from frappe/mergify/bp/version-14-hotfix/pr-31871
fix: incorrect buying amount in Gross Profit rpt (backport #31871)
2022-08-22 10:56:47 +05:30
Deepesh Garg
be9607e27b Merge pull request #31915 from frappe/mergify/bp/version-14-hotfix/pr-31894
fix: TDS calculation for advance payment (backport #31894)
2022-08-22 10:32:04 +05:30
Deepesh Garg
fd45a7afbe Merge pull request #31818 from frappe/mergify/bp/version-14-hotfix/pr-31816
fix: limited options for no-of-employees in the crm documents (backport #31816)
2022-08-22 10:31:06 +05:30
Deepesh Garg
0c73af6ee2 Merge pull request #31809 from frappe/mergify/bp/version-14-hotfix/pr-31801
Tds report (backport #31801)
2022-08-22 10:30:42 +05:30
ruthra kumar
981add9b6f fix: incorrect buying amount in Gross Profit rpt
(cherry picked from commit 967dd398e7)
2022-08-22 05:00:26 +00:00
Deepesh Garg
8f5736c500 Merge pull request #31832 from frappe/mergify/bp/version-14-hotfix/pr-31830
fix: Unable to make payment entry against Fees using education app (backport #31830)
2022-08-22 10:29:26 +05:30
Deepesh Garg
052f7c3345 Merge pull request #31827 from frappe/mergify/bp/version-14-hotfix/pr-31799
fix: process loan interest accrual (backport #31799)
2022-08-22 10:28:50 +05:30
Deepesh Garg
fa4a40812c Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-31816 2022-08-22 09:09:16 +05:30
ruthra kumar
a76732613e fix: incorrect tax amt due to different exchange rate in PR and PI
(cherry picked from commit 5fd0770372)
2022-08-22 03:38:43 +00:00
Deepesh Garg
dd602989a8 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-31801 2022-08-22 09:00:36 +05:30
Maharshi Patel
78b39d6ca4 fix: TDS calculation for advance payment
"against_voucher": ["is", "not set"] was used in query due to which if TDS was added on "advance" payment vouchers and then reconciled against purchase invoice. it will not find those vouchers and consider this as first-time threshold due to which it will calculate Tax for all transactions.

(cherry picked from commit a452143782)
2022-08-22 03:30:04 +00:00
Deepesh Garg
77fa64e100 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-31799 2022-08-22 08:59:57 +05:30
Deepesh Garg
ac04fc60ef Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-31830 2022-08-22 08:59:31 +05:30
mergify[bot]
6be77d5729 fix: make rate field read-only in subcontracting receipt item (backport #31905) (#31906)
fix: make rate field read-only in subcontracting receipt item (#31905)

(cherry picked from commit 588ca68171)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-20 19:28:21 +05:30
Sagar Sharma
37e5b93e2d Merge pull request #31900 from frappe/mergify/bp/version-14-hotfix/pr-31899
fix: repost item valuation for subcontracting receipt  (backport #31899)
2022-08-19 22:41:28 +05:30
Sagar Sharma
19d29d1861 chore: add option for "Subcontracting Receipt" in "Voucher Type"
(cherry picked from commit f92f3e0208)
2022-08-19 16:31:53 +00:00
Sagar Sharma
0db912998a chore: allow subcontracting receipt backdated entry
(cherry picked from commit f8c11847bb)
2022-08-19 16:31:53 +00:00
mergify[bot]
200a971743 fix(pos): edge case while closing pos (#31892) 2022-08-19 17:11:13 +05:30
Sagar Sharma
c0f0986539 Merge pull request #31897 from frappe/mergify/bp/version-14-hotfix/pr-31890
fix: additional-costs in SCO and SCR (backport #31890)
2022-08-19 15:58:06 +05:30
Sagar Sharma
376293326b chore: conflicts 2022-08-19 15:30:50 +05:30
Sagar Sharma
9a29e3c9f2 chore: add test for additional-cost
(cherry picked from commit c247cf728c)
2022-08-19 09:56:52 +00:00
Sagar Sharma
e099e10c8e fix: test "test_pending_and_received_qty"
(cherry picked from commit addd7347d8)
2022-08-19 09:56:52 +00:00
Sagar Sharma
3b222339b8 chore: add additional-cost table in SCR
(cherry picked from commit 256b4245d5)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
2022-08-19 09:56:51 +00:00
Sagar Sharma
9e60dd32e8 fix: recalculate rate of items based on "Recalculate Rate" checkbox
(cherry picked from commit 2fc6833684)
2022-08-19 09:56:51 +00:00
Sagar Sharma
7ff5414571 chore: move "set_missing_values_in_additional_costs" from SCO to SC"
(cherry picked from commit ea82fe5bc2)
2022-08-19 09:56:50 +00:00
Sagar Sharma
d48487ada2 fix: base_amount and exchange_rate in additional-cost table
(cherry picked from commit eabd3135f0)
2022-08-19 09:56:50 +00:00
Sagar Sharma
da69cc5477 fix: additional-cost in items table
(cherry picked from commit d7ed4093d8)
2022-08-19 09:56:50 +00:00
Sagar Vora
b2a720d847 Merge pull request #31891 from frappe/mergify/bp/version-14-hotfix/pr-31853
perf: use `create_custom_fields` (backport #31853)
2022-08-18 15:57:59 +00:00
Sagar Vora
6656d23e45 perf: use create_custom_fields (#31853)
* perf: use `create_custom_fields`

* fix: default must be a string

(cherry picked from commit aafb735283)
2022-08-18 15:31:42 +00:00
mergify[bot]
ae5c05081d chore: remove unwanted field "provisional_expense_account" from SCR (backport #31847) (#31886)
chore: remove unwanted field "provisional_expense_account" from SCR (#31847)

(cherry picked from commit 7e88eb549f)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-18 17:41:01 +05:30
Frappe PR Bot
028e939cca chore(release): Bumped to Version 14.0.1
## [14.0.1](https://github.com/frappe/erpnext/compare/v14.0.0...v14.0.1) (2022-08-18)

### Bug Fixes

* add asset repair to accounting dimension list ([65bb1d8](65bb1d8cc2))
* Add dimension section in subcontracting doctypes (backport [#31849](https://github.com/frappe/erpnext/issues/31849)) ([#31877](https://github.com/frappe/erpnext/issues/31877)) ([50ad612](50ad612453))
* check item_code in all rows of po_items (backport [#31741](https://github.com/frappe/erpnext/issues/31741)) ([#31842](https://github.com/frappe/erpnext/issues/31842)) ([82f1dd2](82f1dd268d))
* contact search in request for quotation (backport [#31828](https://github.com/frappe/erpnext/issues/31828)) ([#31840](https://github.com/frappe/erpnext/issues/31840)) ([74664a3](74664a34c0))
* delete custom fields on deletion of inventory dimension ([354a9d6](354a9d6169))
* **ecommerce:** remove query to non-existing field (backport [#31771](https://github.com/frappe/erpnext/issues/31771)) ([#31773](https://github.com/frappe/erpnext/issues/31773)) ([8737c10](8737c10ce4))
* getting error to show sales invoice group or print rep… (backport [#31756](https://github.com/frappe/erpnext/issues/31756)) ([#31767](https://github.com/frappe/erpnext/issues/31767)) ([53b9d61](53b9d61c46))
* incorrect produced-qty in production-plan-item (backport [#31706](https://github.com/frappe/erpnext/issues/31706)) ([#31861](https://github.com/frappe/erpnext/issues/31861)) ([010a0ca](010a0ca0a9))
* incorrect rate in BOM exploded items (backport [#31513](https://github.com/frappe/erpnext/issues/31513)) ([#31864](https://github.com/frappe/erpnext/issues/31864)) ([abe1894](abe18945a6))
* intercompany SO throws exception ([1d1f12f](1d1f12f949))
* linter (backport [#31763](https://github.com/frappe/erpnext/issues/31763)) ([#31766](https://github.com/frappe/erpnext/issues/31766)) ([3092131](3092131913))
* Make expense account editable in Purchase Receipt Item (backport [#31730](https://github.com/frappe/erpnext/issues/31730)) ([#31878](https://github.com/frappe/erpnext/issues/31878)) ([b637d4d](b637d4d5f1))
* Make expense account editable in Subcontracting Receipt Item (backport [#31848](https://github.com/frappe/erpnext/issues/31848)) ([#31870](https://github.com/frappe/erpnext/issues/31870)) ([aa5aaa1](aa5aaa113e))
* not able to issue expired batches ([ce5fc5b](ce5fc5b457))
* posting_date of linked vouchers should not affect outstanding ([0faa7b0](0faa7b0432))
* **projects:** Add missing comma ([623f56a](623f56a95c))
* set `billing_address` for purchases in `get_party_details` ([5a28ba8](5a28ba8537))
* set `company_address` for purchases in `party.js` ([4c82533](4c82533239))
* specify allowed doctype in queries (backport [#31761](https://github.com/frappe/erpnext/issues/31761)) ([#31764](https://github.com/frappe/erpnext/issues/31764)) ([6dce122](6dce122825))
* Transit filter for Default Target Warehouse in SE (backport [#31839](https://github.com/frappe/erpnext/issues/31839)) ([#31873](https://github.com/frappe/erpnext/issues/31873)) ([04d3571](04d3571dd9))
2022-08-18 08:30:58 +00:00
Deepesh Garg
216cb9b07b Merge pull request #31881 from frappe/version-14-hotfix
chore: weekly version-14 release
2022-08-18 13:59:28 +05:30
mergify[bot]
50ad612453 fix: Add dimension section in subcontracting doctypes (backport #31849) (#31877)
* fix: Add dimension section in subcontracting doctypes (#31849)

(cherry picked from commit 8704ca783d)

# Conflicts:
#	erpnext/patches.txt

* chore: conflicts

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-18 12:04:07 +05:30
Deepesh Garg
6b71af9008 Merge pull request #31883 from frappe/mergify/bp/version-14-hotfix/pr-31875
fix(projects): Add missing comma (backport #31875)
2022-08-18 11:20:55 +05:30
Deepesh Garg
3c8412efdb Merge pull request #31796 from frappe/mergify/bp/version-14-hotfix/pr-31737
fix: incorrect invoice outstanding in reconciliation tool (backport #31737)
2022-08-18 11:20:26 +05:30
Aditya Hase
623f56a95c fix(projects): Add missing comma
Added with https://github.com/frappe/erpnext/pull/31360

(cherry picked from commit d38778e400)
2022-08-18 05:49:58 +00:00
mergify[bot]
b637d4d5f1 fix: Make expense account editable in Purchase Receipt Item (backport #31730) (#31878)
fix: Make expense account editable in Purchase Receipt Item (#31730)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
(cherry picked from commit 1a6508972e)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2022-08-18 11:16:15 +05:30
rohitwaghchaure
601bc64618 Merge pull request #31869 from frappe/mergify/bp/version-14-hotfix/pr-31860
fix: delete custom fields on deletion of inventory dimension (backport #31860)
2022-08-17 21:45:57 +05:30
mergify[bot]
04d3571dd9 fix: Transit filter for Default Target Warehouse in SE (backport #31839) (#31873)
fix: Transit filter for Default Target Warehouse in SE (#31839)

(cherry picked from commit f1a612245c)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-17 17:26:15 +05:30
mergify[bot]
aa5aaa113e fix: Make expense account editable in Subcontracting Receipt Item (backport #31848) (#31870)
fix: Make expense account editable in Subcontracting Receipt Item (#31848)

(cherry picked from commit 2d04e71412)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-17 16:31:18 +05:30
Rohit Waghchaure
354a9d6169 fix: delete custom fields on deletion of inventory dimension
(cherry picked from commit 0b39a0123e)
2022-08-17 10:14:14 +00:00
rohitwaghchaure
c7e2217c92 Merge pull request #31867 from frappe/mergify/bp/version-14-hotfix/pr-31863
fix: not able to issue expired batches (backport #31863)
2022-08-17 15:43:32 +05:30
Rohit Waghchaure
ce5fc5b457 fix: not able to issue expired batches
(cherry picked from commit 795c94384a)
2022-08-17 09:16:57 +00:00
mergify[bot]
abe18945a6 fix: incorrect rate in BOM exploded items (backport #31513) (#31864)
fix: incorrect rate in BOM exploded items (#31513)

(cherry picked from commit 313625c349)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-17 14:37:40 +05:30
mergify[bot]
010a0ca0a9 fix: incorrect produced-qty in production-plan-item (backport #31706) (#31861)
fix: incorrect produced-qty in production-plan-item (#31706)

(cherry picked from commit 538cd6fdcf)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-17 13:46:33 +05:30
mergify[bot]
74664a34c0 fix: contact search in request for quotation (backport #31828) (#31840)
fix: contact search in request for quotation (#31828)

(cherry picked from commit e5e88bb9f1)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-15 12:19:59 +05:30
mergify[bot]
82f1dd268d fix: check item_code in all rows of po_items (backport #31741) (#31842)
fix: check item_code in all rows of po_items (#31741)

fix: check the item code in each row of PO items
(cherry picked from commit 0047e18a9b)

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2022-08-14 16:00:54 +05:30
Deepesh Garg
1aa96defda fix: Unable to make payment entry against Fees using education app
(cherry picked from commit 79ac50d0f7)
2022-08-11 15:17:42 +00:00
Abhinav Raut
8fdbbf374d fix: term loan interest calculation
(cherry picked from commit 534d7ce64b)
2022-08-11 10:18:24 +00:00
Abhinav Raut
66e5202642 fix: process loan interest accrual
(cherry picked from commit 9ef8d5c5c3)
2022-08-11 10:18:24 +00:00
Nabin Hait
35e9bfca38 fix: map old data as per new options of no-of-employees
(cherry picked from commit 909945c0ac)
2022-08-09 15:18:25 +00:00
Nabin Hait
735a60807a fix: limited options for no-of-employees in crm
(cherry picked from commit 7ecd67605f)
2022-08-09 15:18:25 +00:00
mergify[bot]
c3fd802351 refactor: use browser native lazy loading (backport #31814) (#31815)
refactor: use browser native lazy loading (#31814)

(cherry picked from commit 08d7c48dc7)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-08-09 18:51:27 +05:30
Deepesh Garg
6046f8bc5e Merge pull request #31798 from frappe/mergify/bp/version-14-hotfix/pr-31779
Bug add accouting dimension in asset repair (backport #31779)
2022-08-09 17:58:35 +05:30
Akash Krishna
d7db8ed12e Tds report (#31801)
* fix: TDS Computation Summary Report not loading, too many values to unpack

(cherry picked from commit 32b30bc5de)
2022-08-09 12:12:33 +00:00
Sagar Vora
adcd21724b Merge pull request #31793 from frappe/mergify/bp/version-14-hotfix/pr-31733
fix: set `billing_address` for purchases in `get_party_details` (backport #31733)
2022-08-08 12:09:43 +00:00
ruthra kumar
04bdff736b chore: patch for creating existing dimensions in asset repair
(cherry picked from commit 80f508c4b1)
2022-08-08 11:10:25 +00:00
ruthra kumar
65bb1d8cc2 fix: add asset repair to accounting dimension list
(cherry picked from commit 452584c4bd)
2022-08-08 11:10:25 +00:00
Deepesh Garg
ebf766cf62 Merge pull request #31794 from frappe/mergify/bp/version-14-hotfix/pr-31777
fix: intercompany SO created from Purchase Order throws exception (backport #31777)
2022-08-08 16:36:46 +05:30
ruthra kumar
80bf47170f test: posting_date should not affect outstanding amount calculation
(cherry picked from commit ef312b8fc4)
2022-08-08 10:59:05 +00:00
ruthra kumar
0faa7b0432 fix: posting_date of linked vouchers should not affect outstanding
posting_date filter should not be applied for linked vouchers.

(cherry picked from commit 5f1562c5b2)
2022-08-08 10:59:05 +00:00
ruthra kumar
1d1f12f949 fix: intercompany SO throws exception
(cherry picked from commit af0a353b79)
2022-08-08 10:36:55 +00:00
Sagar Vora
4c82533239 fix: set company_address for purchases in party.js
(cherry picked from commit d05082987f)
2022-08-08 07:31:44 +00:00
Sagar Vora
5a28ba8537 fix: set billing_address for purchases in get_party_details
(cherry picked from commit a3625b3817)
2022-08-08 07:31:44 +00:00
mergify[bot]
8737c10ce4 fix(ecommerce): remove query to non-existing field (backport #31771) (#31773)
fix(ecommerce): remove query to non-existing field (#31771)

(cherry picked from commit 17b9bfd249)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-08-03 16:49:16 +05:30
Ankush Menat
2defb89962 ci: fix automated release regex (#31770)
[skip ci]
2022-08-03 16:15:32 +05:30
mergify[bot]
53b9d61c46 fix: getting error to show sales invoice group or print rep… (backport #31756) (#31767)
fix: getting error to show sales invoice group or print rep… (#31756)

fix: formatter getting error to show sales invoice group or print report.

1 - When I view the Gross Profit report in Sales Invoice mode, the table is all broken.
Error on browser console:
TypeError: Cannot read properties of undefined (reading 'indent')

2 - When I try to print, no matter the Group (Sales Invoice, Item Code, Item Group...) nothing happens. in browser log console I have the following error:
TypeError: Cannot read properties of undefined (reading 'content')

i fixed both errors and all working perfectly.

(cherry picked from commit ea88451875)

Co-authored-by: HarryPaulo <paulo_fabris@hotmail.com>
2022-08-03 11:22:54 +05:30
mergify[bot]
3092131913 fix: linter (backport #31763) (#31766)
fix: linter (#31763)

(cherry picked from commit 9c580dde39)

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
2022-08-03 11:17:44 +05:30
mergify[bot]
6dce122825 fix: specify allowed doctype in queries (backport #31761) (#31764)
fix: specify allowed doctype in queries (#31761)

(cherry picked from commit 9baa222976)

Co-authored-by: Sagar Vora <sagar@resilient.tech>
2022-08-03 11:16:41 +05:30
Ankush Menat
248cc48842 ci: setup releases for v14 (#31759)
[skip ci]
2022-08-02 12:11:36 +05:30
Deepesh Garg
ebd8f2f45b chore: verison bump and change log 2022-08-01 22:29:34 +05:30
4273 changed files with 800796 additions and 2583506 deletions

View File

@@ -1,12 +0,0 @@
reviews:
auto_review:
ignore_title_keywords:
- "sync translations"
- "update POT file"
- "style: "
review_status: false
poem: false
collapse_walkthrough: true
sequence_diagrams: false
changed_files_summary: false
high_level_summary: false

View File

@@ -9,13 +9,6 @@ trim_trailing_whitespace = true
charset = utf-8
# python, js indentation settings
[{*.py,*.js,*.vue,*.css,*.scss,*.html}]
[{*.py,*.js}]
indent_style = tab
indent_size = 4
max_line_length = 110
# JSON files - mostly doctype schema files
[{*.json}]
insert_final_newline = false
indent_style = space
indent_size = 1

View File

@@ -2,32 +2,65 @@
"env": {
"browser": true,
"node": true,
"es2022": true
"es6": true
},
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
"indent": "off",
"brace-style": "off",
"no-mixed-spaces-and-tabs": "off",
"no-useless-escape": "off",
"space-unary-ops": ["error", { "words": true }],
"linebreak-style": "off",
"quotes": ["off"],
"semi": "off",
"camelcase": "off",
"no-unused-vars": "off",
"no-console": ["warn"],
"no-extra-boolean-cast": ["off"],
"no-control-regex": ["off"]
"indent": [
"error",
"tab",
{ "SwitchCase": 1 }
],
"brace-style": [
"error",
"1tbs"
],
"space-unary-ops": [
"error",
{ "words": true }
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"off"
],
"semi": [
"warn",
"always"
],
"camelcase": [
"off"
],
"no-unused-vars": [
"warn"
],
"no-redeclare": [
"warn"
],
"no-console": [
"warn"
],
"no-extra-boolean-cast": [
"off"
],
"no-control-regex": [
"off"
],
"space-before-blocks": "warn",
"keyword-spacing": "warn",
"comma-spacing": "warn",
"key-spacing": "warn"
},
"root": true,
"globals": {
"frappe": true,
"Vue": true,
"SetVueGlobals": true,
"erpnext": true,
"hub": true,
"$": true,
@@ -64,10 +97,8 @@
"is_null": true,
"in_list": true,
"has_common": true,
"posthog": true,
"has_words": true,
"validate_email": true,
"open_web_template_values_editor": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
@@ -123,8 +154,8 @@
"before": true,
"beforeEach": true,
"onScan": true,
"html2canvas": true,
"extend_cscript": true,
"localforage": true,
"Plaid": true
"localforage": true
}
}

View File

@@ -28,26 +28,4 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
494bd9ef78313436f0424b918f200dab8fc7c20b
# bulk format python code with black
baec607ff5905b1c67531096a9cf50ec7ff00a5d
# bulk refactor with sourcery
eb9ee3f79b94e594fc6dfa4f6514580e125eee8c
# js formatting
ec74a5e56617bbd76ac402451468fd4668af543d
# ruff formatting
a308792ee7fda18a681e9181f4fd00b36385bc23
# noisy typing refactoring of get_item_details
7b7211ac79c248a79ba8a999ff34e734d874c0ae
d827ed21adc7b36047e247cbb0dc6388d048a7f9
# `frappe.flags.in_test` => `frappe.in_test`
7a482a69985c952de0e8193c9d4e086aee65ee6d
# these commits actually changed something valuable
# but they have a lot of whitespace changes that make blame noisy
# PR: https://github.com/frappe/erpnext/pull/49816
3ffd50c772735877b330d010c1058f623da8721d
0e8f8677b8eb31e7834f72d1c6314d3c3f392ca6
baec607ff5905b1c67531096a9cf50ec7ff00a5d

View File

@@ -1,70 +1,36 @@
### Introduction (For First-Time Contributors)
### Introduction (first timers)
Thank you for your interest in raising an issue with ERPNext. An issue can be either a bug report or a feature request.
Thank you for your interest in raising an Issue with ERPNext. An Issue could mean a bug report or a request for a missing feature. By raising a bug report, you are contributing to the development of ERPNext and this is the first step of participating in the community. Bug reports are very helpful for developers as they quickly fix the issue before other users start facing it.
By reporting bugs, you contribute directly to improving ERPNext. Bug reports help developers identify and fix issues quickly before they affect more users.
Feature requests are also a great way to take the product forward. New ideas can come in any user scenario and the issue list also acts a roadmap of future features.
Feature requests are also valuable. They help shape the future of the product by introducing new ideas and improvements based on real-world use cases.
When you are raising an Issue, you should keep a few things in mind. Remember that the developer does not have access to your machine so you must give all the information you can while raising an Issue. If you are suggesting a feature, you should be very clear about what you want.
When raising an issue, keep in mind that developers do not have access to your environment. Therefore, provide as much relevant information as possible.
If you are suggesting a feature, clearly describe what you expect and how it should behave.
> ⚠️ The issue tracker is not the right place for general questions or discussions.
> Please use the forum instead: https://discuss.frappe.io/c/erpnext/6
---
The Issue list is not the right place to ask a question or start a general discussion. If you want to do that , then the right place is the forum [https://discuss.erpnext.com](https://discuss.erpnext.com).
### Reply and Closing Policy
If your issue is unclear or does not meet the guidelines, it may be closed.
If that happens, please provide the requested information and reopen the issue.
---
If your issue is not clear or does not meet the guidelines, then it will be closed. If it is closed, please supply the information asked and re-open it.
### General Issue Guidelines
1. **Search existing issues:**
Before creating a new issue, check if it already exists. You can support existing issues with a 👍 or contribute additional details or mockups.
2. **Report issues separately:**
Do not combine multiple unrelated issues into a single report.
3. **Be concise:**
Avoid long explanations. Use bullet points and screenshots where possible.
---
1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created.
1. **Report each issue separately:** Don't club multiple, unreleated issues in one note.
1. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs.
### Bug Report Guidelines
1. **Steps to reproduce:**
Clearly list the steps required to reproduce the issue. If the issue cannot be reproduced, it cannot be fixed.
2. **Version number:**
Include the ERPNext version. The issue may already be fixed in a newer release.
3. **Clear title:**
Use a descriptive title (e.g., "Unable to submit Purchase Order without Basic Rate" instead of "Cannot submit").
4. **Screenshots:**
Add screenshots or screen recordings (e.g., `.gif`) to illustrate the issue.
---
1. **Steps to Reproduce:** The bug report must have a list of steps needed to reproduce a bug. If we cannot reproduce it, then we cannot solve it.
1. **Version Number:** Please add the version number in your report. Often a bug is fixed in the latest version
1. **Clear Title:** Add a clear subject to your bug report like "Unable to submit Purchase Order without Basic Rate" instead of just "Cannot Submit"
1. **Screenshots:** Screenshots are a great way of communicating issues. Try adding annotations or using LiceCAP to take a screencast in `gif`.
### Feature Request Guidelines
1. **Clarity:**
Clearly describe the expected behavior. Avoid vague statements.
1. **Clarity:** Clearly specify how do you want the feature to behave. Don't just say "I would like multiple PDF formats", say that "Ability to add multiple print formats for customers with different languages".
1. **Solution:** Try and identify how the feature should look like.
1. **Mockups:** Mockups are a great way to explain your requirement.
2. **Proposed solution:**
Suggest how the feature should work.
### What if my Issue is closed
3. **Mockups:**
Provide mockups or examples whenever possible.
---
### What if my issue is closed?
Don't worry. Review the feedback, provide the required information, and reopen the issue.
Don't worry, take the feedback, supply the correct information and re-open it!

View File

@@ -9,7 +9,7 @@ body:
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.frappe.io/c/erpnext/6)
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
@@ -60,7 +60,7 @@ body:
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext version -
ERPNext Verion -
validations:
required: true

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Community Forum
url: https://discuss.frappe.io/c/erpnext/6
url: https://discuss.erpnext.com/
about: For general QnA, discussions and community help.

View File

@@ -1,6 +1,6 @@
---
name: Feature request
about: Suggest an idea or enhancement for ERPNext
about: Suggest an idea to improve ERPNext
title: ''
labels: feature-request
assignees: ''
@@ -11,27 +11,23 @@ assignees: ''
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://docs.erpnext.com or use https://discuss.frappe.io/c/erpnext/6
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
Please keep in mind that we get many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
If you're in urgent need of a feature, please try the following channels to get paid developments done quickly:
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
1. Certified ERPNext partners: https://erpnext.com/partners
2. Developer community on ERPNext forums: https://discuss.frappe.io/c/framework/5
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
-->
## Before Submitting
- [ ] I searched existing issues and confirmed this is not a duplicate
- [ ] This is a feature request, not a bug or support question
- [ ] For support: https://discuss.frappe.io/c/erpnext/6
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. As a [role], I have to [painful task] because [missing feature].
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
@@ -39,17 +35,5 @@ A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Impact**
<!-- Check one: -->
- [ ] Blocks critical workflow — no viable workaround
- [ ] Significant friction — workaround exists but is painful
- [ ] Nice to have — minor improvement
**Additional context**
Add any other context or screenshots about the feature request here.
**Environment**
- ERPNext Version: <!-- Find this in Help > About, e.g. v15.12.0 -->
- Frappe Version: <!-- Find this in Help > About, e.g. v15.10.0 -->
- Deployment: <!-- Frappe Cloud / Self-hosted / ERPNext Cloud -->

View File

@@ -66,8 +66,7 @@ ignore =
F841,
E713,
E712,
B023,
B028
B023
max-line-length = 200

View File

@@ -1,65 +1,54 @@
import sys
import requests
from urllib.parse import urlparse
import requests
WEBSITE_REPOS = [
docs_repos = [
"frappe_docs",
"erpnext_documentation",
"erpnext_com",
"frappe_io",
]
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"docs.frappe.io",
"frappeframework.com",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def is_valid_url(url: str) -> bool:
parts = urlparse(url)
return all((parts.scheme, parts.netloc, parts.path))
def is_documentation_link(word: str) -> bool:
if not word.startswith("http") or not is_valid_url(word):
return False
parsed_url = urlparse(word)
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
return True
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split("/")
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
return True
return False
def contains_documentation_link(body: str) -> bool:
return any(is_documentation_link(word) for line in body.splitlines() for word in line.split())
def check_pull_request(number: str) -> "tuple[int, str]":
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
if not response.ok:
return 1, "Pull Request Not Found! ⚠️"
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if not title.startswith("feat") or not head_sha or "no-docs" in body or "backport" in body:
return 0, "Skipping documentation checks... 🏃"
if contains_documentation_link(body):
return 0, "Documentation Link Found. You're Awesome! 🎉"
return 1, "Documentation Link Not Found! ⚠️"
def docs_link_exists(body):
for line in body.splitlines():
for word in line.split():
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
elif parsed_url.netloc == "docs.erpnext.com":
return True
if __name__ == "__main__":
exit_code, message = check_pull_request(sys.argv[1])
print(message)
sys.exit(exit_code)
pr = sys.argv[1]
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
if response.ok:
payload = response.json()
title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower()
if (title.startswith("feat")
and head_sha
and "no-docs" not in body
and "backport" not in body
):
if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
else:
print("Documentation Link Not Found! ⚠️")
sys.exit(1)
else:
print("Skipping documentation checks... 🏃")

View File

@@ -4,24 +4,14 @@ set -e
cd ~ || exit
sudo apt update
sudo apt remove mysql-server mysql-client
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev
sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
mkdir frappe
pushd frappe
git init
git remote add origin "https://github.com/${frappeuser}/frappe"
git fetch origin "${frappecommitish}" --depth 1
git checkout FETCH_HEAD
popd
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
@@ -34,14 +24,15 @@ fi
if [ "$DB" == "mariadb" ];then
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
fi
if [ "$DB" == "postgres" ];then
@@ -51,13 +42,12 @@ fi
install_whktml() {
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo apt install /tmp/wkhtmltox.deb
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
}
install_whktml &
wkpid=$!
cd ~/frappe-bench || exit
@@ -66,13 +56,11 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app payments --branch develop
bench get-app payments
bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
wait $wkpid
bench start &>> ~/frappe-bench/bench_start.log &
bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes

View File

@@ -8,10 +8,9 @@
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
"use_mysqlclient": 1,
"root_login": "root",
"root_password": "root",
"root_password": "travis",
"host_name": "http://test_site:8000",
"install_apps": ["payments", "erpnext"],
"install_apps": ["erpnext"],
"throttle_user_limit": 100
}

View File

@@ -2,9 +2,7 @@ import re
import sys
errors_encounter = 0
pattern = re.compile(
r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)"
)
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
f_string_pattern = re.compile(r"_\(f[\"']")
@@ -12,14 +10,14 @@ starts_with_f_pattern = re.compile(r"_\(f")
# skip first argument
files = sys.argv[1:]
files_to_scan = [_file for _file in files if _file.endswith((".py", ".js"))]
files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))]
for _file in files_to_scan:
with open(_file) as f:
print(f"Checking: {_file}")
with open(_file, 'r') as f:
print(f'Checking: {_file}')
file_lines = f.readlines()
for line_number, line in enumerate(file_lines, 1):
if "frappe-lint: disable-translate" in line:
if 'frappe-lint: disable-translate' in line:
continue
start_matches = start_pattern.search(line)
@@ -30,9 +28,7 @@ for _file in files_to_scan:
has_f_string = f_string_pattern.search(line)
if has_f_string:
errors_encounter += 1
print(
f"\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}"
)
print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}')
continue
else:
continue
@@ -40,29 +36,25 @@ for _file in files_to_scan:
match = pattern.search(line)
error_found = False
if not match and line.endswith((",\n", "[\n")):
if not match and line.endswith((',\n', '[\n')):
# concat remaining text to validate multiline pattern
line = "".join(file_lines[line_number - 1 :])
line = line[start_matches.start() + 1 :]
line = "".join(file_lines[line_number - 1:])
line = line[start_matches.start() + 1:]
match = pattern.match(line)
if not match:
error_found = True
print(f"\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}")
print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}')
if not error_found and not words_pattern.search(line):
error_found = True
print(
f"\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}"
)
print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}')
if error_found:
errors_encounter += 1
if errors_encounter > 0:
print(
'\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.'
)
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
sys.exit(1)
else:
print("\nGood To Go!")
print('\nGood To Go!')

View File

@@ -1,40 +0,0 @@
#!/bin/bash
set -e
cd ~ || exit
echo "Setting Up Bench..."
pip install frappe-bench
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)"
cd ./frappe-bench || exit
echo "Get ERPNext..."
bench get-app --skip-assets erpnext "${GITHUB_WORKSPACE}"
echo "Generating POT file..."
bench generate-pot-file --app erpnext
cd ./apps/erpnext || exit
echo "Configuring git user..."
git config user.email "developers@erpnext.com"
git config user.name "frappe-pr-bot"
echo "Setting the correct git remote..."
# Here, the git remote is a local file path by default. Let's change it to the upstream repo.
git remote set-url upstream https://github.com/frappe/erpnext.git
echo "Creating a new branch..."
isodate=$(date -u +"%Y-%m-%d")
branch_name="pot_${BASE_BRANCH}_${isodate}"
git checkout -b "${branch_name}"
echo "Commiting changes..."
git add erpnext/locale/main.pot
git commit -m "chore: update POT file"
gh auth setup-git
git push -u upstream "${branch_name}"
echo "Creating a PR..."
gh pr create --fill --base "${BASE_BRANCH}" --head "${branch_name}" --reviewer ${PR_REVIEWER} -R frappe/erpnext

4
.github/release.yml vendored
View File

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

8
.github/stale.yml vendored
View File

@@ -12,14 +12,6 @@ exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Skip the stale action for draft PRs
exemptDraftPr: true
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- hotfix
- no-stale
pulls:
daysUntilStale: 15
daysUntilClose: 3

View File

@@ -5,16 +5,13 @@ on:
- closed
- labeled
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout Actions
uses: actions/checkout@v6
uses: actions/checkout@v2
with:
repository: "frappe/backport"
path: ./actions
@@ -24,6 +21,6 @@ jobs:
- name: Run backport
uses: ./actions/backport
with:
token: ${{secrets.RELEASE_TOKEN}}
token: ${{secrets.BACKPORT_BOT_TOKEN}}
labelsToAdd: "backport"
title: "{{originalTitle}}"

View File

@@ -2,10 +2,6 @@ name: Trigger Docker build on release
on:
release:
types: [released]
permissions:
contents: read
jobs:
curl:
runs-on: ubuntu-latest

View File

@@ -3,9 +3,6 @@ on:
pull_request:
types: [ opened, synchronize, reopened, edited ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
@@ -13,12 +10,12 @@ jobs:
steps:
- name: 'Setup Environment'
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: 'Clone repo'
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Validate Docs
env:

View File

@@ -1,44 +0,0 @@
# This workflow is agnostic to branches. Only maintain on develop branch.
# To add/remove branches just modify the matrix.
name: Regenerate POT file (translatable strings)
on:
schedule:
# 9:30 UTC => 3 PM IST Sunday
- cron: "30 9 * * 0"
workflow_dispatch:
jobs:
regenerate-pot-file:
name: Regenerate POT file
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branch: ["develop", "version-16-hotfix"]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ matrix.branch }}
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
- name: Run script to update POT file
run: |
bash ${GITHUB_WORKSPACE}/.github/helper/update_pot_file.sh
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
BASE_BRANCH: ${{ matrix.branch }}
PR_REVIEWER: barredterra # change to your GitHub username if you copied this file

View File

@@ -1,36 +0,0 @@
# This workflow is agnostic to branches. Only maintain on develop branch.
# To add/remove versions just modify the matrix.
name: Create weekly release pull requests
permissions:
contents: read
on:
schedule:
# 9:30 UTC => 3 PM IST Tuesday
- cron: "30 9 * * 2"
workflow_dispatch:
jobs:
stable-release:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version: ["15", "16"]
steps:
- uses: octokit/request-action@v2.x
with:
route: POST /repos/{owner}/{repo}/pulls
owner: frappe
repo: erpnext
title: |-
"chore: release v${{ matrix.version }}"
body: "Automated weekly release."
base: version-${{ matrix.version }}
head: version-${{ matrix.version }}-hotfix
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

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

View File

@@ -3,10 +3,6 @@ on:
pull_request_target:
types: [opened, reopened]
permissions:
issues: write
pull-requests: write
jobs:
triage:
runs-on: ubuntu-latest

View File

@@ -3,46 +3,27 @@ name: Linters
on:
pull_request: { }
permissions:
contents: read
jobs:
linters:
name: linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v2
- name: Set up Python 3.14
uses: actions/setup-python@v6
- name: Set up Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.14'
cache: pip
python-version: '3.10'
- name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0
semgrep:
name: semgrep
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: '3.14'
cache: pip
uses: pre-commit/action@v2.0.3
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Download semgrep
run: pip install semgrep
run: pip install semgrep==0.97.0
- name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
- name: Semgrep for Test Correctness
run: semgrep ci --include=**/test_*.py --config ./semgrep/test-correctness.yml

View File

@@ -1,21 +0,0 @@
name: 'Lock threads'
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: 14
pr-inactive-days: 14

View File

@@ -8,16 +8,10 @@ on:
- '**.md'
- '**.html'
- '**.csv'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
group: patch-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
@@ -29,44 +23,41 @@ jobs:
services:
mysql:
image: mariadb:11.8
image: mariadb:10.3
env:
MARIADB_ROOT_PASSWORD: 'root'
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -fq "${GITHUB_WORKSPACE}"
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- name: Setup Python
uses: actions/setup-python@v6
uses: "gabrielfalcao/pyenv-action@v9"
with:
python-version: |
3.11
3.13
3.14
versions: 3.10:latest, 3.7:latest
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v2
with:
node-version: 24
node-version: 14
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -75,7 +66,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
@@ -88,9 +79,9 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -101,6 +92,7 @@ jobs:
- name: Install
run: |
pip install frappe-bench
pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
@@ -109,62 +101,42 @@ jobs:
- name: Run Patch Tests
run: |
cd ~/frappe-bench/
bench remove-app payments --force
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
wget https://frappe.io/files/erpnext-v14.sql.gz
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
wget https://erpnext.com/files/v10-erpnext.sql.gz
bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
pyenv global $(pyenv versions | grep '3.7')
for version in $(seq 12 13)
do
echo "Updating to v$version"
branch_name="version-$version-hotfix"
function update_to_version() {
version=$1
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
branch_name="version-$version-hotfix"
echo "Updating to v$version"
git -C "apps/frappe" checkout -q -f $branch_name
git -C "apps/erpnext" checkout -q -f $branch_name
# Fetch and checkout branches
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
git -C "apps/frappe" checkout -q -f $branch_name
git -C "apps/erpnext" checkout -q -f $branch_name
rm -rf ~/frappe-bench/env
bench setup env
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
# Resetup env and install apps
pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
bench -v setup env --python python$2
bench pip install -e ./apps/erpnext
bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
done
bench --site test_site migrate
}
update_to_version 15 3.13
update_to_version 16 3.14
echo "Updating to latest version"
git -C "apps/frappe" fetch --depth 1 upstream "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/frappe" checkout -q -f FETCH_HEAD
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
pgrep honcho | xargs kill
pyenv global $(pyenv versions | grep '3.10')
rm -rf ~/frappe-bench/env
bench -v setup env
bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext
bench start &>> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
- name: Show bench output
if: ${{ always() }}
run: |
cd ~/frappe-bench
cat bench_start.log || true
cd logs
for f in ./*.log*; do
echo "Printing log: $f";
cat $f
done
bench --site test_site install-app payments

View File

@@ -1,28 +0,0 @@
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
name: Skipped Patch Test
on:
pull_request:
paths:
- "**.js"
- "**.css"
- "**.md"
- "**.html"
- "**.csv"
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
name: Patch Test
steps:
- name: Pass skipped tests unconditionally
run: "echo Skipped"

View File

@@ -2,25 +2,23 @@ name: Generate Semantic Release
on:
push:
branches:
- version-13
permissions:
contents: read
- version-14
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v6
uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v2
with:
node-version: 20
node-version: 16
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save

View File

@@ -1,42 +0,0 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
# This action needs to be maintained on all branches that do releases.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v13.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG \
| jq -r '.body' \
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
| sed -E 's/by @mergify //'
)
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}

View File

@@ -1,143 +0,0 @@
name: Individual
on:
workflow_dispatch:
concurrency:
group: server-individual-tests-lightmode-develop
cancel-in-progress: true
permissions:
contents: read
jobs:
discover:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Clone
uses: actions/checkout@v6
- id: set-matrix
run: |
# Use grep and find to get the list of test files
matrix=$(find . -path '*/test_*.py' | xargs grep -l 'def test_' | sort | awk '{
# Remove ./ prefix, file extension, and replace / with .
gsub(/^\.\//, "", $0)
gsub(/\.py$/, "", $0)
gsub(/\//, ".", $0)
# Add to array
tests[NR] = $0
}
END {
# Start JSON array
printf "{\n \"include\": [\n"
# Loop through array and create JSON objects
for (i=1; i<=NR; i++) {
printf " {\"test\": \"%s\"}", tests[i]
if (i < NR) printf ","
printf "\n"
}
# Close JSON array
printf " ]\n}"
}')
# Output the matrix
echo "matrix=$(echo "$matrix" | jq -c)" >> $GITHUB_OUTPUT
# For debugging (optional)
echo "Generated matrix:"
echo "$matrix"
test:
needs: discover
runs-on: ubuntu-latest
timeout-minutes: 60
env:
NODE_ENV: "production"
strategy:
fail-fast: false
matrix: ${{fromJson(needs.discover.outputs.matrix)}}
max-parallel: 14
name: Test
services:
mysql:
image: mariadb:10.6
env:
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: |
site_name=$(echo "${{matrix.test}}" | sed -e 's/.*\.\(test_.*$\)/\1/')
echo "$site_name"
mkdir ~/frappe-bench/sites/$site_name
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/$site_name/site_config.json
cd ~/frappe-bench/
bench --site $site_name reinstall --yes
bench --site $site_name set-config allow_tests true
bench --site $site_name run-tests --module ${{ matrix.test }} --lightmode

View File

@@ -15,13 +15,13 @@ jobs:
name: Check Commit Titles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
with:
fetch-depth: 200
- uses: actions/setup-node@v6
- uses: actions/setup-node@v3
with:
node-version: 18
node-version: 14
check-latest: true
- name: Check commit titles

View File

@@ -1,31 +0,0 @@
# Tests are skipped for these files but github doesn't allow "passing" hence this is required.
name: Skipped Tests
on:
pull_request:
paths:
- "**.js"
- "**.css"
- "**.svg"
- "**.md"
- "**.html"
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
container: [1, 2, 3, 4]
name: Python Unit Tests
steps:
- name: Pass skipped tests unconditionally
run: "echo Skipped"

View File

@@ -1,96 +1,85 @@
name: Server (Mariadb)
on:
repository_dispatch:
types: [frappe-framework-change]
pull_request:
paths-ignore:
- '**.js'
- '**.css'
- '**.svg'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
schedule:
# Run everday at midnight UTC / 5:30 IST
- cron: "0 0 * * *"
- '**.csv'
push:
branches: [ develop ]
paths-ignore:
- '**.js'
- '**.md'
workflow_dispatch:
inputs:
user:
description: 'Frappe Framework repository user (add your username for forks)'
description: 'user'
required: true
default: 'frappe'
type: string
branch:
description: 'Frappe Framework branch'
description: 'Branch name'
default: 'develop'
required: false
type: string
permissions:
contents: read
concurrency:
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
strategy:
fail-fast: false
matrix:
container: [1, 2, 3, 4]
container: [1, 2, 3]
name: Python Unit Tests
services:
mysql:
image: mariadb:10.6
image: mariadb:10.3
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.14'
python-version: '3.10'
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -fq "${GITHUB_WORKSPACE}"
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v2
with:
node-version: 24
node-version: 14
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -99,7 +88,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
@@ -112,9 +101,9 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -128,39 +117,10 @@ jobs:
DB: mariadb
TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --lightmode --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds 3 --build-number ${{ matrix.container }}'
env:
TYPE: server
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true
- name: Upload coverage data
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.container }}
path: /home/runner/frappe-bench/sites/coverage.xml
coverage:
name: Coverage Wrap Up
needs: test
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Upload coverage data
uses: codecov/codecov-action@v4
with:
name: MariaDB
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
CI_BUILD_ID: ${{ github.run_id }}

View File

@@ -6,18 +6,12 @@ on:
- '**.js'
- '**.md'
- '**.html'
- 'crowdin.yml'
- '.coderabbit.yml'
- '.mergify.yml'
types: [opened, labelled, synchronize, reopened]
concurrency:
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
group: server-postgres-develop-${{ github.event.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
@@ -47,32 +41,32 @@ jobs:
steps:
- name: Clone
uses: actions/checkout@v6
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v2
with:
python-version: '3.14'
python-version: '3.10'
- name: Check for valid Python & Merge Conflicts
run: |
python -m compileall -fq "${GITHUB_WORKSPACE}"
python -m compileall -f "${GITHUB_WORKSPACE}"
if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
then echo "Found merge conflicts"
exit 1
fi
- name: Setup Node
uses: actions/setup-node@v6
uses: actions/setup-node@v2
with:
node-version: 24
node-version: 14
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v4
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
@@ -81,7 +75,7 @@ jobs:
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v4
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
@@ -94,9 +88,9 @@ jobs:
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

10
.gitignore vendored
View File

@@ -2,6 +2,7 @@
*.py~
.DS_Store
conf.py
locale
latest_updates.json
.wnf-lang-status
*.egg-info
@@ -14,12 +15,5 @@ __pycache__
*~
.idea/
.vscode/
.helix/
node_modules/
.backportrc.json
# Aider AI Chat
.aider*
# Banking SPA
erpnext/public/banking
erpnext/www/banking.html
.backportrc.json

View File

@@ -2,27 +2,35 @@ pull_request_rules:
- name: Auto-close PRs on stable branch
conditions:
- and:
- and:
- author!=surajshetty3416
- author!=gavindsouza
- author!=rohitwaghchaure
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:
- base=version-13
- base=version-12
- base=version-14
- base=version-15
- base=version-16
- and:
- author!=surajshetty3416
- author!=gavindsouza
- author!=rohitwaghchaure
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=mergify[bot]
- or:
- base=version-13
- base=version-12
actions:
close:
comment:
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
message: |
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: Auto-close PRs on pre-release branch
conditions:
- base=version-13-pre-release
actions:
close:
comment:
message: |
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
- name: backport to develop
conditions:
- label="backport develop"
@@ -32,6 +40,7 @@ pull_request_rules:
- develop
assignees:
- "{{ author }}"
- name: backport to version-14-hotfix
conditions:
- label="backport version-14-hotfix"
@@ -41,24 +50,57 @@ pull_request_rules:
- version-14-hotfix
assignees:
- "{{ author }}"
- name: backport to version-15-hotfix
- name: backport to version-14-pre-release
conditions:
- label="backport version-15-hotfix"
- label="backport version-14-pre-release"
actions:
backport:
branches:
- version-15-hotfix
- version-14-pre-release
assignees:
- "{{ author }}"
- name: backport to version-16-hotfix
- name: backport to version-13-hotfix
conditions:
- label="backport version-16-hotfix"
- label="backport version-13-hotfix"
actions:
backport:
branches:
- version-16-hotfix
- version-13-hotfix
assignees:
- "{{ author }}"
- name: backport to version-13-pre-release
conditions:
- label="backport version-13-pre-release"
actions:
backport:
branches:
- version-13-pre-release
assignees:
- "{{ author }}"
- name: backport to version-12-hotfix
conditions:
- label="backport version-12-hotfix"
actions:
backport:
branches:
- version-12-hotfix
assignees:
- "{{ author }}"
- name: backport to version-12-pre-release
conditions:
- label="backport version-12-pre-release"
actions:
backport:
branches:
- version-12-pre-release
assignees:
- "{{ author }}"
- name: Automatic merge on CI success and review
conditions:
- status-success=linters
@@ -89,6 +131,6 @@ pull_request_rules:
merge:
method: squash
commit_message_template: |
{{ title }} (#{{ number }})
{{ title }} (#{{ number }})
{{ body }}
{{ body }}

View File

@@ -1,11 +1,11 @@
exclude: 'node_modules|.git'
default_stages: [pre-commit]
default_stages: [commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.0.1
hooks:
- id: trailing-whitespace
files: "erpnext.*"
@@ -15,56 +15,29 @@ repos:
args: ['--branch', 'develop']
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: prettier
types_or: [javascript, vue, scss]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
erpnext/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/templates/includes/.*
)$
- id: flake8
additional_dependencies: [
'flake8-bugbear',
]
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
- repo: https://github.com/adityahase/black
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
erpnext/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$
- id: black
additional_dependencies: ['click==8.0.4']
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
hooks:
- id: ruff
name: "Run ruff import sorter"
args: ["--select=I", "--fix"]
- id: isort
exclude: ".*setup.py$"
- id: ruff
name: "Run ruff linter"
- id: ruff-format
name: "Run ruff formatter"
ci:
autoupdate_schedule: weekly

View File

@@ -1,5 +1,5 @@
{
"branches": ["version-13"],
"branches": ["version-14"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
@@ -10,7 +10,7 @@
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py'
}
],
[
@@ -21,4 +21,4 @@
],
"@semantic-release/github"
]
}
}

View File

@@ -3,21 +3,26 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @ruthra-kumar
erpnext/assets/ @khushi8112
erpnext/regional @ruthra-kumar
erpnext/selling @ruthra-kumar
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
erpnext/maintenance/ @rohitwaghchaure @mihir-kandoi
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
erpnext/quality_management/ @rohitwaghchaure @mihir-kandoi
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
erpnext/subcontracting/ @mihir-kandoi
erpnext/projects/ @nishkagosalia
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/patches/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/projects/ @ruchamahabal
.github/ @ruthra-kumar @mihir-kandoi
pyproject.toml @ruthra-kumar
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
pyproject.toml @ankush

206
README.md
View File

@@ -1,177 +1,89 @@
<div align="center">
<a href="https://frappe.io/erpnext">
<img src="./erpnext/public/images/v16/erpnext.svg" alt="ERPNext Logo" height="80px" width="80px"/>
<a href="https://erpnext.com">
<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" height="128">
</a>
<h2>ERPNext</h2>
<div align="center">
<p>Powerful, Intuitive and Open-Source ERP</p>
</div>
<p align="center">
<p>ERP made simple</p>
</p>
[![Learn on Frappe School](https://img.shields.io/badge/Frappe%20School-Learn%20ERPNext-blue?style=flat-square)](https://frappe.school)<br><br>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext.svg)](https://hub.docker.com/r/frappe/erpnext)
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[![UI](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml/badge.svg?branch=develop&event=schedule)](https://github.com/erpnext/erpnext_ui_tests/actions/workflows/ui-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
[https://erpnext.com](https://erpnext.com)
</div>
<div align="center">
<img src="./erpnext/public/images/v16/hero_image.png" alt="ERPNext Hero Image"/>
ERPNext as a monolith includes the following areas for managing businesses:
1. [Accounting](https://erpnext.com/open-source-accounting)
1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system)
1. [CRM](https://erpnext.com/open-source-crm)
1. [Sales](https://erpnext.com/open-source-sales-purchase)
1. [Purchase](https://erpnext.com/open-source-sales-purchase)
1. [HRMS](https://erpnext.com/open-source-hrms)
1. [Project Management](https://erpnext.com/open-source-projects)
1. [Support](https://erpnext.com/open-source-help-desk-software)
1. [Asset Management](https://erpnext.com/open-source-asset-management-software)
1. [Quality Management](https://erpnext.com/docs/user/manual/en/quality-management)
1. [Manufacturing](https://erpnext.com/open-source-manufacturing-erp-software)
1. [Website Management](https://erpnext.com/open-source-website-builder-software)
1. [Customize ERPNext](https://erpnext.com/docs/user/manual/en/customize-erpnext)
1. [And More](https://erpnext.com/docs/user/manual/en/)
ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a full-stack web app framework built with Python & JavaScript.
## Installation
<div align="center" style="max-height: 40px;">
<a href="https://frappecloud.com/erpnext/signup">
<img src=".github/try-on-f-cloud-button.svg" height="40">
</a>
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD" height="37"/>
</a>
</div>
<div align="center">
<a href="https://erpnext-demo.frappe.cloud/api/method/erpnext_demo.erpnext_demo.auth.login_demo">Live Demo</a>
-
<a href="https://frappe.io/erpnext">Website</a>
-
<a href="https://docs.frappe.io/erpnext/">Documentation</a>
</div>
> Login for the PWD site: (username: Administrator, password: admin)
## ERPNext
### Containerized Installation
100% Open-Source ERP System to help you run your business.
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
### Motivation
Running a business is a complex task - handling invoices, tracking stock, managing personnel, and other daily operations. In a market where software is sold separately to manage each of these tasks, ERPNext does all of the above and more, for free.
### Key Features
- **Accounting**: All the tools you need to manage cash flow in one place, right from recording transactions to summarizing and analyzing financial reports.
- **Order Management**: Track inventory levels, replenish stock, and manage sales orders, customers, suppliers, shipments, deliverables, and order fulfillment.
- **Manufacturing**: Simplifies the production cycle, helps track material consumption, exhibits capacity planning, handles subcontracting, and more!
- **Asset Management**: From purchase to disposal, IT infrastructure to equipment. Covers every branch of your organization, all in one centralized system.
- **Projects**: Deliver both internal and external projects on time, budget and profitability. Track tasks, timesheets, and issues by project.
<details open>
<summary>More</summary>
<img src="https://erpnext.com/files/v16_bom.png"/>
<img src="https://erpnext.com/files/v16_stock_summary.png"/>
<img src="https://erpnext.com/files/v16_job_card.png"/>
<img src="https://erpnext.com/files/v16_tasks.png"/>
</details>
### Under the Hood
- [**Frappe Framework**](https://github.com/frappe/frappe): A full-stack web application framework written in Python and JavaScript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API.
- [**Frappe UI**](https://github.com/frappe/frappe-ui): A Vue-based UI library, to provide a modern user interface. The Frappe UI library provides a variety of components that can be used to build single-page applications on top of the Frappe Framework.
## Production Setup
### Managed Hosting
You can try [Frappe Cloud](https://frappecloud.com), a simple, user-friendly, and sophisticated [open-source](https://github.com/frappe/press) platform to host Frappe applications reliably and securely.
It handles installation, setup, upgrades, monitoring, maintenance, and support of your Frappe deployments. It is a fully featured developer platform with an ability to manage and control multiple Frappe deployments.
<div>
<a href="https://erpnext-demo.frappe.cloud/app/home" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/try-on-fc-white.png">
<img src="https://frappe.io/files/try-on-fc-black.png" alt="Try on Frappe Cloud" height="28" />
</picture>
</a>
</div>
### Self-Hosted
#### Docker
See [Frappe Docker Documentation](https://github.com/frappe/frappe_docker) for full documentation & FAQ on Docker setup
#### Prerequisites
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose v2](https://docs.docker.com/compose/)
- [git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git)
> For Docker basics and best practices refer to Docker's [documentation](https://docs.docker.com)
### Try on your environment
> **⚠️ Disposable demo only**
>
> **This setup is intended for quick evaluation. Expect to throw the environment away.** You will not be able to install custom apps to this setup. For production deployments, custom configurations, and detailed explanations, see the full documentation.
First clone the repo:
```sh
git clone https://github.com/frappe/frappe_docker
cd frappe_docker
```
Then run:
```sh
docker compose -f pwd.yml up -d
```
Wait for a couple of minutes for ERPNext site to be created or check the `create-site` container logs before opening browser on port `8080`. (username: `Administrator`, password: `admin`)
See [Frappe Docker](https://github.com/frappe/frappe_docker/blob/main/docs/01-getting-started/03-arm64.md) for ARM based docker setup
## Development Setup
### Manual Install
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the Frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
### Local
## Learning and community
To setup the repository locally follow the steps mentioned below:
1. Setup bench by following the [Installation Steps](https://frappeframework.com/docs/user/en/installation) and start the server
```
bench start
```
2. In a separate terminal window, run the following commands:
```
# Create a new site
bench new-site erpnext.localhost
```
3. Get the ERPNext app and install it
```
# Get the ERPNext app
bench get-app https://github.com/frappe/erpnext
# Install the app
bench --site erpnext.localhost install-app erpnext
```
4. Open the URL `http://erpnext.localhost:8000/app` in your browser, you should see the app running
## Learning and Community
1. [Frappe School](https://school.frappe.io) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.frappe.io/c/erpnext/6) - Engage with the community of ERPNext users and service providers.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
## Contributing
1. [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
2. [Report Security Vulnerabilities](https://erpnext.com/security)
3. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
4. [Translations](https://crowdin.com/project/frappe)
1. [Report Security Vulnerabilities](https://erpnext.com/security)
1. [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
1. [Translations](https://translate.erpnext.com)
## License
GNU/General Public License (see [license.txt](license.txt))
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
## Logo and Trademark Policy
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
<br />
<br />
<div align="center" style="padding-top: 0.75rem;">
<a href="https://frappe.io" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://frappe.io/files/Frappe-white.png">
<img src="https://frappe.io/files/Frappe-black.png" alt="Frappe Technologies" height="28"/>
</picture>
</a>
</div>

View File

@@ -1,7 +1,7 @@
# Security Policy
The ERPNext team and community take security issues seriously. To report a security issue, please go through the information mentioned [here](https://frappe.io/security).
The ERPNext team and community take security issues seriously. To report a security issue, fill out the form at [https://erpnext.com/security/report](https://erpnext.com/security/report).
You can help us make ERPNext and all its users more secure by following the [Reporting guidelines](https://frappe.io/security).
You can help us make ERPNext and all it's users more secure by following the [Reporting guidelines](https://erpnext.com/security).
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.
We appreciate your efforts to responsibly disclose your findings. We'll endeavor to respond quickly, and will keep you updated throughout the process.

View File

@@ -18,9 +18,8 @@ We will grant permission to use the ERPNext name and logo for projects that meet
- The primary purpose of your project is to promote the spread and improvement of the ERPNext software.
- Your project is non-commercial in nature (it can make money to cover its costs or contribute to non-profit entities, but it cannot be run as a for-profit project or business).
- Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Your project neither promotes nor is associated with entities that currently fail to comply with the GPL license under which ERPNext is distributed.
- If your project meets these criteria, you will be permitted to use the ERPNext name and logo to promote your project in any way you see fit with one exception: Please do not use ERPNext as part of a domain name.
Use of the ERPNext name and logo is additionally allowed in the following situations:

View File

@@ -1,5 +0,0 @@
**/setup/setup_wizard/data/uom_data.json,erpnext.gettext.extractors.uom_data.extract
**/setup/doctype/incoterm/incoterms.csv,erpnext.gettext.extractors.incoterms.extract
**/setup/setup_wizard/data/*.txt,erpnext.gettext.extractors.lines_from_txt_file.extract
**.tsx,frappe.gettext.extractors.html_template.extract
**.ts,frappe.gettext.extractors.html_template.extract
1 **/setup/setup_wizard/data/uom_data.json erpnext.gettext.extractors.uom_data.extract
2 **/setup/doctype/incoterm/incoterms.csv erpnext.gettext.extractors.incoterms.extract
3 **/setup/setup_wizard/data/*.txt erpnext.gettext.extractors.lines_from_txt_file.extract
4 **.tsx frappe.gettext.extractors.html_template.extract
5 **.ts frappe.gettext.extractors.html_template.extract

View File

@@ -1 +0,0 @@
VITE_BASE_NAME="banking"

24
banking/.gitignore vendored
View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,73 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@@ -1,24 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
onlyExportComponents: false,
},
]);

View File

@@ -1,50 +0,0 @@
<!doctype html>
<html lang="{{ lang }}" dir="{{layout_direction}}">
<head>
<meta charset="UTF-8" />
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#0089FF">
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#0089FF">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-status-bar-style" content="#0089FF">
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding">
<meta name="author" content="">
<meta name="viewport" content="width=device-width, initial-scale=1.0,
maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="white">
<meta name="mobile-web-app-capable" content="yes">
<link rel="shortcut icon" href="{{ favicon or ' /assets/erpnext/images/erpnext-favicon.svg' }}" type="image/x-icon">
<link rel="icon" href="{{ favicon or ' /assets/erpnext/images/erpnext-favicon.svg' }}" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Banking | {{ app_name }}</title>
</head>
<body>
<div id="root"></div>
<script>window.csrf_token = '{{ frappe.session.csrf_token }}';
if (!window.frappe) window.frappe = {};
frappe.boot = JSON.parse({{ boot }});
frappe.boot.layout_direction = "{{ layout_direction }}";
frappe._translations_loaded = fetch(
`/api/method/frappe.translate.get_boot_translations?v=${frappe.boot.translations_version}&lang=${frappe.boot.lang}`,
{
credentials: "same-origin",
headers: {
"X-Frappe-CSRF-Token": frappe.csrf_token,
"Accept": "application/json"
}
}
).then(r => r.json()).then(data => {
frappe._messages = data.message || {};
}).catch(() => { });
</script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,66 +0,0 @@
{
"name": "banking",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --base=/assets/erpnext/banking/ && yarn copy-html-entry",
"lint": "eslint .",
"preview": "vite preview",
"copy-html-entry": "cp ../erpnext/public/banking/index.html ../erpnext/www/banking.html"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.3.0",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.24",
"@vitejs/plugin-react": "^6.0.1",
"chrono-node": "^2.9.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dayjs": "^1.11.20",
"frappe-react-sdk": "^1.15.0",
"fuse.js": "^7.3.0",
"jotai": "^2.20.0",
"jotai-family": "^1.0.1",
"lodash.isplainobject": "^4.0.6",
"lucide-react": "^1.14.0",
"radix-ui": "^1.4.3",
"react": "^19.2.6",
"react-currency-input-field": "^4.0.5",
"react-day-picker": "9.14.0",
"react-dom": "^19.2.6",
"react-dropzone": "^15.0.0",
"react-hook-form": "^7.75.0",
"react-hotkeys-hook": "^5.3.2",
"react-markdown": "^10.1.0",
"react-router": "^7.15.0",
"react-router-dom": "^7.15.0",
"react-virtuoso": "^4.18.6",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.3.0",
"tw-animate-css": "^1.4.0",
"usehooks-ts": "^3.1.1",
"vite": "^8.0.11"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^25.3.0",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.48.0"
}
}

View File

@@ -1,13 +0,0 @@
const common_site_config = require('../../../sites/common_site_config.json');
const { webserver_port } = common_site_config;
export default {
'^/(app|api|assets|files|private)': {
target: `http://127.0.0.1:${webserver_port}`,
ws: true,
router: function(req) {
const site_name = req.headers.host.split(':')[0];
return `http://${site_name}:${webserver_port}`;
}
}
};

View File

@@ -1,65 +0,0 @@
import { useEffect } from 'react'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import { FrappeProvider } from 'frappe-react-sdk'
import { Toaster } from '@/components/ui/sonner'
import BankReconciliation from '@/pages/BankReconciliation'
import { TooltipProvider } from './components/ui/tooltip'
import BankStatementImporter from '@/pages/BankStatementImporter'
import { LucideProvider } from 'lucide-react'
import { ThemeProvider } from './components/ui/theme-provider'
import ViewBankStatementImportLog from './pages/ViewBankStatementImportLog'
import BankStatementImporterContainer from './pages/BankStatementImporterContainer'
function App() {
useEffect(() => {
// Check if user is logged in by checking the Cookie "user_id"
// In Frappe, unauthenticated users are "Guest"
const userId = document.cookie?.split('; ').find(row => row.startsWith('user_id='))?.split('=')[1]?.trim()
const isLoggedIn = userId !== 'Guest'
if (!isLoggedIn) {
if (import.meta.env.DEV) {
return
}
// Redirect to Frappe login page
window.location.href = '/login?redirect-to=/banking'
return
}
}, [])
return (
<LucideProvider
strokeWidth={1.5}
>
<TooltipProvider>
<FrappeProvider
swrConfig={{
errorRetryCount: 2
}}
socketPort={import.meta.env.VITE_SOCKET_PORT}
siteName={window.frappe?.boot?.sitename ?? import.meta.env.VITE_SITE_NAME}>
<ThemeProvider
defaultTheme={window.frappe?.boot?.desk_theme ?? "Automatic"}
>
{window.frappe?.boot?.user?.name && window.frappe?.boot?.user?.name !== 'Guest' &&
<BrowserRouter basename={import.meta.env.VITE_BASE_NAME ? `/${import.meta.env.VITE_BASE_NAME}` : ''}>
<Routes>
<Route index element={<BankReconciliation />} />
<Route path="/statement-importer" element={<BankStatementImporterContainer />}>
<Route index element={<BankStatementImporter />} />
<Route path=":id" element={<ViewBankStatementImportLog />} />
</Route>
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</BrowserRouter>
}
<Toaster richColors />
</ThemeProvider>
</FrappeProvider>
</TooltipProvider>
</LucideProvider>
)
}
export default App

View File

@@ -1,228 +0,0 @@
import { Button } from "@/components/ui/button"
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import _ from "@/lib/translate"
import { cn } from "@/lib/utils"
import { useFrappeGetDocList } from "frappe-react-sdk"
import Fuse from "fuse.js"
import { ChevronDownIcon } from "lucide-react"
import { useLayoutEffect, useMemo, useRef, useState } from "react"
import { FormControl } from "../ui/form"
export interface AccountsDropdownProps {
root_type?: ('Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense')[],
report_type?: 'Balance Sheet' | 'Profit and Loss',
account_type?: string[],
value?: string,
onChange?: (value: string) => void,
readOnly?: boolean,
disabled?: boolean,
company?: string,
filterFunction?: (account: Account) => boolean,
// If true, the component will be wrapped in a FormControl component
useInForm?: boolean,
buttonClassName?: string,
size?: 'sm' | 'md' | 'lg',
}
/**
* Component to select an account - supports fuzzy search
* @param root_type - The root type of the account
* @param report_type - The report type of the account
* @param account_type - The type of the account
* @param value - The value of the account field
* @param onChange - The function to call when the value changes
* @returns
*/
const AccountsDropdown = ({ root_type, report_type, account_type, value, onChange, readOnly, disabled, company, filterFunction, useInForm, buttonClassName, size = 'md' }: AccountsDropdownProps) => {
const { data } = useGetAccounts(root_type, report_type, account_type, company, filterFunction)
const groupedAccounts = useMemo(() => {
if (!data) return []
const grouped: Record<string, Account[]> = data.reduce((acc, account) => {
const parentAccount = account.parent_account
if (!parentAccount) return acc
if (!acc[parentAccount]) {
acc[parentAccount] = []
}
acc[parentAccount].push(account)
return acc
}, {} as Record<string, Account[]>)
return Object.entries(grouped).map(([parentAccount, accounts]) => ({
// Remove the last abbreviation from the parent account name like "Assets - TCC" should be "Assets", and "Assets - USD - TCC" should be "Assets - USD"
parentAccount: parentAccount.split(" - ").slice(0, -1).join(" - "),
accounts
}))
}, [data])
const searchIndex = useMemo(() => {
if (!data) {
return null
}
return new Fuse(data, {
keys: ['name'],
threshold: 0.5,
includeScore: true
})
}, [data])
const [search, setSearch] = useState("")
const recommendedAccounts = useMemo(() => {
if (!searchIndex || !search) {
return []
}
return searchIndex.search(search).map((result) => result.item)
}, [searchIndex, search])
const [open, setOpen] = useState(false)
const onOpenChange = (open: boolean) => {
if (readOnly) return
setOpen(open)
// setSearch("")
}
const onSelect = (value: string) => {
onChange?.(value)
setOpen(false)
setSearch(value)
}
const buttonRef = useRef<HTMLButtonElement>(null)
const [width, setWidth] = useState(320)
useLayoutEffect(() => {
if (buttonRef.current) {
setWidth(buttonRef.current.getBoundingClientRect().width)
}
}, [])
return (
<Popover open={open} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild>
{useInForm ? <FormControl>
<Button
variant="subtle"
type='button'
size={size}
role="combobox"
ref={buttonRef}
tabIndex={0}
disabled={disabled || readOnly}
aria-readonly={readOnly}
aria-expanded={open}
className={cn("w-full justify-between font-normal",
readOnly ? "bg-surface-gray-1 pointer-events-none" : ""
, buttonClassName)}>
{value || _('Select Account')}
<ChevronDownIcon className="ms-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
: <Button
variant="subtle"
size={size}
type='button'
role="combobox"
ref={buttonRef}
disabled={disabled}
aria-expanded={open}
className={cn("w-full justify-between font-normal",
readOnly ? "bg-surface-gray-1" : ""
)}>
{value || _('Select Account')}
<ChevronDownIcon className="ms-2 h-4 w-4 shrink-0 opacity-50" />
</Button>}
</PopoverTrigger>
<PopoverContent className="p-0" style={{ minWidth: width }} align="start">
<Command shouldFilter={false} className="w-full">
<CommandInput placeholder={_("Search account...")} onValueChange={setSearch} value={search} />
<CommandList>
<CommandEmpty>{_("No accounts found.")}</CommandEmpty>
{recommendedAccounts.length > 0 && (
<CommandGroup heading={_("Search Results")}>
{recommendedAccounts.map((account) => (
<CommandItem key={account.name} onSelect={() => onSelect(account.name)}>{account.name}</CommandItem>
))}
</CommandGroup>
)}
{!search && groupedAccounts.map((group) => (
<CommandGroup key={group.parentAccount} heading={group.parentAccount}>
{group.accounts.map((account) => (
<CommandItem key={account.name} onSelect={() => onSelect(account.name)}>{account.name}</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
interface Account {
name: string
root_type: 'Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense'
report_type: 'Balance Sheet' | 'Profit and Loss'
account_type: string
account_currency: string
parent_account: string
}
export const useGetAccounts = (root_type?: ('Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense')[], report_type?: 'Balance Sheet' | 'Profit and Loss', account_type?: string[], company?: string,
filterFunction?: (account: Account) => boolean) => {
const currentCompany = useCurrentCompany()
const { data, isLoading, error, mutate } = useFrappeGetDocList<Account>("Account", {
fields: ["name", "root_type", "report_type", "account_type", "account_currency", "parent_account"],
filters: [["is_group", "=", 0], ["disabled", "=", 0], ["company", "=", company ?? currentCompany]],
limit: 1000,
orderBy: {
"field": "root_type",
// @ts-expect-error - we can pass in additional fields to orderBy
"order": "asc, account_number asc"
}
}, `accounts-${company ?? currentCompany}`, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
const filteredData = useMemo(() => {
return data?.filter((account) => {
if (root_type && !root_type.includes(account.root_type)) return false
if (report_type && account.report_type !== report_type) return false
if (account_type && !account_type.includes(account.account_type)) return false
if (filterFunction) return filterFunction(account)
return true
}) ?? []
}, [data, root_type, report_type, account_type, filterFunction])
return { data: filteredData, isLoading, error, mutate }
}
export default AccountsDropdown

View File

@@ -1,26 +0,0 @@
import { cn } from '@/lib/utils'
import { SelectedBank } from '../features/BankReconciliation/bankRecAtoms'
import { useTheme } from '../ui/theme-provider'
import { Landmark } from 'lucide-react'
import { H4 } from '../ui/typography'
const BankLogo = ({ bank, className, imageClassName, iconSize = '18px', iconClassName }: { bank?: SelectedBank | null, className?: string, imageClassName?: string, iconSize?: string, iconClassName?: string }) => {
const { themeValue } = useTheme()
return (
<div className={cn('h-6 flex items-center gap-1', className)}> {bank?.logo ? <img
src={`/assets/erpnext/images/bank-logos/${themeValue === 'Dark' ? (bank.logoDark ?? bank.logo) : bank.logo}`}
alt={bank.bank || bank.name || ''}
className={cn("h-6 max-w-22 me-auto object-contain", imageClassName, {
'dark:invert dark:brightness-0': bank.darkModeInvert
}, bank.logoClassName)}
/> : <>
<Landmark size={iconSize} className={iconClassName} />
<H4 className={cn("text-xs -mb-0.5", {
})}>{bank?.bank ?? ''}</H4>
</>
}</div>
)
}
export default BankLogo

View File

@@ -1,17 +0,0 @@
import { CheckCircle } from 'lucide-react'
import { Progress } from '../ui/progress'
import _ from '@/lib/translate'
const FileUploadBanner = ({
uploadProgress,
}: { uploadProgress: number }) => {
return <div className="flex items-center justify-center flex-col gap-4">
<div className="flex flex-col items-center gap-4">
<CheckCircle size={48} className="text-ink-green-3" />
<span className="text-ink-gray-8 text-p-base">{_("The document has been created and reconciled. Uploading attachments...")}</span>
<Progress value={Math.round(uploadProgress * 100)} size="lg" />
</div>
</div>
}
export default FileUploadBanner

View File

@@ -1,301 +0,0 @@
import { useDocType } from "@/hooks/useDocType";
import { getSystemDefault, slug } from "@/lib/frappe";
import { Filter, useFrappeGetCall } from "frappe-react-sdk"
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import { canCreateDocument } from "@/lib/permissions";
import { useDebounceValue } from "usehooks-ts";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { FormControl } from "../ui/form";
import { ChevronDownIcon, ExternalLink } from "lucide-react";
import { Button } from "../ui/button";
import { cn } from "@/lib/utils";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../ui/command";
import _ from "@/lib/translate";
import ErrorBanner from "../ui/error-banner";
import MarkdownRenderer from "../ui/markdown";
export interface ResultItem {
value: string,
description: string,
label?: string
}
export interface LinkFieldComboboxProps {
/** DocType to be fetched */
doctype: string;
/** Filters to be applied. Default: none */
filters?: Filter[]
/** Number of records to paginate with. Default: Comes from System Settings or 10 */
limit?: number;
/**
* API to call to fetch records.
*
* Default: `frappe.desk.search.search_link`
*
* If you want to use a custom API, you can pass the path to the API here.
*
* The API should return a list of documents in the following format:
* [{value: string, description: string, label?: string}] - where the value is the ID of the document.
*
* If the API sends a label, it will be used as the label in the dropdown.
*/
searchAPIPath?: string;
/**
* Field you want to search against in the doctype.
*
* Default: `name`
*
* If you want to search against a different field, you can pass the fieldname here.
*
* If you want to search against multiple fields, you can try using the `searchAPIPath` prop to call a custom API,
* or use a custom query in the `customQuery` prop.
*/
searchfield?: string;
/**
* Custom query to be used to fetch records.
*
* If you want to use a custom query, you can pass the query here.
*
* The query should be in the following format:
* {
* query: string,
* filters: {
* fieldname: string,
* operator: string,
* value: string
* }
* }
*/
customQuery?: {
/** Path to function for the query.
*
* Refer: Item/Supplier query
*/
query: string,
/** Filters are usually an object instead of an array in a custom query */
filters?: Record<string, string | number | boolean>,
},
/**
* Used for certain queries where a reference doctype is needed.
*
* For example when searching a supplier in a "Purchase Invoice", the reference_doctype is "Purchase Invoice"
*/
reference_doctype?: string,
/** Placeholder for the dropdown. Default: `doctype` */
placeholder?: string;
/**
* Should the field be read-only.
*/
readOnly?: boolean;
/** Should the field be disabled. Default: false */
disabled?: boolean;
/**
* Function to filter the options based on the input value/other criteria.
*
* For example, you might want to limit the companies shown in the dropdown since they have been already added (like in Cost Codes)
*/
filterFn?: (option: ResultItem, inputValue: string) => boolean,
value?: string,
onChange: (value: string) => void,
/** If true, the component will be wrapped in a FormControl component */
useInForm?: boolean,
/** Button Class name */
buttonClassName?: string,
size?: 'sm' | 'md' | 'lg',
}
const LinkFieldCombobox = ({
doctype,
reference_doctype,
filters = [],
value,
onChange,
readOnly,
disabled,
filterFn,
placeholder = `Select ${doctype}`,
customQuery,
searchfield,
searchAPIPath = "frappe.desk.search.search_link",
limit,
useInForm,
buttonClassName,
size = 'md'
}: LinkFieldComboboxProps) => {
const pageLimit = useMemo(() => limit || getSystemDefault('link_field_results_limit') || 20, [limit])
/** Load the Doctype meta so that we can determine the search fields + the name of the title field */
const { data: meta } = useDocType(doctype)
const userCanCreate = useMemo(() => canCreateDocument(doctype), [doctype])
const [open, setOpen] = useState(false)
const [searchInput, setSearchInput] = useDebounceValue('', 400)
const { data: linkTitleData } = useFrappeGetCall('frappe.client.get_value', {
doctype,
filters: JSON.stringify({
name: value
}),
fieldname: meta?.title_field
}, (meta?.show_title_field_in_link ?? false) && (meta?.title_field) && value ? `link_title::${doctype}::${value}` : null, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
const linkTitle = meta?.title_field && meta?.show_title_field_in_link ? (linkTitleData?.message?.[meta?.title_field] ?? value) : value
const buttonRef = useRef<HTMLButtonElement>(null)
const [width, setWidth] = useState(320)
useLayoutEffect(() => {
if (buttonRef.current) {
setWidth(buttonRef.current.getBoundingClientRect().width)
}
}, [])
const { data, error, isLoading } = useFrappeGetCall<{ message: ResultItem[] }>(searchAPIPath, {
doctype,
txt: searchInput,
page_length: pageLimit,
query: customQuery?.query,
searchfield,
filters: JSON.stringify(customQuery?.filters || filters || []),
reference_doctype,
}, () => {
if (!open) {
return null
} else {
let key = `${searchAPIPath}_${doctype}_${searchInput}`
if (pageLimit) {
key += `_${pageLimit}`
}
if (customQuery?.filters) {
key += `_${JSON.stringify(customQuery.filters)}`
} else if (filters) {
key += `_${JSON.stringify(filters)}`
}
if (customQuery && customQuery.query) {
key += `_${customQuery.query}`
}
if (reference_doctype) {
key += `_${reference_doctype}`
}
if (searchfield && searchfield !== 'name') {
key += `_${searchfield}`
}
return key
}
}, {
revalidateOnFocus: false,
revalidateIfStale: false,
shouldRetryOnError: false,
revalidateOnReconnect: false,
})
const onOpenChange = (open: boolean) => {
if (readOnly) return
setOpen(open)
setSearchInput("")
}
const onSelect = (value: string) => {
onChange?.(value)
setOpen(false)
}
const items = filterFn ? data?.message?.slice(0, 50).filter((item) => filterFn(item, searchInput)) : data?.message
const buttonProps = {
variant: "subtle",
type: 'button',
size: size,
role: "combobox",
"data-state": open ? "open" : "closed",
ref: buttonRef,
tabIndex: 0,
disabled: disabled || readOnly,
"aria-expanded": open,
"aria-readonly": readOnly,
className: cn("w-full justify-between font-normal group border border-transparent outline-none",
"data-[state=open]:bg-surface-white data-[state=open]:border-outline-gray-4 data-[state=open]:shadow-sm",
readOnly ? "bg-surface-gray-1" : "",
// Placeholder and value styling
linkTitle ? "text-ink-gray-7" : "text-ink-gray-4",
buttonClassName)
} as const
return (
<Popover open={open} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild>
{useInForm ? <FormControl>
<Button {...buttonProps}>
{linkTitle || placeholder}
<div className="flex items-center gap-1">
{value && <a href={`/desk/${slug(doctype)}/${value}`} target="_blank" className="group-hover:block hidden">
<ExternalLink className="size-4 shrink-0 opacity-50" />
</a>}
<ChevronDownIcon className="ms-2 size-4 shrink-0" />
</div>
</Button>
</FormControl>
: <Button {...buttonProps}>
{linkTitle || placeholder}
<div className="flex items-center gap-1">
{value && <a href={`/desk/${slug(doctype)}/${value}`} target="_blank" className="group-hover:block hidden">
<ExternalLink className="size-4 shrink-0 opacity-50" />
</a>}
<ChevronDownIcon className="ms-2 size-4 shrink-0" />
</div>
</Button>}
</PopoverTrigger>
<PopoverContent className="p-0" style={{ minWidth: width }} align="start">
{error && <ErrorBanner error={error} />}
<Command shouldFilter={false} className="w-full">
<CommandInput placeholder={placeholder} onValueChange={setSearchInput} />
<CommandList>
<CommandEmpty>{isLoading ? _("Loading...") : _("No results found.")}</CommandEmpty>
<CommandGroup>
{items?.map((result) => (
<CommandItem key={result.value} onSelect={() => onSelect(result.value)} className="flex flex-col items-start gap-0.5">
<span className="font-medium">
{result.label || result.value}
</span>
{result.description && <span className="text-xs text-ink-gray-5">
<MarkdownRenderer content={result.description} />
</span>}
</CommandItem>
))}
{userCanCreate && <CommandItem asChild>
<a href={`/desk/${slug(doctype)}/new-${slug(doctype)}-1`}
target="_blank"
className="hover:underline underline-offset-4 cursor-pointer flex justify-between items-center">
{_("Create New {0}", [doctype])}
<ExternalLink />
</a>
</CommandItem>}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}
export default LinkFieldCombobox

View File

@@ -1,82 +0,0 @@
import { Select, SelectValue, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/select'
import _ from '@/lib/translate'
import { useFrappeGetDocList } from 'frappe-react-sdk'
import { ComponentProps, useMemo } from 'react'
import { FormControl } from '../ui/form'
export type PartyTypeDropdownProps = {
value?: string,
onChange?: (value: string) => void,
readOnly?: boolean,
disabled?: boolean,
/** Set this to order the parties so that suggested types are shown first */
type?: 'Receivable' | 'Payable'
/** Set this to true if you want to hide other options by type. e.g. - if type is Receivable, Payable options like "Supplier" will be hidden */
hideOptionsByType?: boolean,
valueProps?: ComponentProps<typeof SelectValue>,
triggerProps?: ComponentProps<typeof SelectTrigger>,
// If true, the component will be wrapped in a FormControl component
useInForm?: boolean
}
const PartyTypeDropdown = ({ value, onChange, readOnly, disabled, type, hideOptionsByType, valueProps, triggerProps, useInForm }: PartyTypeDropdownProps) => {
const { data } = useFrappeGetDocList("Party Type", {
fields: ['name', 'account_type'],
orderBy: {
field: 'creation',
order: 'asc'
}
}, `party_types`, {
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
})
const filteredData = useMemo(() => {
let options = data ?? [
{ name: "Customer", account_type: "Receivable" },
{ name: "Supplier", account_type: "Payable" },
{ name: "Employee", account_type: "Payable" },
{ name: "Shareholder", account_type: "Payable" },
]
if (hideOptionsByType && type) {
options = options.filter((option) => option.account_type === type)
}
// Order by type if type is set
if (type) {
options = options.sort((a) => a.account_type === type ? -1 : 1)
}
return options
}, [data, type, hideOptionsByType])
const onSelect = (value: string) => {
if (!readOnly) {
onChange?.(value)
}
}
return (
<Select onValueChange={onSelect} value={value} disabled={disabled}>
{useInForm ? <FormControl>
<SelectTrigger tabIndex={0} aria-readonly={readOnly} disabled={disabled || readOnly} {...triggerProps}>
<SelectValue placeholder={_("Type")} aria-readonly={readOnly} {...valueProps} />
</SelectTrigger>
</FormControl> : <SelectTrigger tabIndex={0} {...triggerProps}>
<SelectValue placeholder={_("Type")} aria-readonly={readOnly} {...valueProps} />
</SelectTrigger>
}
<SelectContent>
{filteredData.map((option) => (
<SelectItem key={option.name} value={option.name}>{option.name}</SelectItem>
))}
</SelectContent>
</Select>
)
}
export default PartyTypeDropdown

View File

@@ -1,475 +0,0 @@
import { Button } from '@/components/ui/button'
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import _ from '@/lib/translate'
import { useAtomValue, useSetAtom } from 'jotai'
import { ArrowDownRight, ArrowRightLeftIcon, ArrowUpRight, CalendarIcon, CircleXIcon, GitCompareIcon, HistoryIcon, LandmarkIcon, Loader2Icon, ReceiptIcon, ReceiptTextIcon, UserIcon, WalletIcon } from 'lucide-react'
import { useMemo, useState } from 'react'
import { ActionLogItem, ActionLog as ActionLogType, bankRecActionLog, bankRecDateAtom, bankRecMatchFilters, SelectedBank, selectedBankAccountAtom } from '../BankReconciliation/bankRecAtoms'
import { useHotkeys } from 'react-hotkeys-hook'
import { useGetBankAccounts } from '../BankReconciliation/utils'
import { getCompanyCurrency } from '@/lib/company'
import { formatCurrency } from '@/lib/numbers'
import dayjs from 'dayjs'
import { cn } from '@/lib/utils'
import { formatDate } from '@/lib/date'
import { Separator } from '@/components/ui/separator'
import { slug } from '@/lib/frappe'
import { PaymentEntry } from '@/types/Accounts/PaymentEntry'
import { JournalEntry } from '@/types/Accounts/JournalEntry'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Table, TableCell, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
import { useFrappePostCall, useSWRConfig } from 'frappe-react-sdk'
import { toast } from 'sonner'
import { getErrorMessage } from '@/lib/frappe'
import ErrorBanner from '@/components/ui/error-banner'
import SelectedTransactionDetails from '../BankReconciliation/SelectedTransactionDetails'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
import BankLogo from '@/components/common/BankLogo'
const ActionLog = () => {
const [isOpen, setIsOpen] = useState(false)
useHotkeys('meta+z', () => {
setIsOpen(true)
}, {
enabled: true,
enableOnFormTags: false,
preventDefault: true
})
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant={'outline'} isIconButton size='md'>
<HistoryIcon />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
{_("Reconciliation History")}
</TooltipContent>
</Tooltip>
<DialogContent className='min-w-[90vw]'>
<DialogHeader>
<DialogTitle>{_("Reconciliation History")}</DialogTitle>
<DialogDescription>{_("View all reconciliation actions taken in this session.")}</DialogDescription>
</DialogHeader>
<ActionLogDialogContent />
<DialogFooter>
<DialogClose asChild>
<Button variant={'outline'} size='md' onClick={() => setIsOpen(false)}>{_("Close")}</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
const ActionLogDialogContent = () => {
const actionLog = useAtomValue(bankRecActionLog)
return <div className='flex flex-col gap-2'>
{actionLog.map((action) => (
<div key={action.timestamp} className='flex flex-col gap-1'>
<ActionGroupHeader action={action} />
<div>
<div className='ms-2 border-s border-s-outline-gray-2 py-1'>
<div className='ms-5'>
{action.items.map((item, index) => (
<Row
item={item}
key={item.bankTransaction.name}
index={index}
action={action}
isLast={index === action.items.length - 1} />
))}
</div>
</div>
</div>
</div>
))}
{actionLog.length === 0 && <Empty>
<EmptyMedia>
<HistoryIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No reconciliation actions found")}</EmptyTitle>
<EmptyDescription>{_("You have not performed any reconciliations in this session yet.")}</EmptyDescription>
</EmptyHeader>
</Empty>}
</div>
}
const ActionGroupHeader = ({ action }: { action: ActionLogType }) => {
const label = useMemo(() => {
switch (action.type) {
case 'match':
return _("Matched")
case 'payment':
if (action.isBulk) {
return _("Bulk Payment")
}
return _("Payment")
case 'transfer':
if (action.isBulk) {
return _("Bulk Transfer")
}
return _("Transfer")
case 'bank_entry':
if (action.isBulk) {
return _("Bulk Bank Entry")
}
return _("Bank Entry")
default:
return _("Action")
}
}, [action])
return <div className='flex items-center gap-2 text-ink-gray-5'>
{action.type === 'match' && <GitCompareIcon className='w-4 h-4' />}
{action.type === 'payment' && <ReceiptIcon className='w-4 h-4' />}
{action.type === 'transfer' && <ArrowRightLeftIcon className='w-4 h-4' />}
{action.type === 'bank_entry' && <LandmarkIcon className='w-4 h-4' />}
<span className='flex items-center gap-2 text-sm'>
{label} - {dayjs(action.timestamp).fromNow()}
</span>
</div>
}
const Row = ({ item, index, isLast, action }: { item: ActionLogItem, index: number, isLast: boolean, action: ActionLogType }) => {
const isWithdrawal = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0
const { banks } = useGetBankAccounts()
const bank = useMemo(() => {
if (item.bankTransaction.bank_account) {
return banks?.find((bank) => bank.name === item.bankTransaction.bank_account)
}
return null
}, [item.bankTransaction.bank_account, banks])
const amount = item.bankTransaction.withdrawal ? item.bankTransaction.withdrawal : item.bankTransaction.deposit
const currency = item.bankTransaction.currency || getCompanyCurrency(item.bankTransaction.company ?? '')
return <div className='flex items-center gap-2 group'>
<div className={cn('p-3.5 group-hover:bg-surface-gray-1 border-s border-e border-t w-full', isLast ? 'rounded-b border-b' : '', index === 0 ? 'rounded-t' : '')}>
<div className='flex justify-between items-center'>
<div className='flex flex-col gap-2'>
<p className='text-p-base'>{item.bankTransaction.description}</p>
<div className='flex items-center gap-3'>
<div className='flex gap-2 items-center'>
<BankLogo bank={bank} className='h-4 mb-0' iconSize='16px' />
<span className='text-sm text-ink-gray-5'>{item.bankTransaction.bank_account}</span>
</div>
<Separator orientation='vertical' />
<div className='flex items-center gap-2 text-ink-gray-5 text-sm' title={_("Transaction Date")}>
<CalendarIcon className='w-4 h-4' />
<span className='text-sm'>{formatDate(item.bankTransaction.date, 'Do MMM YYYY')}</span>
</div>
<Separator orientation='vertical' />
<div>
<div className='flex items-center gap-1' title={isWithdrawal ? _("Spent") : _("Received")}>
{isWithdrawal ? <ArrowUpRight className="w-5 h-5 text-ink-red-3" /> : <ArrowDownRight className="w-5 h-5 text-ink-green-3" />}
<span className='text-sm text-ink-gray-5'>{formatCurrency(amount, currency)}</span>
</div>
</div>
</div>
</div>
<div className='flex justify-end items-center gap-2'>
<div className='text-end flex flex-col gap-2'>
<a
href={`/desk/${slug(item.voucher.reference_doctype)}/${item.voucher.reference_name}`}
target='_blank'
className='underline underline-offset-4 text-base'>
{["Payment Entry", "Journal Entry"].includes(item.voucher.reference_doctype) ? "" : _("{} :", [item.voucher.reference_doctype])} {item.voucher.reference_name}
</a>
{item.voucher.reference_doctype === "Payment Entry" && item.voucher.doc && <PaymentEntryDetails item={item} />}
{item.voucher.reference_doctype === "Journal Entry" && <JournalEntryDetails item={item} bank={bank} />}
</div>
</div>
</div>
</div>
<div className='w-10 h-10 flex items-center justify-center'>
<CancelActionLogItem item={item} type={action.type} timestamp={action.timestamp} bank={bank} />
</div>
</div>
}
const JournalEntryDetails = ({ item, bank }: { item: ActionLogItem, bank?: SelectedBank | null }) => {
return <div className='flex items-center gap-2 text-ink-gray-5 justify-end'>
<WalletIcon className='w-4 h-4' />
<JournalEntryAccountsTable item={item} bank={bank} />
</div>
}
const JournalEntryAccountsTable = ({ item, bank }: { item: ActionLogItem, bank?: SelectedBank | null }) => {
const accounts = useMemo(() => {
const allAccounts = (item.voucher.doc as JournalEntry).accounts
return allAccounts.filter((acc) => bank ? acc.account !== bank.account : true)
}, [item, bank])
return <>
{accounts.length === 1 ? <span className='text-sm'>{accounts[0].account}</span> :
<HoverCard>
<HoverCardTrigger>
<span className='text-sm cursor-pointer hover:underline underline-offset-4'>{_("Split across {} accounts", [accounts.length.toString()])}</span>
</HoverCardTrigger>
<HoverCardContent className='w-full p-2' align='end'>
<Table>
<TableHeader>
<TableRow>
<TableHead>{_("Account")}</TableHead>
<TableHead className='text-end'>{_("Debit")}</TableHead>
<TableHead className='text-end'>{_("Credit")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{accounts.map((account) => (
<TableRow key={account.account}>
<TableCell>{account.account}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(account.debit ?? 0, account.account_currency ?? '')}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(account.credit ?? 0, account.account_currency ?? '')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</HoverCardContent>
</HoverCard>
}</>
}
const PaymentEntryDetails = ({ item, className }: { item: ActionLogItem, className?: string }) => {
if ((item.voucher.doc as PaymentEntry).payment_type === "Internal Transfer") {
return <TransferDetails item={item} className={className} />
}
const invoices = (item.voucher.doc as PaymentEntry).references ?? []
const currency = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0 ? (item.voucher.doc as PaymentEntry)?.paid_to_account_currency : (item.voucher.doc as PaymentEntry)?.paid_from_account_currency
return <div className='flex items-center gap-3'>
<div className={cn('flex items-center gap-2 text-ink-gray-5 text-sm', className)}>
<UserIcon className='w-4 h-4' />
<span className='text-sm'>{(item.voucher.doc as PaymentEntry).party_name}</span>
</div>
<Separator orientation='vertical' />
<HoverCard>
<HoverCardTrigger>
<div className={cn('flex items-center gap-2 text-ink-gray-5 text-sm', className)}>
<ReceiptTextIcon className='w-4 h-4' />
<span className='text-sm cursor-pointer hover:underline underline-offset-4'>{invoices.length === 0 ? _("No invoice linked") : invoices.length === 1 ? _("1 invoice") : _("{} invoices", [invoices.length.toString()])}</span>
</div>
</HoverCardTrigger>
<HoverCardContent className='w-full p-2' align='end'>
<div className='flex flex-col gap-2'>
{invoices.map((invoice) => (
<Table>
<TableHeader>
<TableRow>
<TableHead>{_("Document")}</TableHead>
<TableHead>{_("Invoice No")}</TableHead>
<TableHead>{_("Due Date")}</TableHead>
<TableHead className='text-end'>{_("Grand Total")}</TableHead>
<TableHead className='text-end'>{_("Allocated")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell><a href={`/desk/${slug(invoice.reference_doctype)}/${invoice.reference_name}`} target='_blank' className='underline underline-offset-4'>{invoice.reference_doctype}: {invoice.reference_name}</a></TableCell>
<TableCell>{invoice.bill_no ?? "-"}</TableCell>
<TableCell>{formatDate(invoice.due_date)}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(invoice.total_amount, currency ?? '')}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(invoice.allocated_amount, currency ?? '')}</TableCell>
</TableRow>
</TableBody>
</Table>
))}
</div>
</HoverCardContent>
</HoverCard>
</div>
}
const TransferDetails = ({ item, className }: { item: ActionLogItem, className?: string }) => {
const { banks } = useGetBankAccounts()
const bank = useMemo(() => {
const isWithdrawal = item.bankTransaction.withdrawal && item.bankTransaction.withdrawal > 0
let transferAccount = ""
if (isWithdrawal) {
transferAccount = (item.voucher.doc as PaymentEntry).paid_to
} else {
transferAccount = (item.voucher.doc as PaymentEntry).paid_from
}
const transferBankAccount = banks?.find((bank) => bank.account === transferAccount)
return transferBankAccount
}, [banks, item])
return <div className={cn('flex items-center gap-2 text-ink-gray-5 text-sm', className)}>
<BankLogo bank={bank} className='h-5 mb-0' iconSize='16px' imageClassName='max-h-5' />
<span className='text-sm'>{bank?.account}</span>
</div>
}
const ACTION_TYPE_MAP = {
'bank_entry': _("Bank Entry"),
'payment': _("Payment"),
'transfer': _("Transfer"),
'match': _("Match"),
}
const CancelActionLogItem = ({ item, type, timestamp, bank }: { item: ActionLogItem, type: ActionLogType['type'], timestamp: number, bank?: SelectedBank | null }) => {
const [isOpen, setIsOpen] = useState(false)
const { call, loading, error } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unreconcile_transaction_entry')
const { mutate } = useSWRConfig()
const actionLog = useSetAtom(bankRecActionLog)
const dates = useAtomValue(bankRecDateAtom)
const matchFilters = useAtomValue(bankRecMatchFilters)
const selectedBank = useAtomValue(selectedBankAccountAtom)
const onUndo = () => {
call({
bank_transaction_id: item.bankTransaction.name,
voucher_type: item.voucher.reference_doctype,
voucher_id: item.voucher.reference_name,
}).then(() => {
toast.success(type === 'match' ? _("Unmatched") : _("Cancelled"))
if (selectedBank?.name === item.bankTransaction.bank_account) {
mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`)
mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`)
// Update the matching vouchers for the selected transaction
mutate(`bank-reconciliation-vouchers-${item.bankTransaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`)
}
setTimeout(() => {
actionLog((prev) => {
// Find the action and then remove the item from the action. If the action is empty, remove the action from the array
const action = prev.find((action) => action.timestamp === timestamp)
if (action) {
action.items = action.items.filter((i) => i.bankTransaction.name !== item.bankTransaction.name)
}
// If the action is empty, remove the action from the array
if (action && action.items.length === 0) {
return prev.filter((a) => a.timestamp !== timestamp)
} else {
return prev.map((a) => a.timestamp === timestamp ? { ...a, items: action?.items ?? [] } : a)
}
})
}, 100)
setIsOpen(false)
}).catch((error) => {
toast.error(_("There was an error while performing the action."), {
duration: 5000,
description: getErrorMessage(error),
})
})
}
return <AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<AlertDialogTrigger asChild>
<Button
variant={'ghost'}
isIconButton
theme='red'
title={_("Cancel")}
className='hover:text-ink-red-3 hover:bg-destructive/5 text-ink-gray-5 hidden group-hover:inline-flex'>
<CircleXIcon className='w-8 h-8' />
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent>
{_("Cancel")}
</TooltipContent>
</Tooltip>
<AlertDialogContent className='min-w-3xl'>
<AlertDialogHeader>
<AlertDialogTitle>{type === 'match' ? _("Unmatch Transaction?") : _("Undo {}?", [item.voucher.reference_doctype])}</AlertDialogTitle>
<AlertDialogDescription>{type === 'match' ? _("Are you sure you want to unmatch the voucher from this transaction?") : _("Are you sure you want to cancel this {} {}?", [_(item.voucher.reference_doctype), item.voucher.reference_name])}</AlertDialogDescription>
</AlertDialogHeader>
{error && <ErrorBanner error={error} />}
<div className='flex flex-col gap-2'>
<SelectedTransactionDetails transaction={item.bankTransaction} />
<Table>
<TableRow>
<TableHead>{_("Action Type")}</TableHead>
<TableCell>{ACTION_TYPE_MAP[type]}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Voucher Type")}</TableHead>
<TableCell>{_(item.voucher.reference_doctype)}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Voucher Name")}</TableHead>
<TableCell><a href={`/desk/${slug(item.voucher.reference_doctype)}/${item.voucher.reference_name}`} target='_blank' className='underline underline-offset-4'>{item.voucher.reference_name}</a></TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Posting Date")}</TableHead>
<TableCell>{formatDate(item.voucher.posting_date, 'Do MMM YYYY')}</TableCell>
</TableRow>
{type === 'transfer' && item.voucher.doc && <TableRow>
<TableHead>{_("Transfer Account")}</TableHead>
<TableCell>
<TransferDetails item={item} className='text-ink-gray-8' />
</TableCell>
</TableRow>}
{type === 'payment' && item.voucher.doc && <TableRow>
<TableHead>{_("Payment Details")}</TableHead>
<TableCell>
<PaymentEntryDetails item={item} className='text-ink-gray-8' />
</TableCell>
</TableRow>}
{type === 'bank_entry' && item.voucher.doc && <TableRow>
<TableHead>{_("Account")}</TableHead>
<TableCell><JournalEntryAccountsTable item={item} bank={bank} /></TableCell>
</TableRow>}
</Table>
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>
{_("Close")}
</AlertDialogCancel>
<Button theme="red" size='md' disabled={loading} onClick={onUndo}>
{loading ? <Loader2Icon className='w-4 h-4 animate-spin' /> : _(("Undo"))}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
}
export default ActionLog

View File

@@ -1,334 +0,0 @@
import { useAtomValue, useSetAtom } from "jotai"
import { bankRecClosingBalanceAtom, bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
import { FrappeConfig, FrappeContext, useFrappeGetDocCount, useFrappeGetDocList, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
import { BankTransaction } from "@/types/Accounts/BankTransaction"
import { Progress } from "@/components/ui/progress"
import { useGetAccountClosingBalance, useGetAccountClosingBalanceAsPerStatement, useGetAccountOpeningBalance, useGetUnreconciledTransactions } from "./utils"
import { flt, formatCurrency } from "@/lib/numbers"
import { Skeleton } from "@/components/ui/skeleton"
import { StatContainer, StatLabel, StatValue } from "@/components/ui/stats"
import { Edit, Info, Trash2 } from "lucide-react"
import { H4, Paragraph } from "@/components/ui/typography"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { getCompanyCurrency } from "@/lib/company"
import _ from "@/lib/translate"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { formatDate } from "@/lib/date"
import { Form } from "@/components/ui/form"
import { CurrencyFormField } from "@/components/ui/form-elements"
import { useForm } from "react-hook-form"
import { Button } from "@/components/ui/button"
import { useContext, useState } from "react"
import { Separator } from "@/components/ui/separator"
import { BankAccountBalance } from "@/types/Accounts/BankAccountBalance"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { toast } from "sonner"
import ErrorBanner from "@/components/ui/error-banner"
const BankBalance = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
if (!bankAccount) {
return null
}
return (
<div className="flex justify-between">
<div className="w-[80%] flex flex-wrap justify-between gap-2 pe-8 border-e-border border-e">
<OpeningBalance />
<ClosingBalance />
<ClosingBalanceAsPerStatement />
<Difference />
</div>
<ReconcileProgress />
</div>
)
}
const OpeningBalance = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const { data, isLoading } = useGetAccountOpeningBalance()
return <StatContainer className="min-w-48">
<StatLabel>{_("Opening Balance")}</StatLabel>
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
</StatContainer>
}
const ClosingBalance = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const { data, isLoading } = useGetAccountClosingBalance()
return (
<StatContainer className="min-w-48">
<div className="flex items-start gap-1">
<StatLabel>
{_("Closing Balance as per system")}
</StatLabel>
<HoverCard openDelay={100}>
<HoverCardTrigger>
<Info className="size-3.5 text-ink-gray-6 -mt-px" />
</HoverCardTrigger>
<HoverCardContent className="w-96" align="start" side="right">
<H4 className="text-base">{_("Closing balance as per system")}</H4>
<Paragraph className="mt-2 text-p-sm">
{_("This is what the system expects the closing balance to be in your bank statement.")}
<br />
{_("It takes into account all the transactions that have been posted and subtracts the transactions that have not cleared yet.")}
<br />
{_("If your bank statement shows a different closing balance, it is because all transactions have not reconciled yet.")}
<br /><br />
For more information, click on the <strong>Bank Reconciliation Statement</strong> tab below.
</Paragraph>
</HoverCardContent>
</HoverCard>
</div>
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
</StatContainer>
)
}
const Difference = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const { data, isLoading } = useGetAccountClosingBalance()
const value = useAtomValue(bankRecClosingBalanceAtom(bankAccount?.name ?? ''))
const difference = flt(value.value - (data?.message ?? 0))
const isError = difference !== 0
return <StatContainer className="w-fit text-end sm:min-w-56">
<StatLabel className="text-end">{_("Difference")}</StatLabel>
{isLoading ? <Skeleton className="w-[150px] h-5 self-end rounded-sm" /> : <StatValue className={isError ? 'text-ink-red-3 font-numeric' : 'font-numeric'}>
{formatCurrency(difference,
bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))
}</StatValue>}
</StatContainer>
}
const ReconcileProgress = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const { data: totalCount } = useFrappeGetDocCount<BankTransaction>('Bank Transaction', [
["bank_account", "=", bankAccount?.name ?? ''],
['docstatus', '=', 1],
['date', '<=', dates?.toDate],
['date', '>=', dates?.fromDate]
], false, undefined, {
revalidateOnFocus: false
})
const { data: unreconciledTransactions, } = useGetUnreconciledTransactions()
const reconciledCount = (totalCount ?? 0) - (unreconciledTransactions?.message?.length ?? 0)
const progress = (totalCount ? reconciledCount / totalCount : 0) * 100
return <div className="w-[18%] flex flex-col gap-1 items-end">
<div className="w-full">
<Progress
value={progress}
max={100}
size="md"
label="Progress"
hint
hintText={`${reconciledCount} / ${totalCount} ${_("reconciled")}`} />
</div>
</div>
}
const ClosingBalanceAsPerStatement = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const setValue = useSetAtom(bankRecClosingBalanceAtom(bankAccount?.name ?? ''))
const { data, isLoading } = useGetAccountClosingBalanceAsPerStatement({
onSuccess: (data) => {
if (data?.message && data?.message?.balance) {
setValue({
value: data?.message?.balance,
stringValue: data?.message?.balance.toString()
})
}
}
})
const isDateSame = data?.message?.date === dates.toDate
const [isOpen, setIsOpen] = useState(false)
return <StatContainer className="min-w-48">
<StatLabel>{_("Closing Balance as per statement")}</StatLabel>
<div className="flex flex-col gap-2 items-start">
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-4 underline cursor-pointer underline-offset-6" role="button">
{isLoading ? <Skeleton className="w-[150px] h-5 rounded-sm" /> : <StatValue className="font-numeric">{formatCurrency(flt(data?.message?.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</StatValue>}
<Edit className="w-4 h-4" />
</div>
</TooltipTrigger>
<TooltipContent>
{_("Click to set the closing balance as per statement")}
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent className="min-w-xl">
<ClosingBalanceForm
defaultBalance={data?.message?.balance ?? 0}
date={dates.toDate}
bankAccount={bankAccount}
onClose={() => setIsOpen(false)}
/>
</DialogContent>
</Dialog>
{!isDateSame && data?.message.date && <span className="text-xs font-medium text-ink-red-3">{_("As of {0}", [formatDate(data?.message?.date ?? '', 'Do MMM YYYY')])}</span>}
</div>
</StatContainer>
}
const ClosingBalanceForm = ({ defaultBalance, date, bankAccount, onClose }: { defaultBalance: number, date: string, bankAccount: SelectedBank | null, onClose: VoidFunction }) => {
const { mutate } = useSWRConfig()
const form = useForm<{ balance: number }>({
defaultValues: {
balance: defaultBalance
}
})
const setValue = useSetAtom(bankRecClosingBalanceAtom(bankAccount?.name ?? ''))
const { call, loading, error } = useFrappePostCall("erpnext.accounts.doctype.bank_account.bank_account.set_closing_balance_as_per_statement")
const onSubmit = (data: { balance: number }) => {
if (data.balance) {
call({
bank_account: bankAccount?.name ?? '',
date: date,
balance: data.balance
})
.then(() => {
// Mutate the closing balance as per statement
mutate(`bank-reconciliation-account-closing-balance-as-per-statement-${bankAccount?.name}-${date}`)
setValue({
value: data.balance,
stringValue: data.balance.toString()
})
toast.success(_("Closing balance set."))
onClose()
})
} else {
toast.error(_("Closing balance is required."))
}
}
const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '')
return <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>{_("Set closing balance as per bank statement")}</DialogTitle>
<DialogDescription>
{_("Enter the closing balance you see in your bank statement for {0} as of the {1}", [bankAccount?.account_name ?? bankAccount?.name ?? '', formatDate(date, 'Do MMM YYYY')])}
</DialogDescription>
</DialogHeader>
{error && <ErrorBanner error={error} />}
<div className="py-4">
<CurrencyFormField
name="balance"
label={_("Closing balance on bank statement as of {0}", [formatDate(date, 'Do MMM YYYY')])}
isRequired
currency={currency}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant={'outline'} size='md' disabled={loading}>{_("Cancel")}</Button>
</DialogClose>
<Button type='submit' size='md' disabled={loading}>{_("Save")}</Button>
</DialogFooter>
<ClosingBalancesList bankAccount={bankAccount} date={date} />
</form>
</Form>
}
const ClosingBalancesList = ({ bankAccount, date }: { bankAccount: SelectedBank | null, date: string }) => {
const { data, mutate } = useFrappeGetDocList<BankAccountBalance>("Bank Account Balance", {
filters: [["bank_account", "=", bankAccount?.name ?? ''], ["date", "<=", date]],
orderBy: {
field: "date",
order: "desc"
},
fields: ["date", "balance", "name"],
limit: 10
})
const { db } = useContext(FrappeContext) as FrappeConfig
const onDelete = (name: string) => {
toast.promise(db.deleteDoc("Bank Account Balance", name).then(() => {
mutate()
}), {
loading: _("Deleting closing balance..."),
success: _("Closing balance deleted."),
error: _("Failed to delete closing balance.")
})
}
if (data?.length === 0) {
return null
}
return <div>
<Separator className="my-8" />
<p className="text-sm text-center">{_("Balances as per bank statement before {0}", [formatDate(date, 'Do MMM YYYY')])}</p>
<Table>
<TableHeader>
<TableRow>
<TableHead>{_("Date")}</TableHead>
<TableHead className="text-end">{_("Balance")}</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((item) => (
<TableRow key={item.name}>
<TableCell>{formatDate(item.date, 'Do MMM YYYY')}</TableCell>
<TableCell className="text-end">{formatCurrency(flt(item.balance, 2), bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ''))}</TableCell>
<TableCell className="text-end">
<Button
title={_("Delete")}
type='button' isIconButton variant='ghost' onClick={() => onDelete(item.name)}>
<Trash2 />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
}
export default BankBalance

View File

@@ -1,355 +0,0 @@
import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo, useState } from "react"
import { useFrappeGetCall, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
import { QueryReportReturnType } from "@/types/custom/Reports"
import { formatDate } from "@/lib/date"
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
import { Table, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { formatCurrency } from "@/lib/numbers"
import { getCompanyCurrency } from "@/lib/company"
import { slug } from "@/lib/frappe"
import { CheckCircle2, ReceiptTextIcon, XCircle } from "lucide-react"
import ErrorBanner from "@/components/ui/error-banner"
import { Badge } from "@/components/ui/badge"
import _ from "@/lib/translate"
import { useCopyToClipboard } from "usehooks-ts"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Form } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { DateField } from "@/components/ui/form-elements"
import { Empty, EmptyMedia, EmptyHeader, EmptyTitle, EmptyDescription } from "@/components/ui/empty"
const BankClearanceSummary = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
if (!bankAccount) {
return <MissingFiltersBanner text={_("Please select a bank account to view the bank clearance summary.")} />
}
if (!dates) {
return <MissingFiltersBanner text={_("Please select dates to view the bank clearance summary.")} />
}
return <BankClearanceSummaryView />
}
interface BankClearanceSummaryEntry {
payment_document_type: string
payment_entry: string
posting_date: string,
cheque_no?: string,
amount: number,
against: string,
clearance_date: string,
}
const BankClearanceSummaryView = () => {
const companyID = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const filters = useMemo(() => {
return JSON.stringify({
account: bankAccount?.account,
from_date: dates.fromDate,
to_date: dates.toDate
})
}, [bankAccount, dates])
const { data, error, mutate } = useFrappeGetCall<{ message: QueryReportReturnType<BankClearanceSummaryEntry> }>('frappe.desk.query_report.run', {
report_name: 'Bank Clearance Summary',
filters,
ignore_prepared_report: 1,
are_default_filters: false,
}, `Report-Bank Clearance Summary-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST')
const formattedFromDate = formatDate(dates.fromDate)
const formattedToDate = formatDate(dates.toDate)
const [, copyToClipboard] = useCopyToClipboard()
const onCopy = useCallback(
(text: string) => {
copyToClipboard(text).then(() => {
toast.success(_("Copied to clipboard"))
})
},
[copyToClipboard, _],
)
const accountCurrency = useMemo(
() => bankAccount?.account_currency ?? getCompanyCurrency(companyID),
[bankAccount?.account_currency, companyID],
)
const clearanceColumns = useMemo<ColumnDef<BankClearanceSummaryEntry, unknown>[]>(
() => [
{
accessorKey: "payment_document_type",
header: _("Document Type"),
size: 140,
cell: ({ row }) => _(row.original.payment_document_type),
},
{
id: "payment_entry",
header: _("Payment Document"),
size: 160,
meta: {
getTooltipText: (r) => {
const x = r as BankClearanceSummaryEntry
return [x.payment_document_type, x.payment_entry].filter(Boolean).join(" · ") || undefined
},
} satisfies ListViewColumnMeta,
cell: ({ row }) => (
<a
target="_blank"
rel="noreferrer"
className="text-ink-gray-8 block min-w-0 w-full underline underline-offset-4"
href={`/desk/${slug(row.original.payment_document_type)}/${row.original.payment_entry}`}
>
{row.original.payment_entry}
</a>
),
},
{
accessorKey: "posting_date",
header: _("Posting Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.posting_date),
},
{
accessorKey: "cheque_no",
header: _("Cheque/Reference Number"),
size: 160,
cell: ({ row }) => {
const ref = row.original.cheque_no ?? ""
return (
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<button
type="button"
className="text-ink-gray-8 hover:underline min-w-0 w-full cursor-pointer truncate text-start underline-offset-4"
onClick={() => onCopy(ref)}
>
{ref}
</button>
</TooltipTrigger>
<TooltipContent>
{ref}
</TooltipContent>
</Tooltip>
)
},
},
{
accessorKey: "clearance_date",
header: _("Clearance Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.clearance_date),
},
{
accessorKey: "against",
header: _("Against Account"),
size: 250,
},
{
accessorKey: "amount",
header: _("Amount"),
size: 150,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.amount, accountCurrency)}</span>,
},
{
id: "status",
header: _("Status"),
size: 200,
meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta,
cell: ({ row }) => {
const r = row.original
return r.clearance_date ? (
<Badge theme="green">
<CheckCircle2 />
{_("Cleared")}
</Badge>
) : (
<div className="flex min-w-0 flex-wrap items-center gap-2">
<Badge theme="red">
<XCircle />
{_("Not Cleared")}
</Badge>
<SetClearanceDateButton
voucher={r}
bankAccount={bankAccount}
companyID={companyID}
mutate={mutate}
/>
</div>
)
},
},
],
[_, accountCurrency, bankAccount, companyID, mutate, onCopy],
)
return <div className="space-y-4 py-2">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("Below is a list of all accounting entries posted against the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
</Paragraph>
</div>
{error && <ErrorBanner error={error} />}
{data && data.message.result.length > 0 ? (
<ListView
data={data.message.result}
columns={clearanceColumns}
getRowId={(row) => `${row.payment_entry}-${row.posting_date}`}
maxHeight="calc(100vh - 200px)"
scrollAreaClassName="min-h-[calc(100vh-200px)]"
emptyState={_("No rows to display.")}
/>
) : null}
{data && data.message.result.length == 0 &&
<Empty>
<EmptyMedia>
<ReceiptTextIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No entries found")}</EmptyTitle>
<EmptyDescription>{_("There are no accounting entries in the system for the selected account and dates.")}</EmptyDescription>
</EmptyHeader>
</Empty>
}
</div>
}
const SetClearanceDateButton = ({ voucher, bankAccount, companyID, mutate }: { voucher: BankClearanceSummaryEntry, bankAccount: SelectedBank | null, companyID: string, mutate: VoidFunction }) => {
const [open, setOpen] = useState(false)
const onClose = () => {
setOpen(false)
mutate()
}
return <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger disabled={!bankAccount}>
<Tooltip delayDuration={500}>
<TooltipTrigger>
<Button variant='link' size="sm" className="px-0" theme="red">{_("Force Clear")}</Button>
</TooltipTrigger>
<TooltipContent align='start'>
{_("Set the clearance date for this voucher without reconciling with a bank transaction.")}
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent className="min-w-2xl">
{bankAccount && <ForceClearVoucherForm voucher={voucher} bankAccount={bankAccount} companyID={companyID} onClose={onClose} />}
</DialogContent>
</Dialog>
}
const ForceClearVoucherForm = ({ voucher, bankAccount, companyID, onClose }: { voucher: BankClearanceSummaryEntry, bankAccount: SelectedBank, companyID: string, onClose: () => void }) => {
const { mutate } = useSWRConfig()
const dates = useAtomValue(bankRecDateAtom)
const form = useForm<{ clearance_date: string }>({
defaultValues: {
clearance_date: voucher.posting_date,
}
})
const { call, loading, error } = useFrappePostCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_clearance_date')
const onSubmit = (data: { clearance_date: string }) => {
call({
payment_document: voucher.payment_document_type,
payment_entry: voucher.payment_entry,
account: bankAccount.account,
clearance_date: data.clearance_date,
})
.then(() => {
toast.success(_("Clearance date updated"))
onClose()
mutate(`bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`)
})
}
return <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='flex flex-col gap-4'>
<DialogHeader>
<DialogTitle>{_("Force Clear Voucher")}</DialogTitle>
<DialogDescription>
{_("Set the clearance date for this voucher without reconciling with a bank transaction.")}
</DialogDescription>
</DialogHeader>
{error && <ErrorBanner error={error} />}
<div>
<Table>
<TableHeader>
<TableRow>
<TableHead>{_("Payment Document")}</TableHead>
<TableCell><a target="_blank" className="underline underline-offset-4"
href={`/desk/${slug(voucher.payment_document_type)}/${voucher.payment_entry}`}>{_(voucher.payment_document_type)} : {voucher.payment_entry}</a></TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Posting Date")}</TableHead>
<TableCell>{formatDate(voucher.posting_date)}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Cheque/Reference Number")}</TableHead>
<TableCell title={voucher.cheque_no}>{voucher.cheque_no?.slice(0, 40)}{voucher.cheque_no?.length && voucher.cheque_no?.length > 40 ? "..." : ""}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Amount")}</TableHead>
<TableCell className="text-end">{formatCurrency(voucher.amount, bankAccount?.account_currency ?? getCompanyCurrency(companyID))}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Against Account")}</TableHead>
<TableCell><a target="_blank" className="underline underline-offset-4" href={`/desk/account/${voucher.against}`}>{voucher.against}</a></TableCell>
</TableRow>
</TableHeader>
</Table>
</div>
<DateField
name='clearance_date'
label={_("Clearance Date")}
isRequired
inputProps={{ autoFocus: true }}
/>
<DialogFooter>
<DialogClose asChild>
<Button variant={'outline'} disabled={loading} size='md'>{_("Cancel")}</Button>
</DialogClose>
<Button type='submit' disabled={loading} size='md'>{_("Submit")}</Button>
</DialogFooter>
</div>
</form>
</Form>
}
export default BankClearanceSummary

View File

@@ -1,831 +0,0 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai"
import { bankRecRecordJournalEntryModalAtom, bankRecSelectedTransactionAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { Dialog, DialogContent, DialogTitle, DialogDescription, DialogHeader, DialogFooter, DialogClose } from "@/components/ui/dialog"
import _ from "@/lib/translate"
import { UnreconciledTransaction, useGetRuleForTransaction, useRefreshUnreconciledTransactions, useUpdateActionLog } from "./utils"
import { useFieldArray, useForm, useFormContext, useWatch } from "react-hook-form"
import { JournalEntry } from "@/types/Accounts/JournalEntry"
import { getCompanyCostCenter, getCompanyCurrency } from "@/lib/company"
import { FrappeConfig, FrappeContext, useFrappePostCall } from "frappe-react-sdk"
import { toast } from "sonner"
import ErrorBanner from "@/components/ui/error-banner"
import { Button } from "@/components/ui/button"
import SelectedTransactionDetails from "./SelectedTransactionDetails"
import { AccountFormField, CurrencyFormField, DataField, DateField, LinkFormField, PartyTypeFormField, SmallTextField } from "@/components/ui/form-elements"
import { Form } from "@/components/ui/form"
import { useCallback, useContext, useMemo, useRef, useState } from "react"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Checkbox } from "@/components/ui/checkbox"
import { ArrowDownRight, ArrowUpRight, Plus, Trash2 } from "lucide-react"
import { flt, formatCurrency } from "@/lib/numbers"
import { cn } from "@/lib/utils"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import SelectedTransactionsTable from "./SelectedTransactionsTable"
import { JournalEntryAccount } from "@/types/Accounts/JournalEntryAccount"
import { BankTransaction } from "@/types/Accounts/BankTransaction"
import FileUploadBanner from "@/components/common/FileUploadBanner"
import { Label } from "@/components/ui/label"
import { FileDropzone } from "@/components/ui/file-dropzone"
import { useGetAccounts } from "@/components/common/AccountsDropdown"
import { useHotkeys } from "react-hotkeys-hook"
const BankEntryModal = () => {
const [isOpen, setIsOpen] = useAtom(bankRecRecordJournalEntryModalAtom)
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className='min-w-[95vw]'>
<DialogHeader>
<DialogTitle>{_("Bank Entry")}</DialogTitle>
<DialogDescription>
{_("Record a journal entry for expenses, income or split transactions.")}
</DialogDescription>
</DialogHeader>
<RecordBankEntryModalContent />
</DialogContent>
</Dialog>
)
}
const RecordBankEntryModalContent = () => {
const selectedBankAccount = useAtomValue(selectedBankAccountAtom)
const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? ''))
if (!selectedTransaction || !selectedBankAccount || selectedTransaction.length === 0) {
return <div className='p-4'>
<span className='text-center'>{_("No transaction selected")}</span>
</div>
}
if (selectedTransaction.length === 1) {
return <BankEntryForm
selectedTransaction={selectedTransaction[0]} />
}
return <BulkBankEntryForm
selectedTransactions={selectedTransaction}
/>
}
const BulkBankEntryForm = ({ selectedTransactions }: { selectedTransactions: UnreconciledTransaction[] }) => {
const form = useForm<{
account: string
}>({
defaultValues: {
account: ''
}
})
const { call, loading, error } = useFrappePostCall<{ message: { transaction: BankTransaction, journal_entry: JournalEntry }[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bulk_bank_entry_and_reconcile')
const onReconcile = useRefreshUnreconciledTransactions()
const addToActionLog = useUpdateActionLog()
const setIsOpen = useSetAtom(bankRecRecordJournalEntryModalAtom)
const onSubmit = (data: { account: string }) => {
call({
bank_transactions: selectedTransactions.map(transaction => transaction.name),
account: data.account
}).then(({ message }) => {
addToActionLog({
type: 'bank_entry',
timestamp: (new Date()).getTime(),
isBulk: true,
items: message.map((item) => ({
bankTransaction: item.transaction,
voucher: {
reference_doctype: "Journal Entry",
reference_name: item.journal_entry.name,
doc: item.journal_entry,
posting_date: item.journal_entry.posting_date,
}
})),
bulkCommonData: {
account: data.account,
}
})
toast.success(_("Bank Entries Created"), {
duration: 4000,
})
// Set this to the last selected transaction
onReconcile(selectedTransactions[selectedTransactions.length - 1])
setIsOpen(false)
})
}
return <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="flex flex-col gap-4">
{error && <ErrorBanner error={error} />}
<SelectedTransactionsTable />
<div className="grid grid-cols-3 gap-4">
<AccountFormField
name='account'
filterFunction={(acc) => {
// Do not allow payable and receivable accounts
return acc.account_type !== 'Payable' && acc.account_type !== 'Receivable'
}}
label={_('Account')}
isRequired
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button size='md' variant={'outline'} disabled={loading}>{_("Cancel")}</Button>
</DialogClose>
<Button size='md' type='submit' disabled={loading}>{_("Submit")}</Button>
</DialogFooter>
</div>
</form>
</Form>
}
interface BankEntryFormData extends Pick<JournalEntry, 'voucher_type' | 'cheque_date' | 'posting_date' | 'cheque_no' | 'user_remark'> {
entries: JournalEntry['accounts']
}
const BankEntryForm = ({ selectedTransaction }: { selectedTransaction: UnreconciledTransaction }) => {
const selectedBankAccount = useAtomValue(selectedBankAccountAtom)
const { data: rule } = useGetRuleForTransaction(selectedTransaction)
const setIsOpen = useSetAtom(bankRecRecordJournalEntryModalAtom)
const onClose = () => {
setIsOpen(false)
}
const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false
const defaultAccounts = useMemo(() => {
const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false
const accounts: Partial<JournalEntryAccount>[] = [
{
account: selectedBankAccount?.account ?? '',
bank_account: selectedTransaction.bank_account,
// Bank is debited if it's a deposit
debit: isWithdrawal ? 0 : selectedTransaction.unallocated_amount,
credit: isWithdrawal ? selectedTransaction.unallocated_amount : 0,
party_type: '',
party: '',
cost_center: ''
}]
// If there is no rule, we can just add the entries for the bank account transaction and the other side will be the reverse
if (!rule) {
accounts.push(
{
account: '',
// Amounts will be the reverse of the bank account transaction
debit: isWithdrawal ? selectedTransaction.unallocated_amount : 0,
credit: isWithdrawal ? 0 : selectedTransaction.unallocated_amount,
cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '',
}
)
} else {
// Rule exists, so we need to check the type of rule
if (!rule.bank_entry_type || rule.bank_entry_type === "Single Account") {
// Only a single account needs to be added
accounts.push({
account: rule.account ?? '',
// Amounts will be the reverse of the bank account transaction
debit: isWithdrawal ? selectedTransaction.unallocated_amount : 0,
credit: isWithdrawal ? 0 : selectedTransaction.unallocated_amount,
cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '',
})
} else {
// For multiple accounts, we need to loop over and add entries for each
// The last row will just be the remaining amount
let hasTotallyEmptyRowEarlier = false;
let totalDebits = isWithdrawal ? 0 : selectedTransaction.unallocated_amount ?? 0
let totalCredits = isWithdrawal ? selectedTransaction.unallocated_amount ?? 0 : 0
for (let i = 0; i < (rule.accounts?.length ?? 0); i++) {
const acc = rule.accounts?.[i]
// If it's the last row, add the difference amount
if (i === (rule.accounts?.length ?? 0) - 1 && !hasTotallyEmptyRowEarlier) {
const differenceAmount = flt(totalDebits - totalCredits, 2)
accounts.push({
account: acc?.account ?? '',
debit: differenceAmount > 0 ? 0 : Math.abs(differenceAmount),
credit: differenceAmount > 0 ? Math.abs(differenceAmount) : 0,
cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '',
user_remark: acc?.user_remark ?? '',
})
} else {
/**
* The debit and credit amounts can also be expressions - like "transaction_amount * 0.5"
* So we need to compute the value of the expression
* We can use the eval function to do this. But we need to expose certain variables to the expression.
* One of them is transaction_amount which is the unallocated amount of the selected transaction
* @param expression - The expression to compute
* @returns The computed value
*/
const computeExpression = (expression: string) => {
const script = `
const transaction_amount = ${selectedTransaction.unallocated_amount ?? 0}
${expression};
`
let value = 0;
try {
value = window.eval(script);
} catch (error: unknown) {
console.error(error);
value = 0;
}
return value;
}
if (!acc?.debit && !acc?.credit) {
hasTotallyEmptyRowEarlier = true;
}
const computedDebit = acc?.debit ? flt(computeExpression(acc.debit), 2) : 0
const computedCredit = acc?.credit ? flt(computeExpression(acc.credit), 2) : 0
totalDebits = flt(totalDebits + computedDebit, 2)
totalCredits = flt(totalCredits + computedCredit, 2)
accounts.push({
account: acc?.account ?? '',
debit: computedDebit,
credit: computedCredit,
cost_center: getCompanyCostCenter(selectedTransaction.company ?? '') ?? '',
user_remark: acc?.user_remark ?? '',
})
}
}
}
}
return accounts
}, [rule, selectedTransaction, selectedBankAccount])
const form = useForm<BankEntryFormData>({
defaultValues: {
voucher_type: selectedBankAccount?.is_credit_card ? 'Credit Card Entry' : 'Bank Entry',
cheque_date: selectedTransaction.date,
posting_date: selectedTransaction.date,
cheque_no: (selectedTransaction.reference_number || selectedTransaction.description || '').slice(0, 140),
user_remark: selectedTransaction.description,
entries: defaultAccounts,
}
})
const onReconcile = useRefreshUnreconciledTransactions()
const { call: createBankEntry, loading, error, isCompleted } = useFrappePostCall<{ message: { transaction: BankTransaction, journal_entry: JournalEntry } }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bank_entry_and_reconcile')
const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom)
const addToActionLog = useUpdateActionLog()
const { file: frappeFile } = useContext(FrappeContext) as FrappeConfig
const [isUploading, setIsUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
const [files, setFiles] = useState<File[]>([])
const onSubmit = (data: BankEntryFormData) => {
createBankEntry({
bank_transaction_name: selectedTransaction.name,
...data
}).then(async ({ message }) => {
addToActionLog({
type: 'bank_entry',
isBulk: false,
timestamp: (new Date()).getTime(),
items: [
{
bankTransaction: message.transaction,
voucher: {
reference_doctype: "Journal Entry",
reference_name: message.journal_entry.name,
reference_no: message.journal_entry.cheque_no,
reference_date: message.journal_entry.cheque_date,
posting_date: message.journal_entry.posting_date,
doc: message.journal_entry,
}
}
]
})
toast.success(_("Bank Entry Created"), {
duration: 4000,
closeButton: true,
action: {
label: _("Undo"),
onClick: () => setBankRecUnreconcileModalAtom(selectedTransaction.name)
},
actionButtonStyle: {
backgroundColor: "rgb(0, 138, 46)"
}
})
if (files.length > 0) {
setIsUploading(true)
const uploadPromises = files.map(f => {
return frappeFile.uploadFile(f, {
isPrivate: true,
doctype: "Journal Entry",
docname: message.journal_entry.name,
}, (_bytesUploaded, _totalBytes, progress) => {
setUploadProgress((currentProgress) => {
//If there are multiple files, we need to add the progress to the current progress
return currentProgress + ((progress?.progress ?? 0) / files.length)
})
})
})
return Promise.all(uploadPromises).then(() => {
setUploadProgress(0)
setIsUploading(false)
}).catch((error) => {
console.error(error)
toast.error(_("Error uploading attachments"), {
duration: 4000,
})
setIsUploading(false)
})
} else {
return Promise.resolve()
}
}).then(() => {
onReconcile(selectedTransaction)
onClose()
})
}
useHotkeys('meta+s', () => {
form.handleSubmit(onSubmit)()
}, {
enabled: true,
preventDefault: true,
enableOnFormTags: true
})
if (isUploading && isCompleted) {
return <FileUploadBanner uploadProgress={uploadProgress} />
}
return <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='flex flex-col gap-4'>
{error && <ErrorBanner error={error} />}
<div className='grid grid-cols-2 gap-4'>
<SelectedTransactionDetails transaction={selectedTransaction} />
<div className='flex flex-col gap-4'>
<div className='grid grid-cols-2 gap-4'>
<DateField
name='posting_date'
label={_("Posting Date")}
isRequired
inputProps={{ autoFocus: false }}
/>
<DateField
name='cheque_date'
label={_("Reference Date")}
isRequired
inputProps={{ autoFocus: false }}
rules={{
required: _("Reference Date is required"),
}}
/>
</div>
<DataField name='cheque_no' label={_("Reference")} isRequired inputProps={{ autoFocus: false }}
rules={{
required: _("Reference is required"),
}} />
</div>
</div>
<div>
<Entries company={selectedTransaction.company ?? ''} isWithdrawal={isWithdrawal} currency={selectedTransaction.currency ?? getCompanyCurrency(selectedTransaction.company ?? '')} />
</div>
<div className='flex flex-col gap-2'>
<div className='grid grid-cols-2 gap-4'>
<SmallTextField
name='user_remark'
label={_("Remarks")}
/>
<div
data-slot="form-item"
className="flex flex-col gap-2"
>
<Label>{_("Attachments")}</Label>
<FileDropzone files={files} setFiles={setFiles} />
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button size='md' variant={'outline'} disabled={loading}>{_("Cancel")}</Button>
</DialogClose>
<Button size='md' type='submit' disabled={loading}>{_("Submit")}</Button>
</DialogFooter>
</div>
</form>
</Form>
}
const Entries = ({ company, isWithdrawal, currency }: { company: string, isWithdrawal: boolean, currency: string }) => {
const { getValues, setValue, control } = useFormContext<BankEntryFormData>()
const { call } = useContext(FrappeContext) as FrappeConfig
const partyMapRef = useRef<Record<string, string>>({})
const onPartyChange = (value: string, index: number) => {
// Get the account for the party type
if (value) {
if (partyMapRef.current[value]) {
setValue(`entries.${index}.account`, partyMapRef.current[value])
} else {
call.get('erpnext.accounts.party.get_party_account', {
party: value,
party_type: getValues(`entries.${index}.party_type`),
company: company
}).then((result: { message: string }) => {
setValue(`entries.${index}.account`, result.message)
partyMapRef.current[value] = result.message
})
}
} else {
setValue(`entries.${index}.account`, '')
}
}
const { data: accounts } = useGetAccounts()
const onAccountChange = (value: string, index: number) => {
// If it's an income or expense account, get the default cost center
if (value) {
const account = accounts?.find((acc) => acc.name === value)
if (account && account.report_type === "Profit and Loss") {
// Set the default company cost center
setValue(`entries.${index}.cost_center`, getCompanyCostCenter(company) ?? '')
return
}
}
setValue(`entries.${index}.cost_center`, '')
}
const { fields, append, remove } = useFieldArray({
control: control,
name: 'entries'
})
const onAdd = useCallback(() => {
const existingEntries = getValues('entries')
const totalDebits = existingEntries.reduce((acc, curr) => flt(acc + (curr.debit ?? 0), 2), 0)
const totalCredits = existingEntries.reduce((acc, curr) => flt(acc + (curr.credit ?? 0), 2), 0)
const remainingAmount = flt(totalDebits - totalCredits, 2)
// Remaining amount is credit if it's positive - since some debit is pending to be cleared.
const debitAmount = remainingAmount > 0 ? 0 : Math.abs(remainingAmount)
const creditAmount = remainingAmount > 0 ? Math.abs(remainingAmount) : 0
append({
party_type: '',
party: '',
account: '',
debit: debitAmount,
credit: creditAmount,
cost_center: getCompanyCostCenter(company) ?? ''
} as JournalEntryAccount, {
focusName: `entries.${existingEntries.length}.account`
})
}, [company, append, getValues])
const [selectedRows, setSelectedRows] = useState<number[]>([])
const onSelectRow = useCallback((index: number) => {
setSelectedRows(prev => {
if (prev.includes(index)) {
return prev.filter(i => i !== index)
}
return [...prev, index]
})
}, [])
const onSelectAll = useCallback(() => {
setSelectedRows(prev => {
if (prev.length === fields.length) {
return []
}
return [...fields.map((_, index) => index)]
})
}, [fields])
const onRemove = useCallback(() => {
remove(selectedRows)
setSelectedRows([])
}, [remove, selectedRows])
/**
* When add difference is clicked, check if the last row has nothing filled in.
* If last row is empty (no debit or credit), then set that row's amount. Else, add a new row with the difference amount.
*/
const onAddDifferenceClicked = () => {
const existingEntries = getValues('entries')
const totalDebits = existingEntries.reduce((acc, curr) => flt(acc + (curr.debit ?? 0), 2), 0)
const totalCredits = existingEntries.reduce((acc, curr) => flt(acc + (curr.credit ?? 0), 2), 0)
const lastIndex = existingEntries.length - 1
const isLastRowEmpty = (existingEntries[lastIndex]?.debit === 0 || existingEntries[lastIndex]?.debit === undefined) && (existingEntries[lastIndex]?.credit === 0 || existingEntries[lastIndex]?.credit === undefined)
const remainingAmount = flt(totalDebits - totalCredits, 2)
// Remaining amount is credit if it's positive - since some debit is pending to be cleared.
const debitAmount = remainingAmount > 0 ? 0 : Math.abs(remainingAmount)
const creditAmount = remainingAmount > 0 ? Math.abs(remainingAmount) : 0
if (isLastRowEmpty) {
setValue(`entries.${lastIndex}.debit`, debitAmount)
setValue(`entries.${lastIndex}.credit`, creditAmount)
} else {
append({
party_type: '',
party: '',
account: '',
debit: debitAmount,
credit: creditAmount,
cost_center: getCompanyCostCenter(company) ?? ''
} as JournalEntryAccount, {
focusName: `entries.${existingEntries.length}.account`
})
}
}
return <div className="flex flex-col gap-2">
<Table>
<TableHeader>
<TableRow>
<TableHead><Checkbox
disabled={fields.length === 0}
// Make this accessible to screen readers
aria-label={_("Select all")}
checked={selectedRows.length > 0 && selectedRows.length === fields.length}
onCheckedChange={onSelectAll} /></TableHead>
<TableHead>{_("Party")}</TableHead>
<TableHead>{_("Account")}</TableHead>
<TableHead>{_("Cost Center")}</TableHead>
<TableHead>{_("Remarks")}</TableHead>
<TableHead className="text-end">{_("Debit")}</TableHead>
<TableHead className="text-end">{_("Credit")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields.map((field, index) => (
<TableRow key={field.id} className={index === 0 ? 'bg-surface-gray-1 cursor-not-allowed' : ''} title={index === 0 ? _("This is the bank account entry. You cannot edit it.") : ''}>
<TableCell>
<Checkbox
checked={selectedRows.includes(index)}
onCheckedChange={() => onSelectRow(index)}
// Make this accessible to screen readers
aria-label={_("Select row {0}", [String(index + 1)])}
disabled={index === 0}
/>
</TableCell>
<TableCell className="align-top">
<div className="flex">
<PartyTypeFormField
name={`entries.${index}.party_type`}
label={_("Party Type")}
isRequired
readOnly={index === 0}
hideLabel
inputProps={{
type: isWithdrawal ? 'Payable' : 'Receivable',
triggerProps: {
className: 'rounded-e-none',
tabIndex: -1
},
readOnly: index === 0,
}} />
<PartyField index={index} onChange={onPartyChange} readOnly={index === 0} />
</div>
</TableCell>
<TableCell className="align-top">
<AccountFormField
name={`entries.${index}.account`}
label={_("Account")}
rules={{
required: _("Account is required"),
onChange: (event) => {
onAccountChange(event.target.value, index)
}
}}
buttonClassName="min-w-64"
readOnly={index === 0}
isRequired
hideLabel
/>
</TableCell>
<TableCell className="align-top">
<LinkFormField
doctype="Cost Center"
name={`entries.${index}.cost_center`}
label={_("Cost Center")}
filters={[["company", "=", company], ["is_group", "=", 0], ["disabled", "=", 0]]}
buttonClassName="min-w-48"
readOnly={index === 0}
hideLabel
/>
</TableCell>
<TableCell className="align-top">
<DataField
name={`entries.${index}.user_remark`}
label={_("Remarks")}
readOnly={index === 0}
inputProps={{
placeholder: _("e.g. Bank Charges"),
className: 'min-w-64',
readOnly: index === 0
}}
hideLabel
/>
</TableCell>
<TableCell className={cn("text-end align-top")}>
<CurrencyFormField
name={`entries.${index}.debit`}
label={_("Debit")}
isRequired
hideLabel
readOnly={index === 0}
style={index === 0 ? !isWithdrawal ? {
color: "var(--color-ink-gray-8)",
} : {} : {}}
currency={currency}
leftSlot={index === 0 && !isWithdrawal ? <Tooltip>
<TooltipTrigger asChild><ArrowDownRight className="text-ink-green-3" /></TooltipTrigger>
<TooltipContent>{_("Bank account debit for deposit")}</TooltipContent>
</Tooltip> : undefined}
/>
</TableCell>
<TableCell className={cn("text-end align-top")}>
<CurrencyFormField
name={`entries.${index}.credit`}
style={index === 0 && isWithdrawal ? {
color: "var(--color-ink-gray-8)",
} : {}}
label={_("Credit")}
isRequired
hideLabel
readOnly={index === 0}
currency={currency}
leftSlot={index === 0 && isWithdrawal ? <Tooltip>
<TooltipTrigger asChild><ArrowUpRight className="text-ink-red-3" /></TooltipTrigger>
<TooltipContent>{_("Bank account credit for withdrawal")}</TooltipContent>
</Tooltip> : undefined}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex justify-between gap-2">
<div className="flex gap-2 justify-end">
<div>
<Button size='sm' type='button' variant={'outline'} onClick={onAdd}><Plus /> {_("Add Row")}</Button>
</div>
{selectedRows.length > 0 && <div>
<Button size='sm' type='button' theme="red" onClick={onRemove}><Trash2 /> {_("Remove")}</Button>
</div>}
</div>
<Summary currency={currency} addRow={onAddDifferenceClicked} />
</div>
</div>
}
const PartyField = ({ index, onChange, readOnly }: { index: number, onChange: (value: string, index: number) => void, readOnly: boolean }) => {
const { control } = useFormContext<BankEntryFormData>()
const party_type = useWatch({
control,
name: `entries.${index}.party_type`
})
if (!party_type) {
return <DataField
name={`entries.${index}.party`}
label={_("Party")}
isRequired
inputProps={{
disabled: true,
className: 'rounded-s-none border-s-0 min-w-64'
}}
hideLabel
/>
}
return <LinkFormField
name={`entries.${index}.party`}
label={_("Party")}
rules={{
onChange: (event) => {
onChange(event.target.value, index)
},
}}
hideLabel
readOnly={readOnly}
buttonClassName="rounded-s-none border-s-0 min-w-64"
doctype={party_type}
/>
}
const Summary = ({ currency, addRow }: { currency: string, addRow: () => void }) => {
const { control } = useFormContext<BankEntryFormData>()
const entries = useWatch({ control, name: 'entries' })
const { total, totalCredits, totalDebits } = useMemo(() => {
// Do a total debits - total credits
const totalDebits = entries.reduce((acc, curr) => flt(acc + (curr.debit ?? 0), 2), 0)
const totalCredits = entries.reduce((acc, curr) => flt(acc + (curr.credit ?? 0), 2), 0)
return { total: flt(totalDebits - totalCredits, 2), totalDebits, totalCredits }
}, [entries])
const onAddRow = useCallback(() => {
addRow()
}, [addRow])
const TextComponent = ({ className, children }: { className?: string, children: React.ReactNode }) => {
return <span className={cn("w-32 text-end font-medium text-sm font-numeric", className)}>{children}</span>
}
return <div className="flex flex-col gap-2 items-end">
<div className="flex gap-2 justify-between">
<TextComponent>{_("Total Debit")}</TextComponent>
<TextComponent>{formatCurrency(totalDebits, currency)}</TextComponent>
</div>
<div className="flex gap-2 justify-between">
<TextComponent>{_("Total Credit")}</TextComponent>
<TextComponent>{formatCurrency(totalCredits, currency)}</TextComponent>
</div>
{total !== 0 && <div className="flex gap-2 justify-between">
<TextComponent>{_("Difference")}</TextComponent>
<Tooltip>
<TooltipTrigger asChild>
<Button type='button' variant='link' className="p-0 text-ink-red-3 underline h-fit" role='button' onClick={onAddRow}>
<TextComponent className='text-ink-red-3'>{formatCurrency(total, currency)}</TextComponent>
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Add a row with the difference amount")}
</TooltipContent>
</Tooltip>
</div>}
</div>
}
export default BankEntryModal

View File

@@ -1,134 +0,0 @@
import { useAtom } from "jotai"
import { SelectedBank, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCallback } from "react"
import { useGetBankAccounts, useGetUnreconciledTransactions } from "./utils"
import { cn } from "@/lib/utils"
import { getTimeago } from "@/lib/date"
import ErrorBanner from "@/components/ui/error-banner"
import _ from "@/lib/translate"
import { Badge } from "@/components/ui/badge"
import { useTheme } from "@/components/ui/theme-provider"
import BankLogo from "@/components/common/BankLogo"
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { LandmarkIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
const BankPicker = ({ className }: { className?: string }) => {
const [selectedBank, setSelectedBank] = useAtom(selectedBankAccountAtom)
const onLoadingSuccess = useCallback((data?: SelectedBank[]) => {
// If the bank is already selected, then don't set it again
if (selectedBank) {
// Check if selected bank is in the data
if (data?.some((bank: SelectedBank) => bank.name === selectedBank.name)) {
return
}
}
if (!data) return
if (data.length === 1) {
setSelectedBank(data[0])
} else if (data.length > 1) {
const defaultBank = data.find((bank: SelectedBank) => bank.is_default)
if (defaultBank) {
setSelectedBank(defaultBank)
} else {
// Select the first available bank account
setSelectedBank(data[0])
}
}
}, [setSelectedBank, selectedBank])
const selectedCompany = useCurrentCompany()
const { banks, isLoading, error } = useGetBankAccounts(onLoadingSuccess)
const { themeValue } = useTheme()
if (isLoading) {
return null
}
if (error) {
return <ErrorBanner error={error} />
}
if (banks?.length === 0) {
return <Empty>
<EmptyMedia>
<LandmarkIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No bank accounts found")}</EmptyTitle>
<EmptyDescription>{_("You have not added any bank accounts to your company.")}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button asChild>
<a href={`/desk/bank-account?company=${encodeURIComponent(selectedCompany)}&is_company_account=1`}>
{_("Configure Bank Accounts")}
</a>
</Button>
</EmptyContent>
</Empty>
}
return (
<div
className={cn("flex gap-3 items-stretch w-full overflow-x-auto pe-4",
banks?.length > 4 ? 'pb-2' : '', className,
)}
style={{
scrollbarWidth: 'thin',
scrollbarColor: themeValue === 'Dark' ? 'var(--surface-gray-2) var(--surface-gray-1)' : 'rgb(209 213 219) rgb(243 244 246)',
}}
>
{
banks?.map((bank) => (
<BankPickerItem key={bank.name} bank={bank} />
))
}
</div>
)
}
const BankPickerItem = ({ bank }: { bank: SelectedBank }) => {
const [selectedBank, setSelectedBank] = useAtom(selectedBankAccountAtom)
const isSelected = selectedBank?.name === bank.name
const { mutate } = useGetUnreconciledTransactions()
const onSelect = () => {
setSelectedBank(bank)
mutate()
}
return <div
role="button"
title={`Select ${bank.account_name}`}
onClick={onSelect}
className={cn('rounded-md border border-outline-gray-1 max-w-60 min-w-60 p-2 overflow-hidden cursor-pointer',
isSelected ? 'border-outline-gray-5 bg-surface-gray-1' : 'hover:bg-surface-gray-1'
)}
>
<BankLogo bank={bank} className="mb-2" />
<div className="flex flex-col gap-1">
<div className="flex gap-2 items-center">
<span className={cn("text-sm font-medium line-clamp-1 text-ink-gray-8")}>{bank.account_name}</span>
{bank.account_type && <Badge variant='subtle' size='sm' theme='gray'>
{bank.account_type?.slice(0, 24)}
</Badge>}
</div>
<span title={_("GL Account")} className={cn("text-ellipsis line-clamp-1 text-sm text-ink-gray-6")}>{bank.account}</span>
{bank.last_integration_date && <span className="text-xs text-ink-gray-5">{_("Last Synced Transaction")}: {getTimeago(bank.last_integration_date)}</span>}
</div>
</div >
}
export default BankPicker

View File

@@ -1,275 +0,0 @@
import { useAtom } from 'jotai'
import { bankRecDateAtom } from './bankRecAtoms'
import { useMemo, useState } from 'react'
import { AVAILABLE_TIME_PERIODS, formatDate, getDatesForTimePeriod, TimePeriod } from '@/lib/date'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ChevronDownIcon, ChevronLeftIcon, ChevronRight } from 'lucide-react'
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
import { parse } from "chrono-node"
import { Calendar } from '@/components/ui/calendar'
import useFiscalYear from '@/hooks/useFiscalYear'
import dayjs from 'dayjs'
import _ from '@/lib/translate'
import { useDirection } from '@/components/ui/direction'
const BankRecDateFilter = () => {
const [bankRecDate, setBankRecDate] = useAtom(bankRecDateAtom)
const { data: fiscalYear } = useFiscalYear()
const timePeriodOptions = useMemo(() => {
const standardOptions = AVAILABLE_TIME_PERIODS.map((period) => {
const dates = getDatesForTimePeriod(period)
return {
label: period,
fromDate: dates.fromDate,
toDate: dates.toDate,
format: dates.format,
translatedLabel: dates.translatedLabel
}
})
if (fiscalYear?.message) {
// For a fiscal year, we need to replace "Last Year", "This Year", and add options for quarters
const fiscalYearStart = fiscalYear.message.year_start_date
const fiscalYearEnd = fiscalYear.message.year_end_date
const q1 = {
label: `Q1: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q1")}: ${fiscalYear.message.name}`,
fromDate: fiscalYearStart,
toDate: dayjs(fiscalYearStart).add(3, 'month').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
const q2 = {
label: `Q2: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q2")}: ${fiscalYear.message.name}`,
fromDate: dayjs(fiscalYearStart).add(3, 'month').format('YYYY-MM-DD'),
toDate: dayjs(fiscalYearStart).add(6, 'month').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
const q3 = {
label: `Q3: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q3")}: ${fiscalYear.message.name}`,
fromDate: dayjs(fiscalYearStart).add(6, 'month').format('YYYY-MM-DD'),
toDate: dayjs(fiscalYearStart).add(9, 'month').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
const q4 = {
label: `Q4: ${fiscalYear.message.name}`,
translatedLabel: `${_("Q4")}: ${fiscalYear.message.name}`,
fromDate: dayjs(fiscalYearStart).add(9, 'month').format('YYYY-MM-DD'),
toDate: fiscalYearEnd,
format: 'MMM YYYY'
}
const thisYear = {
label: `This Fiscal Year`,
translatedLabel: `${_("This Fiscal Year")}`,
fromDate: fiscalYearStart,
toDate: fiscalYearEnd,
format: 'MMM YYYY'
}
const lastYear = {
label: `Last Fiscal Year`,
translatedLabel: `${_("Last Fiscal Year")}`,
fromDate: dayjs(fiscalYearStart).subtract(1, 'year').format('YYYY-MM-DD'),
toDate: dayjs(fiscalYearEnd).subtract(1, 'year').format('YYYY-MM-DD'),
format: 'MMM YYYY'
}
// Sort the options so that we get "This Month", "Last Month", quarters, fiscal year, then the rest of the standard options
const topRankedItems = standardOptions.filter((option) => {
return option.label === "This Month" || option.label === "Last Month"
})
const bottomRankedItems = standardOptions.filter((option) => {
return option.label !== "This Month" && option.label !== "Last Month"
})
return [...topRankedItems, q1, q2, q3, q4, thisYear, lastYear, ...bottomRankedItems]
}
return standardOptions
}, [fiscalYear])
const [open, setOpen] = useState(false)
const [value, setValue] = useState("")
const timePeriod: TimePeriod | string = useMemo(() => {
if (bankRecDate.fromDate && bankRecDate.toDate) {
// Check if the from and to dates match any predefined time period
for (const period of timePeriodOptions) {
if (period.fromDate === bankRecDate.fromDate && period.toDate === bankRecDate.toDate) {
return period.label;
}
}
return "Date Range";
} else {
return "Date Range";
}
}, [bankRecDate.fromDate, bankRecDate.toDate, timePeriodOptions]);
const handleTimePeriodChange = (fromDate: string, toDate: string) => {
setBankRecDate({ fromDate, toDate })
setOpen(false)
}
const dateObj = useMemo(() => {
return {
from: new Date(bankRecDate.fromDate),
to: new Date(bankRecDate.toDate)
}
}, [bankRecDate.fromDate, bankRecDate.toDate])
const direction = useDirection()
return <div className='flex items-center'>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant={'outline'}
aria-expanded={open}
size='md'
className='rounded-e-none border-e-0'
role="combobox">
{timePeriodOptions.find((period) => period.label === timePeriod)?.translatedLabel ?? _(timePeriod)}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-84 p-1" align='start'>
<Command>
<CommandInput placeholder="e.g. Last 3 weeks" onValueChange={setValue} value={value} />
<CommandList className='max-h-fit'>
<CommandEmpty className='text-start p-2 hover:bg-surface-gray-1'>
<EmptyState onSelect={handleTimePeriodChange} value={value} />
</CommandEmpty>
{timePeriodOptions.map((period) => (
<CommandItem key={period.label} className='flex justify-between' onSelect={() => handleTimePeriodChange(period.fromDate, period.toDate)}>
<span>
{period.translatedLabel ?? _(period.label)}
</span>
<span className='text-xs text-ink-gray-5 flex items-center gap-1 text-end whitespace-nowrap'>
{formatDate(period.fromDate, period.format)} {direction === 'ltr' ? <ChevronRight className='text-[12px] text-ink-gray-5/70' /> : <ChevronLeftIcon className='text-[12px] text-ink-gray-5/70' />} {formatDate(period.toDate, period.format)}
</span>
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant={'outline'} className='rounded-s-none' size='md'>
{formatDate(bankRecDate.fromDate)} - {formatDate(bankRecDate.toDate)}
</Button>
</PopoverTrigger>
<PopoverContent className='w-auto overflow-hidden p-0' align='end'>
<Calendar
mode='range'
captionLayout='dropdown'
selected={{
from: dateObj.from,
to: dateObj.to
}}
numberOfMonths={2}
defaultMonth={dateObj.from}
onSelect={(date) => {
if (date) {
setBankRecDate({ fromDate: formatDate(date.from, 'YYYY-MM-DD'), toDate: formatDate(date.to, 'YYYY-MM-DD') })
}
}}
/>
</PopoverContent>
</Popover>
</div>
}
const referentialKeywords = ["last", "this", "next", "previous"]
const EmptyState = ({ onSelect, value }: { onSelect: (fromDate: string, toDate: string) => void, value: string }) => {
const dates = useMemo(() => {
if (value) {
// Try parsing the value
const parsedDate = parse(value, undefined, { forwardDate: false })
if (parsedDate && parsedDate.length > 0) {
const startDate = parsedDate[0].start.date()
const endDate = parsedDate[0].end?.date()
if (!endDate) {
const today = new Date()
// If today is greater than the start date, use today as the end date
if (startDate.getTime() > today.getTime()) {
return { fromDate: today, toDate: startDate }
} else {
// Check if the user only wants a specific month like "May 2025"
// If the "known values" just has month and year, then we need to get the first day of the month and the last day of the month
// @ts-expect-error - "Known Values" is available in the start "ParsingComponents"
if (parsedDate[0].start.knownValues?.month && !parsedDate[0].start.knownValues?.day) {
return {
fromDate: startDate,
toDate: dayjs(startDate).endOf('month').toDate()
}
// @ts-expect-error - "Known Values" is available in the start "ParsingComponents"
} else if (parsedDate[0].start.knownValues?.month && parsedDate[0].start.knownValues?.day && !referentialKeywords.some(keyword => value.toLowerCase().includes(keyword))) {
// If month and day is known, then we should not assume that the user wants to get everything until today
return {
fromDate: startDate,
toDate: startDate,
}
}
return {
fromDate: startDate,
toDate: today
}
}
} else {
return { fromDate: startDate, toDate: endDate }
}
}
}
}, [value])
const onClick = (fromDate: Date, toDate: Date) => {
onSelect(formatDate(fromDate, 'YYYY-MM-DD'), formatDate(toDate, 'YYYY-MM-DD'))
}
const isEqual = dates?.fromDate && dates?.toDate && dayjs(dates.fromDate).isSame(dates.toDate, 'date')
return <div>
{dates ?
<div className='flex gap-2 items-center justify-between cursor-pointer' onClick={() => onClick(dates.fromDate, dates.toDate)}>
<span className='text-sm text-ink-gray-5 max-w-[30%]'>
{value}
</span>
{isEqual ? <span className='text-xs text-ink-gray-5 text-balance flex items-center gap-1'>
{formatDate(dates.fromDate, 'Do MMM YYYY')}
</span> :
<span className='text-xs text-ink-gray-5 flex items-center gap-1'>
{formatDate(dates.fromDate, 'Do MMM YY')} <ChevronRight size='16' className='text-ink-gray-5' /> {formatDate(dates.toDate, 'Do MMM YY')}
</span>}
</div> :
<span className='text-sm text-ink-gray-5'>
No results found
</span>
}
</div>
}
export default BankRecDateFilter

View File

@@ -1,315 +0,0 @@
import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import { useCallback, useMemo } from "react"
import type { ColumnDef } from "@tanstack/react-table"
import { useFrappeGetCall } from "frappe-react-sdk"
import { QueryReportReturnType } from "@/types/custom/Reports"
import { formatDate } from "@/lib/date"
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
import { formatCurrency } from "@/lib/numbers"
import { getCompanyCurrency } from "@/lib/company"
import { slug } from "@/lib/frappe"
import { ScrollTextIcon } from "lucide-react"
import ErrorBanner from "@/components/ui/error-banner"
import { StatContainer, StatLabel, StatValue } from "@/components/ui/stats"
import _ from "@/lib/translate"
import { toast } from "sonner"
import { useCopyToClipboard } from "usehooks-ts"
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
const BankReconciliationStatement = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
if (!bankAccount) {
return <MissingFiltersBanner text={_("Please select a bank account to view the bank reconciliation statement.")} />
}
if (!dates) {
return <MissingFiltersBanner text={_("Please select dates to view the bank reconciliation statement.")} />
}
return <BankReconciliationStatementView />
}
interface BankClearanceSummaryEntry {
payment_document: string
payment_entry: string
posting_date: string,
reference_no: string,
credit: number,
debit: number,
against_account: string,
ref_date: string,
account_currency: string,
clearance_date: string
}
const BankReconciliationStatementView = () => {
const companyID = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const filters = useMemo(() => {
return JSON.stringify({
account: bankAccount?.account,
report_date: dates.toDate,
company: companyID
})
}, [bankAccount, dates, companyID])
const { data, error } = useFrappeGetCall<{ message: QueryReportReturnType }>('frappe.desk.query_report.run', {
report_name: 'Bank Reconciliation Statement',
filters,
ignore_prepared_report: 1,
are_default_filters: false,
}, `Report-Bank Reconciliation Statement-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST')
const [, copyToClipboard] = useCopyToClipboard()
const onCopy = useCallback(
(text: string) => {
copyToClipboard(text).then(() => {
toast.success(_("Copied to clipboard"))
})
},
[copyToClipboard, _],
)
const statementColumns = useMemo<ColumnDef<BankClearanceSummaryEntry, unknown>[]>(
() => [
{
accessorKey: "posting_date",
header: _("Posting Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.posting_date),
},
{
accessorKey: "payment_document",
header: _("Document Type"),
size: 140,
cell: ({ row }) => _(row.original.payment_document),
},
{
id: "payment_entry",
header: _("Payment Document"),
size: 300,
meta: {
getTooltipText: (r) => {
const x = r as BankClearanceSummaryEntry
const parts = [x.payment_document, x.payment_entry].filter(Boolean)
return parts.length ? parts.join(" · ") : undefined
},
} satisfies ListViewColumnMeta,
cell: ({ row }) => {
const { payment_document, payment_entry } = row.original
return payment_document ? (
<a
target="_blank"
rel="noreferrer"
className="text-ink-gray-8 block min-w-0 w-full underline underline-offset-4"
href={`/desk/${slug(payment_document)}/${payment_entry}`}
>
{payment_entry}
</a>
) : (
payment_entry
)
},
},
{
accessorKey: "debit",
header: _("Debit"),
size: 112,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.debit, row.original.account_currency)}</span>,
},
{
accessorKey: "credit",
header: _("Credit"),
size: 112,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.credit, row.original.account_currency)}</span>,
},
{
accessorKey: "against_account",
header: _("Against Account"),
meta: { gridWidth: "minmax(0,1.25fr)" } satisfies ListViewColumnMeta,
cell: ({ row }) => (
<a
target="_blank"
rel="noreferrer"
className="text-ink-gray-8 block min-w-0 w-full underline underline-offset-4"
href={`/desk/account/${row.original.against_account}`}
>
{row.original.against_account}
</a>
),
},
{
accessorKey: "reference_no",
header: _("Reference #"),
cell: ({ row }) => {
const ref = row.original.reference_no
return (
<button
type="button"
className="text-ink-gray-8 hover:underline min-w-0 w-full cursor-pointer truncate text-start underline-offset-4"
onClick={() => onCopy(ref)}
>
{ref}
</button>
)
},
},
{
accessorKey: "ref_date",
header: _("Reference Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.ref_date),
},
{
accessorKey: "clearance_date",
header: _("Clearance Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.clearance_date),
},
],
[_, onCopy],
)
const statementRows = useMemo(() => {
if (!data?.message.result) return []
return data.message.result.filter((row: BankClearanceSummaryEntry) => Boolean(row.payment_entry))
}, [data])
return <div className="space-y-4 py-2">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("Below is a list of all entries posted against the bank account {0} which have not been cleared till {1}.", [`<strong>${bankAccount?.account}</strong>`, `<strong>${formatDate(dates.toDate)}</strong>`])
}} />
</Paragraph>
</div>
{error && <ErrorBanner error={error} />}
{data && <SummarySection data={data} />}
{data && data.message.result.length > 0 && (
<div className="space-y-2">
<p className="text-ink-gray-5 text-sm">{_("Bank Reconciliation Statement")}</p>
<ListView
data={statementRows}
columns={statementColumns}
getRowId={(row) => row.payment_entry}
maxHeight="min(70vh, 640px)"
emptyState={_("No entries with a payment document in this list.")}
/>
</div>
)}
{data && data.message.result.length === 0 &&
<Empty>
<EmptyMedia>
<ScrollTextIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No entries found")}</EmptyTitle>
<EmptyDescription>{_("There are no accounting entries in the system for the selected account and dates.")}</EmptyDescription>
</EmptyHeader>
</Empty>
}
</div>
}
const SummarySection = ({ data }: { data: { message: QueryReportReturnType } }) => {
const company = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const { bankStatementBalanceAsPerGL, outstandingChecksDebit, outstandingChecksCredit, incorrectlyClearedEntriesDebit, incorrectlyClearedEntriesCredit, calculatedBankStatementBalance } = useMemo(() => {
// Loop over the results and find the corresponding rows
let bankStatementBalanceAsPerGL = 0
let outstandingChecksDebit = 0
let outstandingChecksCredit = 0
let incorrectlyClearedEntriesDebit = 0
let incorrectlyClearedEntriesCredit = 0
let calculatedBankStatementBalance = 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?.message.result.forEach((r: any) => {
if (r.payment_entry === 'Bank Statement balance as per General Ledger') {
bankStatementBalanceAsPerGL = r.debit - r.credit
}
if (r.payment_entry === 'Outstanding Checks and Deposits to clear') {
outstandingChecksDebit = r.debit
outstandingChecksCredit = r.credit
}
if (r.payment_entry === 'Checks and Deposits incorrectly cleared') {
incorrectlyClearedEntriesDebit = r.debit
incorrectlyClearedEntriesCredit = r.credit
}
if (r.payment_entry === 'Calculated Bank Statement balance') {
calculatedBankStatementBalance = r.debit - r.credit
}
})
return {
bankStatementBalanceAsPerGL,
outstandingChecksDebit,
outstandingChecksCredit,
incorrectlyClearedEntriesDebit,
incorrectlyClearedEntriesCredit,
calculatedBankStatementBalance
}
}, [data])
const currency = bankAccount?.account_currency ?? getCompanyCurrency(company)
return <div className="flex gap-4 items-start justify-between">
<StatContainer>
<StatLabel>{_("Bank Statement Balance as per General Ledger")}</StatLabel>
<StatValue className="font-numeric">{formatCurrency(bankStatementBalanceAsPerGL, currency)}</StatValue>
</StatContainer>
<StatContainer>
<StatLabel>{_("Outstanding Checks and Deposits to clear")}</StatLabel>
<StatValue className="font-numeric">{formatCurrency(outstandingChecksDebit - outstandingChecksCredit, currency)}</StatValue>
</StatContainer>
{(incorrectlyClearedEntriesDebit > 0 || incorrectlyClearedEntriesCredit > 0) && <StatContainer>
<StatLabel className="text-ink-red-3">{_("Checks and Deposits incorrectly cleared")}</StatLabel>
<StatValue className="text-ink-red-3 font-numeric">{formatCurrency(incorrectlyClearedEntriesDebit - incorrectlyClearedEntriesCredit)}</StatValue>
{/* <div className="" divider={<StackDivider height='20px' />}>
{incorrectlyClearedEntriesDebit !== 0 && <StatHelpText>Debit: {formatCurrency(incorrectlyClearedEntriesDebit)}</StatHelpText>}
{incorrectlyClearedEntriesCredit !== 0 && <StatHelpText>Credit: {formatCurrency(incorrectlyClearedEntriesCredit)}</StatHelpText>}
</div> */}
</StatContainer>}
<StatContainer>
<StatLabel>{_("Calculated Bank Statement Balance")}</StatLabel>
<StatValue className="font-numeric">{formatCurrency(calculatedBankStatementBalance)}</StatValue>
</StatContainer>
</div>
}
export default BankReconciliationStatement

View File

@@ -1,422 +0,0 @@
import { useAtomValue, useSetAtom } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { Paragraph } from "@/components/ui/typography"
import { formatDate } from "@/lib/date"
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers"
import { getCompanyCurrency } from "@/lib/company"
import { ArrowDownRight, ArrowUpRight, CheckCircle2, ChevronDown, DollarSign, ExternalLink, ImportIcon, ListIcon, Search, Undo2, XCircle } from "lucide-react"
import ErrorBanner from "@/components/ui/error-banner"
import { Badge } from "@/components/ui/badge"
import { useGetBankTransactions } from "./utils"
import { BankTransaction } from "@/types/Accounts/BankTransaction"
import { Button } from "@/components/ui/button"
import _ from "@/lib/translate"
import { Input } from "@/components/ui/input"
import CurrencyInput from "react-currency-input-field"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { getCurrencySymbol } from "@/lib/currency"
import { useDebounceValue } from "usehooks-ts"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo, useState } from "react"
import { Link } from "react-router"
import { Empty, EmptyTitle, EmptyHeader, EmptyMedia, EmptyDescription, EmptyContent } from "@/components/ui/empty"
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group"
const BankTransactions = () => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
if (!selectedBank || !dates) {
return <MissingFiltersBanner text={_("Please select a bank and set the date range")} />
}
return <>
<BankTransactionListView />
</>
}
const BankTransactionListView = () => {
const { data, error } = useGetBankTransactions()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const formattedFromDate = formatDate(dates.fromDate)
const formattedToDate = formatDate(dates.toDate)
const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom)
const onUndo = useCallback(
(transaction: BankTransaction) => {
setBankRecUnreconcileModalAtom(transaction.name)
},
[setBankRecUnreconcileModalAtom],
)
const accountCurrency = useMemo(
() => bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? ""),
[bankAccount?.account_currency, bankAccount?.company],
)
const transactionColumns = useMemo<ColumnDef<BankTransaction, unknown>[]>(
() => [
{
accessorKey: "date",
header: _("Date"),
size: 112,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.date),
},
{
accessorKey: "description",
header: _("Description"),
size: 250,
// meta: { gridWidth: "minmax(0,2fr)" } satisfies ListViewColumnMeta,
cell: ({ row }) => row.original.description,
},
{
accessorKey: "reference_number",
header: _("Reference #"),
size: 128,
cell: ({ row }) => row.original.reference_number,
},
{
accessorKey: "withdrawal",
header: _("Withdrawal"),
size: 120,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.withdrawal, accountCurrency)}</span>,
},
{
accessorKey: "deposit",
header: _("Deposit"),
size: 120,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.deposit, accountCurrency)}</span>,
},
{
accessorKey: "unallocated_amount",
header: _("Unallocated"),
size: 120,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => <span className="font-numeric">{formatCurrency(row.original.unallocated_amount, accountCurrency)}</span>,
},
{
accessorKey: "transaction_type",
header: _("Type"),
size: 112,
cell: ({ row }) =>
row.original.transaction_type ? <Badge>{row.original.transaction_type}</Badge> : null,
},
{
id: "status",
header: _("Status"),
size: 168,
meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta,
cell: ({ row }) => {
const tx = row.original
if (!tx.allocated_amount || (tx.allocated_amount && tx.allocated_amount === 0)) {
return (
<Badge theme="red">
<XCircle />
{_("Not Reconciled")}
</Badge>
)
}
if (tx.allocated_amount && tx.allocated_amount > 0 && tx.unallocated_amount !== 0) {
return (
<Badge theme="orange">
<CheckCircle2 />
{_("Partially Reconciled")}
</Badge>
)
}
return (
<Badge theme="green">
<CheckCircle2 />
{_("Reconciled")}
</Badge>
)
},
},
{
id: "actions",
header: _("Actions"),
size: 200,
enableResizing: false,
meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta,
cell: ({ row }) => (
<div className="flex gap-2 ps-0.5 items-center">
<Button variant="ghost" asChild size='sm'>
<a
href={`/desk/bank-transaction/${row.original.name}`}
target="_blank"
rel="noreferrer"
// className="text-ink-gray-8 underline underline-offset-4 inline-flex gap-2"
>
{_("View")} <ExternalLink className="w-4 h-4" />
</a>
</Button>
{row.original.allocated_amount && row.original.allocated_amount > 0 ? (
<Button
variant="ghost"
onClick={() => onUndo(row.original)}
size="sm"
theme='red'
>
<Undo2 />
{_("Undo")}
</Button>
) : null}
</div>
),
},
],
[_, accountCurrency, onUndo],
)
const [search, setSearch] = useDebounceValue('', 250)
const [amountFilter, setAmountFilter] = useState<{ value: number, stringValue?: string | number }>({ value: 0, stringValue: '0.00' })
const [typeFilter, setTypeFilter] = useState('All')
const [status, setStatus] = useState<'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled'>('All')
const onSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
const filteredResults = useMemo(() => {
if (!data) {
return []
}
return data.message.filter((transaction) => {
if (search && !transaction.description?.toLowerCase().includes(search.toLowerCase())) {
return false
}
if (typeFilter !== 'All') {
if (typeFilter === 'Debits' && transaction.deposit && transaction.deposit > 0) {
return false
}
if (typeFilter === 'Credits' && transaction.withdrawal && transaction.withdrawal > 0) {
return false
}
}
if (status !== 'All') {
if (status === 'Reconciled' && transaction.status !== 'Reconciled') {
return false
}
if (status === 'Unreconciled') {
if (transaction.status === 'Reconciled') {
return false
}
// Filter out partially reconciled transactions
if (transaction.allocated_amount && transaction.allocated_amount > 0 && transaction.unallocated_amount !== 0) {
return false
}
}
if (status === 'Partially Reconciled') {
if (transaction.status === 'Reconciled') {
return false
}
if ((transaction.allocated_amount ?? 0) === 0) {
return false
}
}
}
if (amountFilter.value > 0 && transaction.withdrawal !== amountFilter.value && transaction.deposit !== amountFilter.value) {
return false
}
return true
})
}, [data, search, amountFilter, typeFilter, status])
return <div className="space-y-2 py-2">
<div className="flex gap-2 justify-between items-center">
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("Below is a list of all bank transactions imported in the system for the bank account {0} between {1} and {2}.", [`<strong>${bankAccount?.account_name}</strong>`, `<strong>${formattedFromDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
</Paragraph>
<Button size='md' variant='subtle' asChild>
<Link to="/statement-importer">
<ImportIcon />
{_("Import Bank Statement")}
</Link>
</Button>
</div>
{error && <ErrorBanner error={error} />}
<Filters
onSearchChange={onSearchChange}
search={search}
results={filteredResults}
setAmountFilter={setAmountFilter}
amountFilter={amountFilter}
onTypeFilterChange={setTypeFilter}
typeFilter={typeFilter}
status={status}
setStatus={setStatus}
/>
<ListView
data={filteredResults}
columns={transactionColumns}
getRowId={(row) => row.name}
maxHeight="calc(100vh - 200px)"
scrollAreaClassName="min-h-[calc(100vh-200px)]"
emptyState={<Empty>
<EmptyMedia>
<ListIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No bank transactions found")}</EmptyTitle>
<EmptyDescription>{_("There are no transactions in the system for the selected bank account and dates that match the filters.")}</EmptyDescription>
</EmptyHeader>
{data && data.message.length === 0 ? <EmptyContent>
<Button type='button' asChild variant='outline'>
<Link to="/statement-importer">
{_("Import Bank Statement")}
</Link>
</Button>
</EmptyContent> : null}
</Empty>}
/>
</div>
}
interface FilterProps {
onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void
search: string
results: BankTransaction[]
setAmountFilter: (value: { value: number, stringValue?: string | number }) => void
amountFilter: { value: number, stringValue?: string | number }
onTypeFilterChange: (type: string) => void
typeFilter: string
status: 'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled'
setStatus: (status: 'Reconciled' | 'Unreconciled' | 'All' | 'Partially Reconciled') => void
}
const Filters = ({
onSearchChange,
search,
results,
setAmountFilter,
amountFilter,
onTypeFilterChange,
typeFilter,
status,
setStatus,
}: FilterProps) => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '')
const currencySymbol = getCurrencySymbol(currency)
const formatInfo = getCurrencyFormatInfo(currency)
const groupSeparator = formatInfo.group_sep || ","
const decimalSeparator = formatInfo.decimal_str || "."
return <div className="flex py-2 w-full gap-2">
<InputGroup variant='outline'>
<label className="sr-only">{_("Search transactions")}</label>
<InputGroupAddon>
<Search className="w-4 h-4 text-ink-gray-5" />
</InputGroupAddon>
<Input
placeholder={_("Search")} type='search' onChange={onSearchChange} variant='outline' defaultValue={search}
className="border-none px-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0" />
<InputGroupAddon align='inline-end'>
<span className="text-sm text-ink-gray-5 text-nowrap whitespace-nowrap">{results?.length} {_(results?.length === 1 ? "result" : "results")}</span>
</InputGroupAddon>
</InputGroup>
<div className="w-[25%]">
<label className="sr-only">{_("Filter by amount")}</label>
<CurrencyInput
groupSeparator={groupSeparator}
decimalSeparator={decimalSeparator}
placeholder={`${currencySymbol}0${decimalSeparator}00`}
decimalsLimit={2}
value={amountFilter.stringValue}
maxLength={12}
decimalScale={2}
prefix={currencySymbol}
onValueChange={(v, _n, values) => {
// If the input ends with a decimal or a decimal with trailing zeroes, store the string since we need the user to be able to type the decimals.
// When the user eventually types the decimals or blurs out, the value is formatted anyway.
// Otherwise store the float value
// Check if the value ends with a decimal or a decimal with trailing zeroes
const isDecimal = v?.endsWith(decimalSeparator) || v?.endsWith(decimalSeparator + '0')
const newValue = isDecimal ? v : values?.float ?? ''
setAmountFilter({
value: Number(newValue),
stringValue: newValue
})
}}
// @ts-expect-error - CurrencyInputProps doesn't have a variant prop but Input does
variant={"outline"}
customInput={Input}
/>
</div>
<div className="w-[25%]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size='md' className="min-w-32 w-full text-start justify-between">
<div className="flex gap-2 items-center">
{typeFilter === 'All' ? <DollarSign className="w-4 h-4 text-ink-gray-5" /> : typeFilter === 'Debits' ? <ArrowUpRight className="w-4 h-4 text-ink-red-3" /> : <ArrowDownRight className="w-4 h-4 text-ink-green-3" />}
{_(typeFilter)}
</div>
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onTypeFilterChange('All')}><DollarSign /> {_("All")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => onTypeFilterChange('Debits')}><ArrowUpRight className="text-ink-red-3" /> {_("Debits")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => onTypeFilterChange('Credits')}><ArrowDownRight className="text-ink-green-3" /> {_("Credits")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="w-[25%]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size='md' className="min-w-32 w-full text-start justify-between">
<div className="flex gap-2 items-center">
{status === 'All' ? <ListIcon className="w-4 h-4 text-ink-gray-5" /> :
status === 'Reconciled' ? <CheckCircle2 className="w-4 h-4 text-ink-green-3" /> :
status === 'Unreconciled' ? <XCircle className="w-4 h-4 text-ink-red-3" /> :
<CheckCircle2 className="w-4 h-4 text-yellow-500" />}
{_(status)}
</div>
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => setStatus('All')}>{<ListIcon className="w-4 h-4 text-ink-gray-5" />} {_("All")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setStatus('Reconciled')}>{<CheckCircle2 className="w-4 h-4 text-ink-green-3" />} {_("Reconciled")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setStatus('Unreconciled')}>{<XCircle className="w-4 h-4 text-ink-red-3" />} {_("Unreconciled")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => setStatus('Partially Reconciled')}>{<CheckCircle2 className="w-4 h-4 text-yellow-500" />} {_("Partially Reconciled")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
}
export default BankTransactions

View File

@@ -1,125 +0,0 @@
import { AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "@/components/ui/alert-dialog"
import { useAtom, useAtomValue } from "jotai"
import { bankRecDateAtom, bankRecUnreconcileModalAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useMemo } from "react"
import { useFrappeGetDoc, useFrappePostCall, useSWRConfig } from "frappe-react-sdk"
import { BankTransaction } from "@/types/Accounts/BankTransaction"
import { toast } from "sonner"
import ErrorBanner from "@/components/ui/error-banner"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { formatCurrency } from "@/lib/numbers"
import { Badge } from "@/components/ui/badge"
import { slug } from "@/lib/frappe"
import SelectedTransactionDetails from "./SelectedTransactionDetails"
import _ from "@/lib/translate"
const BankTransactionUnreconcileModal = () => {
const [unreconcileModal, setBankRecUnreconcileModal] = useAtom(bankRecUnreconcileModalAtom)
const onOpenChange = (v: boolean) => {
if (!v) {
setBankRecUnreconcileModal('')
}
}
return <AlertDialog open={!!unreconcileModal} onOpenChange={onOpenChange}>
<AlertDialogOverlay />
<AlertDialogContent className="min-w-2xl">
<AlertDialogHeader>
<AlertDialogTitle>{_("Undo Transaction Reconciliation")}</AlertDialogTitle>
<AlertDialogDescription>
{_("Are you sure you want to unreconcile this transaction?")}
</AlertDialogDescription>
</AlertDialogHeader>
<BankTransactionUnreconcileModalContent />
</AlertDialogContent>
</AlertDialog>
}
const BankTransactionUnreconcileModalContent = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const { mutate } = useSWRConfig()
const [unreconcileModal, setBankRecUnreconcileModal] = useAtom(bankRecUnreconcileModalAtom)
const { data: transaction, error } = useFrappeGetDoc<BankTransaction>('Bank Transaction', unreconcileModal)
const { call, loading, error: unreconcileError } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unreconcile_transaction')
const onUnreconcile = (event: React.MouseEvent<HTMLButtonElement>) => {
call({
transaction_name: unreconcileModal
}).then(() => {
// Mutate the transactions list, unreconciled transactions list and account closing balance
mutate(`bank-reconciliation-bank-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}`)
mutate(`bank-reconciliation-unreconciled-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}`)
mutate(`bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`)
toast.success(_("Transaction Unreconciled"))
setBankRecUnreconcileModal('')
})
event.preventDefault()
}
const vouchersWhichWillBeCancelled = useMemo(() => {
return transaction?.payment_entries?.filter((payment) => payment.reconciliation_type === 'Voucher Created')
}, [transaction])
return <div>
<div className="flex flex-col gap-3">
{error && <ErrorBanner error={error} />}
{unreconcileError && <ErrorBanner error={unreconcileError} />}
{transaction && <SelectedTransactionDetails transaction={transaction} />}
<span className="font-medium text-sm">{_("This transaction has been reconciled with the following document(s):")}</span>
<Table>
<TableHeader>
<TableRow>
<TableHead>{_("Document")}</TableHead>
<TableHead>{_("Amount")}</TableHead>
<TableHead>{_("Reconciliation Type")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transaction?.payment_entries?.map((voucher) => {
return <TableRow key={voucher.name}>
<TableCell>
<a className="underline underline-offset-4"
target="_blank"
rel="noopener noreferrer"
href={`/desk/${slug(voucher.payment_document as string)}/${voucher.payment_entry}`}
>
{`${_(voucher.payment_document)}: ${voucher.payment_entry}`}
</a>
</TableCell>
<TableCell>{formatCurrency(voucher.allocated_amount)}</TableCell>
<TableCell>{voucher.reconciliation_type === 'Voucher Created' ?
<Badge theme="green">{_(voucher.reconciliation_type)}</Badge> :
<Badge theme="blue">{_(voucher.reconciliation_type ?? "Matched")}</Badge>}</TableCell>
</TableRow>
})}
</TableBody>
</Table>
<div className="py-4">
{vouchersWhichWillBeCancelled && vouchersWhichWillBeCancelled?.length > 0 && <span>The following documents will be <strong>cancelled</strong>:</span>}
{vouchersWhichWillBeCancelled && vouchersWhichWillBeCancelled?.length > 0 && <ol className="ms-6 list-disc [&>li]:mt-2">
{vouchersWhichWillBeCancelled?.map((voucher) => {
return <li key={voucher.name}>{_(voucher.payment_document)}: {voucher.payment_entry}</li>
})}
</ol>}
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>{_("Cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={onUnreconcile} theme="red" disabled={loading}>
{_("Unreconcile")}
</AlertDialogAction>
</AlertDialogFooter>
</div>
}
export default BankTransactionUnreconcileModal

View File

@@ -1,92 +0,0 @@
import { Button } from "@/components/ui/button"
import { selectedCompanyAtom, useCurrentCompany } from "@/hooks/useCurrentCompany"
import { useSetAtom } from "jotai"
import { Building2, Check, ChevronDown } from "lucide-react"
import { useState } from "react"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { cn } from "@/lib/utils"
import _ from "@/lib/translate"
import { selectedBankAccountAtom } from "./bankRecAtoms"
const CompanySelector = ({ onChange }: { onChange?: (company: string) => void }) => {
const [open, setOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState("")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options = window.frappe?.boot?.docs?.filter((doc: Record<string, any>) => doc.doctype === ":Company").map((company: Record<string, any>) => company.name) || []
const setSelectedCompany = useSetAtom(selectedCompanyAtom)
const setSelectedBankAccount = useSetAtom(selectedBankAccountAtom)
const selectedCompany = useCurrentCompany()
const handleSelectCompany = (company: string) => {
setSelectedCompany(company)
setSearchQuery("")
setOpen(false)
// Only reset bank account if the company is changed
if (selectedCompany !== company) {
setSelectedBankAccount(null)
onChange?.(company)
}
}
return (<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
type='button'
role="combobox"
size='md'
aria-expanded={open}
className="justify-between"
>
<div className="flex items-center gap-2">
<Building2 />
{selectedCompany}
</div>
<ChevronDown className="text-ink-gray-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="min-w-56 w-fit p-0">
<Command value={selectedCompany}>
{options.length > 5 && <CommandInput placeholder={_("Search company...")} className="h-9" />}
<CommandList>
<CommandEmpty>{_("No company found.")}</CommandEmpty>
<CommandGroup>
{options.map((option: string) => (
<CommandItem
key={option}
value={option}
onSelect={(currentValue) => {
handleSelectCompany(currentValue)
}}
>
{option}
<Check
className={cn(
"ms-auto",
searchQuery === option ? "opacity-100" : "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>)
}
export default CompanySelector

View File

@@ -1,229 +0,0 @@
import { useAtomValue } from "jotai"
import { MissingFiltersBanner } from "./MissingFiltersBanner"
import { bankRecDateAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import { Paragraph } from "@/components/ui/typography"
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useMemo } from "react"
import { useFrappeGetCall, useFrappePostCall } from "frappe-react-sdk"
import { QueryReportReturnType } from "@/types/custom/Reports"
import { formatDate } from "@/lib/date"
import { ListView, type ListViewColumnMeta } from "@/components/ui/list-view"
import { formatCurrency } from "@/lib/numbers"
import { getCompanyCurrency } from "@/lib/company"
import { getErrorMessage, slug } from "@/lib/frappe"
import { Button } from "@/components/ui/button"
import { toast } from "sonner"
import { PartyPopper } from "lucide-react"
import ErrorBanner from "@/components/ui/error-banner"
import _ from "@/lib/translate"
import { Empty, EmptyTitle, EmptyDescription, EmptyMedia, EmptyHeader } from "@/components/ui/empty"
const IncorrectlyClearedEntries = () => {
const companyID = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
if (!companyID || !bankAccount || !dates) {
const missingFields = []
if (!companyID) {
missingFields.push('Company')
}
if (!bankAccount) {
missingFields.push('Bank Account')
}
if (!dates) {
missingFields.push('Dates')
}
return <MissingFiltersBanner text={`Please select ${missingFields.join(', ')} to view the incorrectly cleared entries.`} />
}
return <IncorrectlyClearedEntriesView />
}
interface IncorrectlyClearedEntry {
payment_document: string
payment_entry: string
debit: number
credit: number
posting_date: string,
clearance_date: string,
}
const IncorrectlyClearedEntriesView = () => {
const companyID = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const filters = useMemo(() => {
return JSON.stringify({
company: companyID,
account: bankAccount?.account,
report_date: dates.toDate
})
}, [companyID, bankAccount, dates])
const { data, error, mutate } = useFrappeGetCall<{ message: QueryReportReturnType<IncorrectlyClearedEntry> }>('frappe.desk.query_report.run', {
report_name: 'Cheques and Deposits Incorrectly cleared',
filters,
ignore_prepared_report: 1,
are_default_filters: false,
}, `Report-Cheques and Deposits Incorrectly cleared-${filters}`, { keepPreviousData: true, revalidateOnFocus: false }, 'POST')
const formattedToDate = formatDate(dates.toDate)
const { call: clearClearingDate } = useFrappePostCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.clear_clearing_date')
const onClearClick = useCallback(
(voucher_type: string, voucher_name: string) => {
clearClearingDate({ voucher_type, voucher_name })
.then(() => {
toast.success(_("Cleared"), {
duration: 1000,
})
mutate()
})
.catch((e) => {
toast.error(_("There was an error while performing the action."), {
description: getErrorMessage(e),
duration: 5000,
})
})
},
[clearClearingDate, mutate, _],
)
const accountCurrency = useMemo(
() => bankAccount?.account_currency ?? getCompanyCurrency(companyID),
[bankAccount?.account_currency, companyID],
)
const incorrectlyClearedColumns = useMemo<ColumnDef<IncorrectlyClearedEntry, unknown>[]>(
() => [
{
accessorKey: "payment_document",
header: _("Document Type"),
size: 128,
cell: ({ row }) => _(row.original.payment_document),
},
{
id: "payment_entry",
header: _("Payment Document"),
size: 160,
meta: {
getTooltipText: (r) => {
const x = r as IncorrectlyClearedEntry
return [x.payment_document, x.payment_entry].filter(Boolean).join(" · ") || undefined
},
} satisfies ListViewColumnMeta,
cell: ({ row }) => (
<a
target="_blank"
rel="noreferrer"
className="text-ink-gray-8 block min-w-0 w-full underline underline-offset-4"
href={`/desk/${slug(row.original.payment_document)}/${row.original.payment_entry}`}
>
{row.original.payment_entry}
</a>
),
},
{
accessorKey: "debit",
header: _("Debit"),
size: 120,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => formatCurrency(row.original.debit, accountCurrency),
},
{
accessorKey: "credit",
header: _("Credit"),
size: 120,
meta: { align: "right" } satisfies ListViewColumnMeta,
cell: ({ row }) => formatCurrency(row.original.credit, accountCurrency),
},
{
accessorKey: "posting_date",
header: _("Posting Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.posting_date),
},
{
accessorKey: "clearance_date",
header: _("Clearance Date"),
size: 118,
meta: { tabularNums: true } satisfies ListViewColumnMeta,
cell: ({ row }) => formatDate(row.original.clearance_date),
},
{
id: "actions",
header: _("Actions"),
size: 180,
enableResizing: false,
meta: { truncate: false, truncateTooltip: false } satisfies ListViewColumnMeta,
cell: ({ row }) => (
<Button
variant="link"
size="sm"
className="text-ink-red-3 px-0"
onClick={() => onClearClick(row.original.payment_document, row.original.payment_entry)}
>
{_("Reset Clearing Date")}
</Button>
),
},
],
[_, accountCurrency, onClearClick],
)
return <div className="space-y-4 py-2">
<div>
<Paragraph className="text-sm">
<span dangerouslySetInnerHTML={{
__html: _("This report shows all entries in the system where the <strong>clearance date is before the posting date</strong> which is incorrect.")
}} />
<br />
{data && data.message.result.length > 0 && <span>
<span dangerouslySetInnerHTML={{
__html: _("Entries below have a posting date after {0} but the clearance date is before {1}.", [`<strong>${formattedToDate}</strong>`, `<strong>${formattedToDate}</strong>`])
}} />
<br />
{_("You can reset the clearing dates of these entries here.")}
</span>}
</Paragraph>
</div>
{error && <ErrorBanner error={error} />}
{data && data.message.result.length > 0 && (
<div className="space-y-2">
<p className="text-ink-gray-5 text-sm">{_("Incorrectly cleared entries as per the report.")}</p>
<ListView
data={data.message.result}
columns={incorrectlyClearedColumns}
getRowId={(row) => `${row.payment_entry}-${row.posting_date}`}
maxHeight="min(70vh, 640px)"
emptyState={_("No rows to display.")}
/>
</div>
)}
{data && data.message.result.length === 0 &&
<Empty>
<EmptyMedia>
<PartyPopper />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("It's all good!")}</EmptyTitle>
<EmptyDescription>{_("There are no entries in the system where the clearance date is before the posting date.")}</EmptyDescription>
</EmptyHeader>
</Empty>
}
</div>
}
export default IncorrectlyClearedEntries

View File

@@ -1,949 +0,0 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai"
import { bankRecAmountFilter, bankRecDateAtom, bankRecRecordJournalEntryModalAtom, bankRecRecordPaymentModalAtom, bankRecSelectedTransactionAtom, bankRecTransactionTypeFilter, bankRecTransferModalAtom, selectedBankAccountAtom } from "./bankRecAtoms"
import { H4 } from "@/components/ui/typography"
import { useMemo, useRef } from "react"
import { getCompanyCurrency } from "@/lib/company"
import ErrorBanner from "@/components/ui/error-banner"
import { Separator } from "@/components/ui/separator"
import Fuse from 'fuse.js'
import { getSearchResults, LinkedPayment, UnreconciledTransaction, useGetRuleForTransaction, useGetUnreconciledTransactions, useGetVouchersForTransaction, useIsTransactionWithdrawal, useReconcileTransaction, useTransactionSearch } from "./utils"
import { Input } from "@/components/ui/input"
import { AlertCircleIcon, ArrowDownRight, ArrowRightIcon, ArrowRightLeft, ArrowUpRight, BadgeCheck, ChevronDown, DollarSign, Landmark, LandmarkIcon, ListIcon, Loader2, Receipt, ReceiptIcon, Search, User, XCircle, ZapIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
import CurrencyInput from 'react-currency-input-field'
import { getCurrencySymbol } from "@/lib/currency"
import { Virtuoso } from 'react-virtuoso'
import { formatDate } from "@/lib/date"
import { Badge } from "@/components/ui/badge"
import { formatCurrency, getCurrencyFormatInfo } from "@/lib/numbers"
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip"
import { Skeleton } from "@/components/ui/skeleton"
import { slug } from "@/lib/frappe"
import _ from "@/lib/translate"
import TransferModal from "./TransferModal"
import BankEntryModal from "./BankEntryModal"
import RecordPaymentModal from "./RecordPaymentModal"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import SelectedTransactionsTable from "./SelectedTransactionsTable"
import MatchFilters from "./MatchFilters"
import { useHotkeys } from "react-hotkeys-hook"
import { KeyboardMetaKeyIcon } from "@/components/ui/keyboard-keys"
import { Kbd, KbdGroup } from "@/components/ui/kbd"
import { useFrappeGetCall } from "frappe-react-sdk"
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { Link } from "react-router"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
const MatchAndReconcile = ({ contentHeight }: { contentHeight: number }) => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
if (!selectedBank) {
return <Empty>
<EmptyMedia>
<LandmarkIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("Select a bank account to reconcile")}</EmptyTitle>
</EmptyHeader>
</Empty>
}
return <>
<div className={`flex items-start space-x-2`} >
<div className="flex-1">
<H4 className="text-sm font-medium">{_("Unreconciled Transactions")}</H4>
<UnreconciledTransactions contentHeight={contentHeight} />
</div>
<Separator orientation="vertical" style={{ minHeight: `${contentHeight}px` }} />
<div className="flex-1 px-1">
<H4 className="text-sm font-medium">{_("Match or Create")}</H4>
<VouchersSection contentHeight={contentHeight} />
</div>
</div>
<TransferModal />
<BankEntryModal />
<RecordPaymentModal />
</>
}
const UnreconciledTransactions = ({ contentHeight }: { contentHeight: number }) => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const currency = bankAccount?.account_currency ?? getCompanyCurrency(bankAccount?.company ?? '')
const currencySymbol = getCurrencySymbol(currency)
const formatInfo = getCurrencyFormatInfo(currency)
const groupSeparator = formatInfo.group_sep || ","
const decimalSeparator = formatInfo.decimal_str || "."
const inputRef = useRef<HTMLInputElement>(null)
const { data: unreconciledTransactions, isLoading, error } = useGetUnreconciledTransactions()
const [typeFilter, setTypeFilter] = useAtom(bankRecTransactionTypeFilter)
const [amountFilter, setAmountFilter] = useAtom(bankRecAmountFilter)
const [search, setSearch] = useTransactionSearch()
const searchIndex = useMemo(() => {
if (!unreconciledTransactions) {
return null
}
return new Fuse(unreconciledTransactions.message, {
keys: ['description', 'reference_number'],
threshold: 0.5,
includeScore: true
})
}, [unreconciledTransactions])
const results = useMemo(() => {
return getSearchResults(searchIndex, search, typeFilter, amountFilter.value, unreconciledTransactions?.message)
}, [searchIndex, search, typeFilter, amountFilter.value, unreconciledTransactions?.message])
const setSelectedTransaction = useSetAtom(bankRecSelectedTransactionAtom(bankAccount?.name || ''))
const onFilterChange = () => {
setSelectedTransaction([])
}
const onSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
onFilterChange()
}
const onTypeFilterChange = (type: string) => {
setTypeFilter(type)
onFilterChange()
}
const onClearFilters = () => {
setSearch('')
if (inputRef.current) {
inputRef.current.value = ''
}
setTypeFilter('All')
setAmountFilter({ value: 0, stringValue: '' })
onFilterChange()
}
const hasFilters = search !== '' || typeFilter !== 'All' || amountFilter.value !== 0
if (isLoading) {
return <UnreconciledTransactionsLoadingState />
}
return <div className="space-y-1">
<div className="flex py-2 w-full gap-2">
<InputGroup variant='outline'>
<label className="sr-only">{_("Search transactions")}</label>
<InputGroupAddon>
<Search className="w-4 h-4 text-ink-gray-5" />
</InputGroupAddon>
<Input
placeholder={_("Search")}
// type='search'
variant='outline'
onChange={onSearchChange}
defaultValue={search}
ref={inputRef}
/>
<InputGroupAddon align='inline-end'>
<InputGroupText>{results?.length} {_(results?.length === 1 ? "result" : "results")}</InputGroupText>
</InputGroupAddon>
</InputGroup>
<div>
<label className="sr-only">{_("Filter by amount")}</label>
<CurrencyInput
groupSeparator={groupSeparator}
decimalSeparator={decimalSeparator}
placeholder={`${currencySymbol}0${decimalSeparator}00`}
decimalsLimit={2}
value={amountFilter.stringValue}
maxLength={12}
decimalScale={2}
prefix={currencySymbol}
onValueChange={(v, _n, values) => {
// If the input ends with a decimal or a decimal with trailing zeroes, store the string since we need the user to be able to type the decimals.
// When the user eventually types the decimals or blurs out, the value is formatted anyway.
// Otherwise store the float value
// Check if the value ends with a decimal or a decimal with trailing zeroes
const isDecimal = v?.endsWith(decimalSeparator) || v?.endsWith(decimalSeparator + '0')
const newValue = isDecimal ? v : values?.float ?? ''
const nextAmountFilter = {
value: Number(newValue),
stringValue: newValue
}
const hasAmountFilterChanged = amountFilter.value !== nextAmountFilter.value || amountFilter.stringValue !== nextAmountFilter.stringValue
setAmountFilter(nextAmountFilter)
// `onValueChange` also fires on blur; avoid clearing selected transaction unless filter value actually changed.
if (hasAmountFilterChanged) {
onFilterChange()
}
}}
// @ts-expect-error - CurrencyInputProps doesn't have a variant prop but Input does
variant={"outline"}
customInput={Input}
/>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size='md' className="min-w-32 text-start">
{typeFilter === 'All' ? <DollarSign className="text-ink-gray-5" /> : typeFilter === 'Debits' ? <ArrowUpRight className="text-ink-red-3" /> : <ArrowDownRight className="text-ink-green-3" />}
{_(typeFilter)}
<ChevronDown className="text-ink-gray-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => onTypeFilterChange('All')}><DollarSign /> {_("All")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => onTypeFilterChange('Debits')}><ArrowUpRight className="text-ink-red-3" /> {_("Debits")}</DropdownMenuItem>
<DropdownMenuItem onClick={() => onTypeFilterChange('Credits')}><ArrowDownRight className="text-ink-green-3" /> {_("Credits")}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{error && <ErrorBanner error={error} />}
<OlderUnreconciledTransactionsBanner />
{results.length === 0 && <NoTransactionsFoundBanner
onClearFilters={hasFilters ? onClearFilters : undefined}
text={hasFilters ? _("No transactions found for the given filters.") : _("No unreconciled transactions found")}
description={hasFilters ? _("Try adjusting your search or filter criteria.") : _("Import your bank statement to get started.")} />}
<Virtuoso
data={results}
itemContent={(_index, transaction) => (
<UnreconciledTransactionItem transaction={transaction} />
)}
style={{ minHeight: Math.max(contentHeight - 80, 400) }}
totalCount={results?.length}
/>
</div>
}
const NoTransactionsFoundBanner = ({ text, description, onClearFilters }: { text: string, description?: string, onClearFilters?: () => void }) => {
return <Empty>
<EmptyMedia>
<ListIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{text}</EmptyTitle>
{description && <EmptyDescription>{description}</EmptyDescription>}
</EmptyHeader>
<EmptyContent>
{onClearFilters ? <Button type='button' size='sm' variant='subtle' onClick={onClearFilters}>Clear Filters</Button> :
<Button type='button' asChild size='sm' variant='subtle'>
<Link to="/statement-importer">
{_("Import Bank Statement")}
</Link>
</Button>}
</EmptyContent>
</Empty>
}
const UnreconciledTransactionsLoadingState = () => {
return <div className="flex flex-col gap-2 py-2">
<div className="flex items-center gap-2 pb-2">
<Skeleton className="h-9.5 w-full" />
<Skeleton className="h-9.5 min-w-36" />
<Skeleton className="h-9.5 min-w-32" />
</div>
{Array.from({ length: 6 }).map((_, index) => (
<Skeleton key={index} className="h-16 w-full" />
))}
</div>
}
const UnreconciledTransactionItem = ({ transaction }: { transaction: UnreconciledTransaction }) => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
const [selectedTransaction, setSelectedTransaction] = useAtom(bankRecSelectedTransactionAtom(selectedBank?.name || ''))
const { amount, isWithdrawal } = useIsTransactionWithdrawal(transaction)
const isSelected = selectedTransaction?.some((t) => t.name === transaction.name)
const currency = transaction.currency ?? selectedBank?.account_currency ?? getCompanyCurrency(selectedBank?.company ?? '')
const handleSelectTransaction = (event: React.MouseEvent<HTMLDivElement>) => {
// If the user is pressing the shift key, add/remove the transaction from the selected transactions
if (event.shiftKey) {
setSelectedTransaction(isSelected ? selectedTransaction.filter((t) => t.name !== transaction.name) : [...selectedTransaction, transaction])
} else {
setSelectedTransaction([transaction])
}
}
return <div className="py-1">
<div className={cn("border outline rounded-md p-2 mx-0.5 cursor-pointer transition-[color,box-shadow, bg] hover:bg-surface-gray-1",
isSelected ? "bg-surface-gray-1 border-outline-gray-5 outline-outline-gray-5" : "border-outline-gray-2 outline-none"
)}
role='button'
tabIndex={0}
onClick={handleSelectTransaction}>
<div className="flex justify-between items-start w-full">
<div className="space-y-1 overflow-hidden whitespace-pre-wrap">
<div className="flex items-center gap-1">
<span className="font-medium text-sm">{formatDate(transaction.date)}</span>
{transaction.transaction_type &&
<Badge theme="blue">{transaction.transaction_type}</Badge>}
{transaction.reference_number && <Badge
title={transaction.reference_number}
className="max-w-[300px] text-ellipsis"
>
{_("Ref")}: {transaction.reference_number}</Badge>}
{transaction.matched_transaction_rule && <Badge
theme="violet"
title={_("Matched by rule")}>
<ZapIcon className="w-4 h-4" /> {transaction.matched_transaction_rule}</Badge>}
</div>
<span className="text-sm wrap-anywhere" title={transaction.description}>{transaction.description}</span>
</div>
<div className="gap-1 flex flex-col items-end min-w-36 h-full text-end">
{isWithdrawal ? <ArrowUpRight className="size-5 text-ink-red-3" /> : <ArrowDownRight className="size-5 text-ink-green-3" />}
{amount && amount > 0 && <span className="font-semibold font-numeric text-base">{formatCurrency(amount, currency)}</span>}
{amount !== transaction.unallocated_amount && <span className="text-xs leading-normal text-ink-gray-5">{formatCurrency(transaction.unallocated_amount, currency)} {_("Unallocated")}</span>}
</div>
</div>
</div>
</div>
}
const VouchersSection = ({ contentHeight }: { contentHeight: number }) => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
const selectedTransactions = useAtomValue(bankRecSelectedTransactionAtom(selectedBank?.name || ''))
if (selectedTransactions.length === 0) {
return <Empty>
<EmptyMedia>
<ReceiptIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("Select a transaction to match and reconcile with vouchers")}</EmptyTitle>
</EmptyHeader>
</Empty>
}
if (selectedTransactions.length > 1) {
return <OptionsForMultipleTransactions transactions={selectedTransactions} />
}
return <div style={{ minHeight: contentHeight }} className="mt-2">
<OptionsForSingleTransaction transaction={selectedTransactions[0]} contentHeight={contentHeight} />
</div>
}
const useKeyboardShortcuts = () => {
const setTransferModalOpen = useSetAtom(bankRecTransferModalAtom)
const setRecordPaymentModalOpen = useSetAtom(bankRecRecordPaymentModalAtom)
const setRecordJournalEntryModalOpen = useSetAtom(bankRecRecordJournalEntryModalAtom)
useHotkeys('meta+p', () => {
//
setRecordPaymentModalOpen(true)
}, {
enabled: true,
enableOnFormTags: false,
preventDefault: true
})
useHotkeys('meta+b', () => {
//
setRecordJournalEntryModalOpen(true)
}, {
enabled: true,
enableOnFormTags: false,
preventDefault: true
})
useHotkeys('meta+i', () => {
//
setTransferModalOpen(true)
}, {
enabled: true,
enableOnFormTags: false,
preventDefault: true
})
return {
setTransferModalOpen,
setRecordPaymentModalOpen,
setRecordJournalEntryModalOpen
}
}
const OptionsForMultipleTransactions = ({ transactions }: { transactions: UnreconciledTransaction[] }) => {
const { setTransferModalOpen, setRecordPaymentModalOpen, setRecordJournalEntryModalOpen } = useKeyboardShortcuts()
return <div className="flex flex-col py-4">
<Card className="gap-2">
<CardHeader>
<CardTitle>
<div className="flex items-center justify-between">
<span className="text-md font-medium">{transactions.length} {_(transactions.length === 1 ? _("transaction selected") : _("transactions selected"))}</span>
<span className="text-md font-medium font-numeric">
{formatCurrency(transactions.reduce((acc, transaction) => acc + (transaction.unallocated_amount ?? 0), 0), transactions[0].currency ?? '')}
</span>
</div>
</CardTitle>
</CardHeader>
<CardContent>
<SelectedTransactionsTable />
<CardAction className="mt-4 justify-self-center">
<div className="flex gap-3 justify-center">
<TooltipProvider>
<div className="flex gap-4 justify-center">
<Tooltip>
<TooltipTrigger asChild>
<Button
size='md'
aria-label={_("Record a bank journal entry for expenses, income or split transactions")}
onClick={() => setRecordJournalEntryModalOpen(true)}>
<Landmark /> {_("Bank Entry")}
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Record a journal entry for expenses, income or split transactions")}
<KbdGroup className="ms-2">
<Kbd><KeyboardMetaKeyIcon /></Kbd>
<Kbd>B</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='outline'
size='md'
aria-label={_("Record a payment entry against a customer or supplier")}
onClick={() => setRecordPaymentModalOpen(true)}>
<Receipt /> {_("Record Payment")}
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Record a payment entry against a customer or supplier")}
<KbdGroup className="ms-2">
<Kbd><KeyboardMetaKeyIcon /></Kbd>
<Kbd>P</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='outline'
size='md'
aria-label={_("Record an internal transfer to another bank/credit card/cash account")}
onClick={() => setTransferModalOpen(true)}>
<ArrowRightLeft /> {_("Transfer")}
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Record an internal transfer to another bank/credit card/cash account")}
<KbdGroup className="ms-2">
<Kbd><KeyboardMetaKeyIcon /></Kbd>
<Kbd>I</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
</div>
</CardAction>
</CardContent>
</Card>
</div>
}
const OptionsForSingleTransaction = ({ transaction, contentHeight }: { transaction: UnreconciledTransaction, contentHeight: number }) => {
const { setTransferModalOpen, setRecordPaymentModalOpen, setRecordJournalEntryModalOpen } = useKeyboardShortcuts()
return <div className="flex flex-col gap-3">
<TooltipProvider>
<div className="flex items-center justify-between pt-2">
<div className="flex gap-4 justify-center">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='outline'
size='md'
aria-label={_("Record a payment entry against a customer or supplier")}
onClick={() => setRecordPaymentModalOpen(true)}>
<Receipt /> {_("Record Payment")}
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Record a payment entry against a customer or supplier")}
<KbdGroup className="ms-2">
<Kbd><KeyboardMetaKeyIcon /></Kbd>
<Kbd>P</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant='outline'
size='md'
aria-label={_("Record a bank journal entry for expenses, income or split transactions")}
onClick={() => setRecordJournalEntryModalOpen(true)}>
<Landmark /> {_("Bank Entry")}
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Record a journal entry for expenses, income or split transactions")}
<KbdGroup className="ms-2">
<Kbd><KeyboardMetaKeyIcon /></Kbd>
<Kbd>B</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
<Tooltip >
<TooltipTrigger asChild>
<Button
variant='outline'
size='md'
aria-label={_("Record an internal transfer to another bank/credit card/cash account")}
onClick={() => setTransferModalOpen(true)}>
<ArrowRightLeft /> {_("Transfer")}
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Record an internal transfer to another bank/credit card/cash account")}
<KbdGroup className="ms-2">
<Kbd><KeyboardMetaKeyIcon /></Kbd>
<Kbd>I</Kbd>
</KbdGroup>
</TooltipContent>
</Tooltip>
</div>
<MatchFilters />
</div>
</TooltipProvider>
{transaction.matched_transaction_rule && <RuleAction transaction={transaction} />}
<VouchersForTransaction transaction={transaction} contentHeight={contentHeight} />
</div>
}
const RuleAction = ({ transaction }: { transaction: UnreconciledTransaction }) => {
const { data: rule } = useGetRuleForTransaction(transaction)
const setTransferModalOpen = useSetAtom(bankRecTransferModalAtom)
const setRecordPaymentModalOpen = useSetAtom(bankRecRecordPaymentModalAtom)
const setRecordJournalEntryModalOpen = useSetAtom(bankRecRecordJournalEntryModalAtom)
if (!rule) {
return null
}
const getActionIcon = () => {
switch (rule.classify_as) {
case "Bank Entry":
return <Landmark />
case "Payment Entry":
return <Receipt className="w-6 h-6" />
case "Transfer":
return <ArrowRightLeft />
default:
return <ZapIcon />
}
}
const getActionStyles = () => {
switch (rule.classify_as) {
case "Bank Entry":
return {
border: "border-outline-blue-3",
bg: "bg-surface-blue-1/50",
text: "text-ink-blue-4",
theme: "blue",
}
case "Payment Entry":
return {
border: "border-outline-green-3",
bg: "bg-surface-green-1/50",
text: "text-ink-green-4",
theme: "green",
}
case "Transfer":
return {
border: "border-outline-violet-3",
bg: "bg-surface-violet-2/50",
text: "text-ink-violet-4",
theme: "violet",
}
default:
return {
border: "border-outline-amber-3",
bg: "bg-surface-amber-1/50",
text: "text-ink-amber-4",
theme: "orange",
}
}
}
const handleActionClick = () => {
switch (rule.classify_as) {
case "Bank Entry":
setRecordJournalEntryModalOpen(true)
break
case "Payment Entry":
setRecordPaymentModalOpen(true)
break
case "Transfer":
setTransferModalOpen(true)
break
}
}
const getActionDescription = () => {
switch (rule.classify_as) {
case "Bank Entry":
return _("Create a journal entry for expenses, income or split transactions")
case "Payment Entry":
return _("Record a payment entry against a customer or supplier")
case "Transfer":
return _("Record an internal transfer to another bank/credit card/cash account")
default:
return _("Create a new entry based on the rule")
}
}
useHotkeys('meta+r', () => {
//
handleActionClick()
}, {
enabled: true,
enableOnFormTags: false,
preventDefault: true
})
const styles = getActionStyles()
return (
<Card className={`border ${styles.border} ${styles.bg} shadow-sm hover:shadow-md transition-all duration-200`}>
<CardHeader className="pb-0">
<CardTitle className="flex justify-between items-center gap-3">
<div className="flex items-center gap-3">
<div className={`px-2.5 rounded-lg ${styles.bg} ${styles.text}`}>
{getActionIcon()}
</div>
<div className="flex flex-col gap-0.5">
<span className="font-semibold text-lg">{rule.rule_name}</span>
<span className="text-sm text-ink-gray-5 font-normal">
{rule.rule_description || _("Rule matched based on transaction description and other criteria.")}
</span>
</div>
</div>
<div className="flex items-center gap-0.5">
<Badge size='lg'
theme={rule.classify_as === "Bank Entry" ? "blue" : rule.classify_as === "Payment Entry" ? "green" : rule.classify_as === "Transfer" ? "violet" : "orange"}>
{rule.classify_as}
</Badge>
</div>
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-3">
<div className="flex items-center justify-between p-2 bg-surface-white rounded-lg border border-outline-gray-1">
<div className="flex items-center gap-2">
<BadgeCheck className="w-4 h-4 text-ink-green-3" />
<span className="text-sm font-medium text-ink-gray-8">{_("Recommended Action")}</span>
</div>
<Badge variant="ghost" theme={styles.theme as "blue" | "green" | "violet" | "orange"}>
{_("Priority")} {rule.priority}
</Badge>
</div>
<div className="space-y-2">
{rule.account && (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-ink-gray-8">{_("Account")}:</span>
<span className="text-sm">{rule.account}</span>
</div>
)}
{rule.party_type && rule.party && (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-ink-gray-8">{_("Party")}:</span>
<span className="text-sm">{rule.party} ({_(rule.party_type)})</span>
</div>
)}
</div>
<div className="pt-1">
<Button
onClick={handleActionClick}
className={`w-full`}
theme={styles.theme as "blue" | "green" | "violet"}
size="md"
>
{getActionIcon()}
<span>{_("Create")} {rule.classify_as}</span>
</Button>
<p className="text-sm text-ink-gray-5 mt-2 text-center leading-relaxed">
{getActionDescription()}
</p>
</div>
</CardContent>
</Card>
)
}
const VouchersForTransaction = ({ transaction, contentHeight }: { transaction: UnreconciledTransaction, contentHeight: number }) => {
const { data: vouchers, isLoading, error } = useGetVouchersForTransaction(transaction)
if (error) {
return <ErrorBanner error={error} />
}
if (isLoading) {
return <div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-sm text-ink-gray-5">
<Separator className="flex-1" />
<span>or</span>
<Separator className="flex-1" />
</div>
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
<Skeleton className="h-16 w-full" />
</div>
}
return <div className="relative space-y-2">
<div className="flex items-center gap-2 text-sm text-ink-gray-5">
<Separator className="flex-1" />
<span>or</span>
<Separator className="flex-1" />
</div>
{vouchers?.message.length === 0 && <Empty className="my-4">
<EmptyMedia>
<ReceiptIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No vouchers found for this transaction")}</EmptyTitle>
</EmptyHeader>
</Empty>}
<Virtuoso
data={vouchers?.message}
itemContent={(index, voucher) => (
<VoucherItem voucher={voucher} index={index} />
)}
style={{ height: contentHeight }}
totalCount={vouchers?.message.length}
/>
</div >
}
const VoucherItem = ({ voucher, index }: { voucher: LinkedPayment, index: number }) => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBank?.name || ''))
const { amountMatches, postingDateMatches, referenceDateMatches, referenceMatchesFull, referenceMatchesPartial, isSuggested } = useMemo(() => {
const transaction = selectedTransaction?.[0]
// We need to check if the following details match:
// Amount
// Date
// Reference/Description: Full or partial
// Whether this is suggested or not - depends on the above scores
const amountMatches = voucher.paid_amount === transaction?.unallocated_amount
const postingDateMatches = voucher.posting_date === transaction?.date
const referenceDateMatches = voucher.reference_date === transaction?.date
const referenceMatchesFull = voucher.reference_no === transaction?.reference_number || voucher.reference_no === transaction?.description
const referenceMatchesPartial = transaction?.reference_number?.includes(voucher.reference_no) || transaction?.description?.includes(voucher.reference_no)
const isSuggested = amountMatches && (postingDateMatches || referenceDateMatches || referenceMatchesPartial) && index === 0
return { isSelected: false, amountMatches, postingDateMatches, referenceDateMatches, referenceMatchesFull, referenceMatchesPartial, isSuggested: isSuggested }
}, [voucher, selectedTransaction, index])
const { reconcileTransaction, loading } = useReconcileTransaction()
const onClick = () => {
if (!selectedTransaction) {
return
}
reconcileTransaction(selectedTransaction[0], voucher)
}
return <div className="py-1 px-1">
<div
className={cn("border outline overflow-hidden relative rounded-md p-2",
isSuggested ? "border-outline-green-4 bg-surface-green-1/40 outline-outline-green-4" : "border-outline-gray-2 outline-transparent"
)}
>
<div className="flex justify-between items-end gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<Badge size='md'>{_(voucher.doctype)}</Badge>
<a target="_blank"
href={`/desk/${slug(voucher.doctype)}/${voucher.name}`}
className="underline underline-offset-2 text-base"
>{voucher.name}</a>
</div>
{voucher.party && voucher.party_type && <div className="flex items-center gap-1.5 text-base">
<User size='18px' />
<span>{_(voucher.party_type)}</span>
<a target="_blank"
href={`/desk/${slug(voucher.party_type)}/${voucher.party}`}
className="underline underline-offset-2"
>{voucher.party}</a>
</div>}
<TooltipProvider>
<div className="flex items-start gap-8 py-0.5">
<div className="flex flex-col gap-1 min-w-24">
<div className="text-xs text-ink-gray-6">{_("Amount")}</div>
<div className="text-base font-medium flex items-center gap-1">{formatCurrency(voucher.paid_amount, voucher.currency)} {amountMatches ? <MatchBadge matchType="full" label={_("Amount matches the selected transaction")} /> : <MatchBadge matchType="none" label={_("Amount does not match the selected transaction")} />}</div>
</div>
<div className="flex flex-col gap-1 min-w-24">
<div className="text-xs text-ink-gray-6">{_("Posted On")}</div>
<div className="text-base font-medium flex items-center gap-1">{formatDate(voucher.posting_date)} {postingDateMatches ? <MatchBadge matchType="full" label={_("Posting date matches the selected transaction")} /> : <MatchBadge matchType="none" label={_("Posting date does not match the selected transaction")} />}</div>
</div>
{voucher.reference_date && <div className="flex flex-col gap-1 min-w-24">
<div className="text-xs text-ink-gray-6">{_("Reference Date")}</div>
<div className="text-base font-medium flex items-center gap-1">{formatDate(voucher.reference_date)} {referenceDateMatches ? <MatchBadge matchType="full" label={_("Reference date matches the selected transaction")} /> : <MatchBadge matchType="none" label={_("Reference date does not match the selected transaction")} />}</div>
</div>}
</div>
{voucher.reference_no && <div className="flex items-start gap-1">
<span className="text-p-base">
{voucher.reference_no}
&nbsp;&nbsp;
<Tooltip>
<TooltipTrigger>
<Badge theme={referenceMatchesFull ? "green" : referenceMatchesPartial ? "orange" : "red"} variant={referenceMatchesFull || referenceMatchesPartial ? "subtle" : "outline"}>
{referenceMatchesFull ? `${_("Complete Match")}` : referenceMatchesPartial ? `${_("Partial Match")}` : `${_("No Match")}`}</Badge>
</TooltipTrigger>
<TooltipContent side="top">
{referenceMatchesFull ? `${_("Reference matches the selected transaction")}` : referenceMatchesPartial ? `${_("Reference matches the selected transaction partially")}` : `${_("Reference does not match the selected transaction")}`}
</TooltipContent>
</Tooltip>
</span>
</div>}
</TooltipProvider>
</div>
<div>
<Button
variant={isSuggested || amountMatches ? "solid" : "outline"}
theme={isSuggested || amountMatches ? "green" : "gray"}
onClick={onClick} disabled={loading}>{loading ? <><Loader2 className="w-4 h-4 animate-spin" /> {_("Reconciling")}...</> : `${_("Reconcile")}`}</Button>
</div>
</div>
{isSuggested && <div className="absolute top-1.5 end-2 flex items-center gap-1 justify-center">
<Badge theme="green" variant="subtle" size='md'>{_("Suggested")}</Badge>
</div>}
</div>
</div>
}
const MatchBadge = ({ matchType, label }: { matchType: 'full' | 'partial' | 'none', label: string }) => {
return <Tooltip>
<TooltipTrigger>
{matchType === 'full' ? <BadgeCheck className="text-ink-white fill-surface-green-5 size-4" /> : matchType === 'partial' ?
<Badge theme="orange" variant="subtle">{_("Partial Match")}</Badge> :
<XCircle className="text-ink-red-4 size-4" />}
</TooltipTrigger>
<TooltipContent>
{label}
</TooltipContent>
</Tooltip>
}
const OlderUnreconciledTransactionsBanner = () => {
// A banner to show when there are unreconciled transactions for the given bank account before the current selected date
const [dates, setDates] = useAtom(bankRecDateAtom)
const selectedBank = useAtomValue(selectedBankAccountAtom)
const { data } = useFrappeGetCall<{
message: {
count: number,
oldest_date: string
}
}>("erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_older_unreconciled_transactions", {
bank_account: selectedBank?.name,
from_date: dates.fromDate,
}, undefined, {
revalidateOnFocus: false,
})
if (data && data.message.count > 0) {
return <Alert theme='gray' variant='subtle'>
<AlertCircleIcon />
<div className="flex justify-between items-center gap-1.5">
<div>
<AlertTitle> {data.message.count > 1 ? (
<span>{_("There are {0} unreconciled transactions before {1}.", [data.message.count.toString(), formatDate(dates.fromDate)])}</span>
) : (
<span>{_("There is one unreconciled transaction before {0}.", [formatDate(dates.fromDate)])}</span>
)}</AlertTitle>
<AlertDescription className="flex justify-between text-balance">
{_("The opening balance might not match your bank statement. Would you like to reconcile them?")}
</AlertDescription>
</div>
<div>
<Button
size='sm'
type='button'
theme='gray'
variant='outline'
onClick={() => setDates({ fromDate: data.message.oldest_date, toDate: dates.toDate })}>
<span>{data.message.count > 1 ? _("View older transactions") : _("View older transaction")}</span>
<ArrowRightIcon />
</Button>
</div>
</div>
</Alert>
}
return null
}
export default MatchAndReconcile

View File

@@ -1,93 +0,0 @@
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import _ from '@/lib/translate'
import { FilterIcon } from 'lucide-react'
import { bankRecMatchFilters } from './bankRecAtoms'
import { useAtom } from 'jotai'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { useFrappeGetCall } from 'frappe-react-sdk'
import { scrub } from '@/lib/frappe'
import { useMemo } from 'react'
const MatchFilters = () => {
return (
<Popover>
<Tooltip>
<PopoverTrigger asChild>
<TooltipTrigger asChild>
<Button size='md' isIconButton variant='outline' aria-label={_("Configure match filters for vouchers")}>
<FilterIcon />
</Button>
</TooltipTrigger>
</PopoverTrigger>
<TooltipContent>
{_("Configure match filters for vouchers")}
</TooltipContent>
</Tooltip>
<PopoverContent>
<div className="flex flex-col gap-4">
<ToggleSwitch label={_("Show Only Exact Amount")} id="exact_match" />
<Separator />
<MatchFiltersContent />
<ToggleSwitch label={_("Bank Transaction")} id="bank_transaction" />
</div>
</PopoverContent>
</Popover>
)
}
const MatchFiltersContent = () => {
const { data } = useFrappeGetCall<{ message: string[] }>("erpnext.accounts.doctype.bank_transaction.bank_transaction.get_doctypes_for_bank_reconciliation", undefined,
"bank_rec_doctypes", {
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
}
)
const doctypes = useMemo(() => {
const STANDARD_DOCTYPES = ["Payment Entry", "Journal Entry", "Purchase Invoice", "Sales Invoice"]
if (data) {
return data.message.map(doctype => ({
label: doctype,
id: scrub(doctype),
}))
} else {
return STANDARD_DOCTYPES.map(doctype => ({
label: doctype,
id: scrub(doctype),
}))
}
}, [data])
return (
<div className="flex flex-col gap-4">
{doctypes.map((doctype) => (
<ToggleSwitch key={doctype.id} label={doctype.label} id={doctype.id} />
))}
</div>
)
}
const ToggleSwitch = ({ label, id }: { label: string, id: string }) => {
const [matchFilters, setMatchFilters] = useAtom(bankRecMatchFilters)
return <div className="flex items-center space-x-2">
<Switch id={id} checked={matchFilters.includes(id)} onCheckedChange={(checked) => {
if (checked) {
setMatchFilters([...matchFilters, id])
} else {
setMatchFilters(matchFilters.filter(filter => filter !== id))
}
}} />
<Label htmlFor={id}>{label}</Label>
</div>
}
export default MatchFilters

View File

@@ -1,10 +0,0 @@
import { Paragraph } from "@/components/ui/typography"
import { cn } from "@/lib/utils"
import { ReactNode } from "react"
export const MissingFiltersBanner = ({ text, className }: { text: ReactNode, className?: string }) => {
return <div className={cn("min-h-[50vh] flex items-center justify-center", className)}>
<Paragraph>{text}</Paragraph>
</div>
}

View File

@@ -1,89 +0,0 @@
import { Button } from "@/components/ui/button"
import ErrorBanner from "@/components/ui/error-banner"
import { Form } from "@/components/ui/form"
import { useCurrentCompany } from "@/hooks/useCurrentCompany"
import _ from "@/lib/translate"
import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule"
import { useFrappeCreateDoc } from "frappe-react-sdk"
import { toast } from "sonner"
import { RuleForm } from "./RuleForm"
import { useForm } from "react-hook-form"
import { SettingsPanelHeader, SettingsPanelDescription, SettingsPanelTitle, SettingsPanelContent } from "@/components/ui/settings-dialog"
import { useHotkeys } from "react-hotkeys-hook"
type Props = {
onCreate: VoidFunction
}
const CreateNewRule = ({ onCreate }: Props) => {
const currentCompany = useCurrentCompany()
const form = useForm<BankTransactionRule>({
defaultValues: {
rule_name: "",
company: currentCompany,
rule_description: "",
transaction_type: "Any",
classify_as: 'Bank Entry',
bank_entry_type: "Single Account",
description_rules: [{
check: "Contains",
}]
}
})
const { createDoc, loading, error } = useFrappeCreateDoc<BankTransactionRule>()
const onSubmit = (data: BankTransactionRule) => {
createDoc("Bank Transaction Rule", data)
.then(() => {
toast.success(_("Rule created successfully"))
onCreate()
})
}
useHotkeys('meta+s', () => {
form.handleSubmit(onSubmit)()
}, {
enabled: true,
preventDefault: true,
enableOnFormTags: true
})
return (
<>
<SettingsPanelHeader
actions={
<div className="flex items-center gap-2">
<Button variant='outline' size='md' type='button' onClick={() => onCreate()}>{_("Cancel")}</Button>
<Button type='submit' form='rule-form' size='md' disabled={loading}>
{_("Save")}
</Button>
</div>
}
>
<SettingsPanelTitle>
{_("New Rule")}
</SettingsPanelTitle>
<SettingsPanelDescription>
{_("Create a new rule to automatically classify transactions.")}
</SettingsPanelDescription>
</SettingsPanelHeader>
<SettingsPanelContent className="px-0">
<Form {...form}>
<form id='rule-form' onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col justify-between h-full overflow-y-auto px-2">
<div className="flex flex-col gap-4">
{error && <ErrorBanner error={error} />}
<RuleForm />
</div>
</form>
</Form>
</SettingsPanelContent>
</>
)
}
export default CreateNewRule

View File

@@ -1,101 +0,0 @@
import { Button } from "@/components/ui/button"
import ErrorBanner from "@/components/ui/error-banner"
import { Form } from "@/components/ui/form"
import _ from "@/lib/translate"
import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule"
import { FrappeError, useFrappeGetDoc, useFrappeUpdateDoc } from "frappe-react-sdk"
import { toast } from "sonner"
import { RuleForm } from "./RuleForm"
import { useForm } from "react-hook-form"
import { Skeleton } from "@/components/ui/skeleton"
import { SettingsPanelContent, SettingsPanelDescription, SettingsPanelHeader, SettingsPanelTitle } from "@/components/ui/settings-dialog"
import { useHotkeys } from "react-hotkeys-hook"
type Props = {
onClose: VoidFunction,
ruleID: string
}
const EditRule = ({ onClose, ruleID }: Props) => {
const { data: rule, isValidating, error, mutate } = useFrappeGetDoc<BankTransactionRule>("Bank Transaction Rule", ruleID, undefined, {
revalidateOnMount: true
})
const { updateDoc, loading, error: updateError } = useFrappeUpdateDoc<BankTransactionRule>()
const onSubmit = (data: BankTransactionRule) => {
updateDoc("Bank Transaction Rule", ruleID, data)
.then(() => {
toast.success(_("Rule updated."))
mutate()
onClose()
})
}
return <>
<SettingsPanelHeader
actions={
<div className="flex items-center gap-2">
<Button variant='outline' size='md' type='button' onClick={() => onClose()}>{_("Cancel")}</Button>
<Button type='submit' form='rule-form' size='md' disabled={isValidating || loading}>
{_("Save")}
</Button>
</div>
}
>
<SettingsPanelTitle>
{rule?.rule_name}
</SettingsPanelTitle>
<SettingsPanelDescription className="sr-only">
{_("Edit this rule")}
</SettingsPanelDescription>
</SettingsPanelHeader>
<SettingsPanelContent className="px-0">
{isValidating && <div className="px-4 flex flex-col gap-4 h-full">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>}
{error && <div className="px-4 flex flex-col gap-4 h-full">
<ErrorBanner error={error} />
</div>}
{rule && <EditRuleForm rule={rule} onSubmit={onSubmit} error={updateError} />}
</SettingsPanelContent>
</>
}
const EditRuleForm = ({ rule, onSubmit, error }: { rule: BankTransactionRule, onSubmit: (data: BankTransactionRule) => void, error?: FrappeError | null }) => {
const form = useForm<BankTransactionRule>({
defaultValues: {
...rule,
}
})
useHotkeys('meta+s', () => {
form.handleSubmit(onSubmit)()
}, {
enabled: true,
preventDefault: true,
enableOnFormTags: true
})
return (
<Form {...form}>
<form id='rule-form' onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col justify-between h-full overflow-y-auto px-2">
<div className="flex flex-col gap-4">
{error && <ErrorBanner error={error} />}
<RuleForm isEdit />
</div>
</form>
</Form>
)
}
export default EditRule

View File

@@ -1,799 +0,0 @@
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Dialog, DialogTitle, DialogContent, DialogHeader, DialogDescription } from "@/components/ui/dialog"
import { FormField, FormItem, FormLabel, FormControl } from "@/components/ui/form"
import { AccountFormField, CurrencyFormField, DataField, LinkFormField, PartyTypeFormField, SelectFormField, SmallTextField } from "@/components/ui/form-elements"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { SelectItem } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { H4, Paragraph } from "@/components/ui/typography"
import { today } from "@/lib/date"
import _ from "@/lib/translate"
import { cn } from "@/lib/utils"
import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule"
import { BankTransactionRuleAccounts } from "@/types/Accounts/BankTransactionRuleAccounts"
import { FrappeConfig, FrappeContext } from "frappe-react-sdk"
import { ArrowDownRight, ArrowDownUp, ArrowRightLeftIcon, ArrowUpRight, LandmarkIcon, Plus, PlusCircleIcon, ReceiptIcon, Settings, Trash2 } from "lucide-react"
import { ChangeEvent, useCallback, useContext, useMemo, useRef, useState } from "react"
import { useFieldArray, useFormContext, useWatch } from "react-hook-form"
export const RuleForm = ({ isEdit = false }: { isEdit?: boolean }) => {
return <div className="flex flex-col gap-4">
<DataField
name='rule_name'
label={_("Rule Name")}
disabled={isEdit}
isRequired
inputProps={{
maxLength: 140,
disabled: isEdit,
placeholder: _("Bank Charges, Salary, etc."),
autoFocus: true,
className: "dark:disabled:bg-surface-gray-2"
}}
rules={{
required: _("Rule name is required")
}}
/>
<CompanySelector />
<SmallTextField
name='rule_description'
label={_("Rule Description")}
inputProps={{
placeholder: _("Any debit transaction with the keyword 'Bank Fee'.")
}}
/>
<TransactionTypeSelector />
<div className="grid grid-cols-2 gap-2 pt-1">
<CurrencyFormField
name='min_amount'
label={_("Minimum Amount")}
/>
<CurrencyFormField
name='max_amount'
label={_("Maximum Amount")}
/>
</div>
<DescriptionRules />
<Separator />
<RuleAction />
</div>
}
const CompanySelector = () => {
const { setValue } = useFormContext<BankTransactionRule>()
return <LinkFormField
name='company'
label={_("Company")}
doctype="Company"
isRequired
rules={{
required: _("Company is required"),
onChange: () => {
setValue('account', '')
}
}}
/>
}
/** Component to render a radio group as a toggle group with options for All, Withdrawal, Deposit */
const TransactionTypeSelector = () => {
const { control } = useFormContext<BankTransactionRule>()
return (
<FormField
control={control}
name='transaction_type'
render={({ field }) => (
<FormItem className="space-y-1">
<FormLabel className="text-sm font-medium">
{_("Transaction Type")}<span className="text-ink-red-3">*</span>
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
value={field.value}
className="grid grid-cols-3 gap-2 w-full"
>
<FormItem className="flex items-center">
<FormControl>
<RadioGroupItem
value="Any"
className="peer sr-only hidden"
/>
</FormControl>
<FormLabel
className={cn(
"w-full flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md border cursor-pointer transition-all hover:bg-surface-gray-1 hover:text-ink-gray-8",
"peer-data-[state=checked]:bg-surface-gray-7 peer-data-[state=checked]:text-ink-white peer-data-[state=checked]:border-outline-gray-5 peer-data-[state=checked]:hover:bg-surface-gray-7 peer-data-[state=checked]:hover:text-ink-white"
)}
>
<ArrowDownUp className="w-5 h-5" />
{_("All")}
</FormLabel>
</FormItem>
<FormItem className="flex items-center">
<FormControl>
<RadioGroupItem
value="Withdrawal"
className="peer sr-only hidden"
/>
</FormControl>
<FormLabel
className={cn(
"w-full flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md border cursor-pointer transition-all hover:bg-surface-gray-1 hover:text-ink-gray-8",
"peer-data-[state=checked]:bg-surface-red-5 peer-data-[state=checked]:text-white peer-data-[state=checked]:border-bg-surface-red-5 peer-data-[state=checked]:hover:bg-surface-red-5 peer-data-[state=checked]:hover:text-white"
)}
>
<ArrowUpRight className="w-5 h-5 peer-data-[state=checked]:text-ink-red-3" />
{_("Withdrawal")}
</FormLabel>
</FormItem>
<FormItem className="flex items-center">
<FormControl>
<RadioGroupItem
value="Deposit"
className="peer sr-only hidden"
/>
</FormControl>
<FormLabel
className={cn(
"w-full flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium rounded-md border cursor-pointer transition-all hover:bg-surface-gray-1 hover:text-ink-gray-8",
"peer-data-[state=checked]:bg-surface-green-5 peer-data-[state=checked]:text-white peer-data-[state=checked]:border-surface-green-5 peer-data-[state=checked]:hover:bg-surface-green-5 peer-data-[state=checked]:hover:text-white"
)}
>
<ArrowDownRight className="w-5 h-5 peer-data-[state=checked]:text-white" />
{_("Deposit")}
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
</FormItem>
)}
/>
)
}
const DescriptionRules = () => {
const { control } = useFormContext<BankTransactionRule>()
const { fields, append, remove } = useFieldArray({
control,
name: "description_rules"
})
const addRow = () => {
// @ts-expect-error - we don't need all fields here
append({ check: "Contains" })
}
return (
<div className="flex flex-col gap-2 pt-1">
<span className="text-sm font-medium">{_("Rules to match against the transaction description")} <span className="text-ink-red-3">*</span></span>
{fields.map((field, index) => (
<div key={field.id} className="flex w-full items-center gap-2">
<div className="min-w-36">
<SelectFormField
label={_("Type of check")}
hideLabel
name={`description_rules.${index}.check`}
rules={{
required: _("This is required")
}}>
<SelectItem value="Contains">{_("Contains")}</SelectItem>
<SelectItem value="Starts With">{_("Starts with")}</SelectItem>
<SelectItem value="Ends With">{_("Ends with")}</SelectItem>
<SelectItem value="Regex">{_("Regex")}</SelectItem>
</SelectFormField>
</div>
<div className="w-full">
<DataField
name={`description_rules.${index}.value`}
label={_("Value")}
hideLabel
inputProps={{
placeholder: _("Bank Fee, Salary, etc."),
}}
/>
</div>
<div>
<Button variant="ghost" theme='red' type='button' isIconButton onClick={() => remove(index)} disabled={fields.length === 1}>
<Trash2 />
</Button>
</div>
</div>
))}
<div>
<Button variant="outline" type='button' onClick={addRow}>
<PlusCircleIcon />
{_("Add Rule")}
</Button>
</div>
</div>
)
}
const RuleAction = () => {
const { control } = useFormContext<BankTransactionRule>()
const classify_as = useWatch({ control, name: "classify_as" })
const party_type = useWatch({ control, name: "party_type" })
const bank_entry_type = useWatch({ control, name: "bank_entry_type" })
const accountType = useMemo(() => {
if (classify_as === "Payment Entry") {
return party_type === "Supplier" ? ["Payable"] : ["Receivable"]
}
if (classify_as === "Transfer") {
return ["Bank", "Cash", "Temporary"]
}
return undefined
}, [classify_as, party_type])
return (
<div className="flex flex-col gap-4">
<H4 className="text-base text-ink-gray-7">{_("If rule matches, then:")}</H4>
<SelectFormField
name='classify_as'
isRequired
label={_("Suggest creating a")}
formDescription={_("This will just suggest creating a new entry, and will not automatically create it.")}
rules={{
required: _("This is required")
}}
>
<SelectItem value="Bank Entry"><LandmarkIcon /> {_("Bank Entry")}</SelectItem>
<SelectItem value="Payment Entry"><ReceiptIcon /> {_("Payment Entry")}</SelectItem>
<SelectItem value="Transfer"><ArrowRightLeftIcon /> {_("Transfer")}</SelectItem>
</SelectFormField>
{classify_as === "Bank Entry" && (<SelectFormField
name='bank_entry_type'
isRequired
label={_("Create Bank Entry against")}
rules={{
required: _("This is required")
}}
>
<SelectItem value="Single Account">{_("Single Account")}</SelectItem>
<SelectItem value="Multiple Accounts">{_("Multiple Accounts (Journal Template)")}</SelectItem>
</SelectFormField>)}
{classify_as === "Payment Entry" && (
<div className='grid grid-cols-4 gap-4'>
<div className="col-span-1">
<PartyTypeFormField
name='party_type'
label={_("Party Type")}
isRequired
inputProps={{
triggerProps: {
className: 'w-full'
},
}}
rules={{
required: "Party Type is required"
}}
/>
</div>
<div className="col-span-3">
<PartyField />
</div>
</div>
)}
{(((bank_entry_type === "Single Account" || !bank_entry_type) && classify_as === "Bank Entry") || classify_as !== "Bank Entry") && (<AccountFormField
name='account'
label={_("Account")}
isRequired
rules={{
required: _("Account is required")
}}
account_type={accountType}
/>)}
{bank_entry_type === "Multiple Accounts" && classify_as === "Bank Entry" && <MultipleAccountsSelection />}
</div>
)
}
const PartyField = () => {
const { control, setValue } = useFormContext<BankTransactionRule>()
const party_type = useWatch({
control,
name: `party_type`
})
const { call } = useContext(FrappeContext) as FrappeConfig
const company = useWatch({ control, name: 'company' })
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
// Fetch the party and account
if (event.target.value) {
call.get('erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details', {
company: company,
party_type: party_type,
party: event.target.value,
date: today()
}).then((res) => {
setValue('account', res.message.party_account)
})
} else {
// Clear the account
setValue('account', '')
}
}
if (!party_type) {
return <DataField
name={`party`}
label={_("Party")}
isRequired
inputProps={{
disabled: true,
}}
/>
}
return <LinkFormField
name={`party`}
label={_("Party")}
rules={{
onChange
}}
doctype={party_type}
/>
}
const MultipleAccountsSelection = () => {
const { control } = useFormContext<BankTransactionRule>()
const accounts = useWatch({
control,
name: 'accounts'
}) ?? []
const [isConfigureAccountsModalOpen, setIsConfigureAccountsModalOpen] = useState(false)
return <div className="flex flex-col gap-2">
<div className="flex justify-between gap-2">
<Label>{_("Journal Template Accounts")}<span className="text-ink-red-3">*</span></Label>
<Button variant="outline" type="button" onClick={() => setIsConfigureAccountsModalOpen(true)}><Settings /> {_("Configure Accounts")}</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>{_("Account")}</TableHead>
<TableHead className="text-end">{_("Debit")}</TableHead>
<TableHead className="text-end">{_("Credit")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{accounts.length === 0 && (
<TableRow>
<TableCell colSpan={3} className="text-center">
<div className="py-2 flex flex-col gap-2 items-center">
<span>{_("No accounts configured")}</span>
<Button variant="subtle" type="button" onClick={() => setIsConfigureAccountsModalOpen(true)}>{_("Configure Accounts")}</Button>
</div>
</TableCell>
</TableRow>
)}
{accounts.map((account, index) => (
<TableRow key={index}>
<TableCell>{account.account}</TableCell>
{index === accounts.length - 1 ? <TableCell className="text-end bg-surface-gray-1" colSpan={2}>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-ink-gray-5">{_("This is auto computed to balance the journal entry.")}</span>
</TooltipTrigger>
<TooltipContent>
{_("Based on the above entries, the balance amount (debit or credit) will be set for the last row to balance the journal entry.")}
</TooltipContent>
</Tooltip>
</TableCell> : <>
<TableCell className="font-numeric text-end"><AmountFormulaRenderer value={account.debit} /></TableCell>
<TableCell className="font-numeric text-end"><AmountFormulaRenderer value={account.credit} /></TableCell>
</>}
</TableRow>
))}
</TableBody>
</Table>
<ConfigureAccountsModal open={isConfigureAccountsModalOpen} onClose={() => setIsConfigureAccountsModalOpen(false)} />
</div>
}
const AmountFormulaRenderer = ({ value }: { value?: string }) => {
// If it's a string and cannot be a number, then show it as a formula
if (isNaN(Number(value))) {
let calculatedValue = "";
try {
calculatedValue = window.eval(`const transaction_amount = 200; ${value}`);
} catch (error: unknown) {
console.error(error);
calculatedValue = "Error";
}
const isComputationValid = !isNaN(Number(calculatedValue)) && calculatedValue !== undefined && calculatedValue !== null;
return <Tooltip>
<TooltipTrigger asChild>
<span className={cn("font-numeric text-end tabular-nums underline underline-offset-4", isComputationValid ? "" : "text-ink-red-3")}>{value}</span>
</TooltipTrigger>
<TooltipContent className={isComputationValid ? "" : "bg-surface-red-5"} arrowClassName={isComputationValid ? "" : "bg-surface-red-5 fill-surface-red-5"}>
<p className="text-sm">
{isComputationValid ? _("This is a formula based value.") : _("This is not a valid formula. Check the variable used in the formula.")}
<br /><br />
{_("Example: If the transaction amount is 200, then this will be calculated as {} = {}", [value ?? "", calculatedValue])}
</p>
</TooltipContent>
</Tooltip>
}
return <span className="font-numeric text-end tabular-nums">{value}</span>
}
const ConfigureAccountsModal = ({ open, onClose }: { open: boolean, onClose: () => void }) => {
return <Dialog
open={open}
onOpenChange={onClose}
>
<DialogContent className='min-w-[95vw]'>
<ConfigureAccountsModalContent />
</DialogContent>
</Dialog>
}
const ConfigureAccountsModalContent = () => {
const { control, getValues, setValue } = useFormContext<BankTransactionRule>()
const { call } = useContext(FrappeContext) as FrappeConfig
// const costCenterMapRef = useRef<Record<string, string>>({})
const partyMapRef = useRef<Record<string, string>>({})
const onPartyChange = (value: string, index: number) => {
// Get the account for the party type
if (value) {
if (partyMapRef.current[value]) {
setValue(`accounts.${index}.account`, partyMapRef.current[value])
} else {
call.get('erpnext.accounts.party.get_party_account', {
party: value,
party_type: getValues(`accounts.${index}.party_type`),
company: company
}).then((result: { message: string }) => {
setValue(`accounts.${index}.account`, result.message)
partyMapRef.current[value] = result.message
})
}
} else {
setValue(`accounts.${index}.account`, '')
}
}
const transaction_type = useWatch({
name: 'transaction_type',
control,
})
const { fields, append, remove } = useFieldArray({
control,
name: 'accounts'
})
const [selectedRows, setSelectedRows] = useState<number[]>([])
const onSelectRow = useCallback((index: number) => {
setSelectedRows(prev => {
if (prev.includes(index)) {
return prev.filter(i => i !== index)
}
return [...prev, index]
})
}, [])
const onSelectAll = useCallback(() => {
setSelectedRows(prev => {
if (prev.length === fields.length) {
return []
}
return [...fields.map((_, index) => index)]
})
}, [fields])
const onAdd = () => {
append({
party_type: '',
party: '',
account: '',
debit: '',
credit: '',
user_remark: ''
} as BankTransactionRuleAccounts, {
focusName: `accounts.${fields.length}.account`
})
}
const onRemove = useCallback(() => {
remove(selectedRows)
setSelectedRows([])
}, [remove, selectedRows])
const isWithdrawal = transaction_type === 'Withdrawal'
const company = useWatch({
name: 'company',
control,
})
return <>
<DialogHeader>
<DialogTitle>{_("Configure Accounts for Bank Entry")}</DialogTitle>
<DialogDescription>{_("Add all accounts that you want to split the transaction into.")}</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
<Table>
<TableHeader>
<TableRow>
<TableHead><Checkbox
disabled={fields.length === 0}
// Make this accessible to screen readers
aria-label={_("Select all")}
checked={selectedRows.length > 0 && selectedRows.length === fields.length}
onCheckedChange={onSelectAll} /></TableHead>
<TableHead>{_("Party")}</TableHead>
<TableHead>{_("Account")} <span className="text-ink-red-3">*</span></TableHead>
{/* <TableHead>{_("Cost Center")}</TableHead> */}
<TableHead>{_("Remarks")}</TableHead>
<TableHead className="text-end">{_("Debit")}</TableHead>
<TableHead className="text-end">{_("Credit")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow className="bg-surface-gray-1 cursor-not-allowed" title={_("This is the row for the bank account. It will be auto populated based on the bank transaction.")}>
<TableCell>
<Checkbox disabled />
</TableCell>
<TableCell className="align-top">
</TableCell>
<TableCell className="align-top text-ink-gray-5">
<span className="px-2">
Bank GL Account
</span>
</TableCell>
<TableCell className="align-top">
</TableCell>
<TableCell className={"align-top text-end"}>
<span className="text-ink-gray-5 text-sm">
{transaction_type === "Withdrawal" || transaction_type === "Any" ? _("Will be auto-populated") : ""}
</span>
</TableCell>
<TableCell className={"text-end align-top"}>
<span className="text-ink-gray-5 text-sm">
{transaction_type === "Deposit" || transaction_type === "Any" ? _("Will be auto-populated") : ""}
</span>
</TableCell>
</TableRow>
{fields.map((field, index) => (
<TableRow key={field.id}>
<TableCell>
<Checkbox
checked={selectedRows.includes(index)}
onCheckedChange={() => onSelectRow(index)}
// Make this accessible to screen readers
aria-label={_("Select row {0}", [String(index + 1)])}
/>
</TableCell>
<TableCell className="align-top">
<div className="flex">
<PartyTypeFormField
name={`accounts.${index}.party_type`}
label={_("Party Type")}
isRequired
hideLabel
inputProps={{
type: isWithdrawal ? 'Payable' : 'Receivable',
triggerProps: {
className: 'rounded-e-none',
tabIndex: -1
},
}} />
<PartyRowField index={index} onChange={onPartyChange} />
</div>
</TableCell>
<TableCell className="align-top">
<AccountFormField
name={`accounts.${index}.account`}
label={_("Account")}
rules={{
required: _("Account is required"),
// onChange: (event) => {
// onAccountChange(event.target.value, index)
// }
}}
buttonClassName="min-w-64"
isRequired
hideLabel
/>
</TableCell>
{/* <TableCell className="align-top">
<LinkFormField
doctype="Cost Center"
name={`accounts.${index}.cost_center`}
label={_("Cost Center")}
filters={[["company", "=", company], ["is_group", "=", 0], ["disabled", "=", 0]]}
buttonClassName="min-w-48"
readOnly={index === 0}
hideLabel
/>
</TableCell> */}
<TableCell className="align-top">
<DataField
name={`accounts.${index}.user_remark`}
label={_("Remarks")}
inputProps={{
placeholder: _("e.g. Bank Charges"),
className: 'min-w-64',
}}
hideLabel
/>
</TableCell>
<TableCell
className={cn("text-end align-top", index === fields.length - 1 ? "cursor-not-allowed" : "")}
title={index === fields.length - 1 ? _("This is the last row. It will be auto populated based on the bank transaction.") : ""}>
<DataField
name={`accounts.${index}.debit`}
label={_("Debit")}
disabled={index === fields.length - 1}
inputProps={{
className: 'text-end',
placeholder: _("0.00"),
disabled: index === fields.length - 1
}}
hideLabel
/>
</TableCell>
<TableCell
className={cn("text-end align-top", index === fields.length - 1 ? "cursor-not-allowed" : "")}
title={index === fields.length - 1 ? _("This is the last row. It will be auto populated based on the bank transaction.") : ""}>
<DataField
name={`accounts.${index}.credit`}
label={_("Credit")}
disabled={index === fields.length - 1}
inputProps={{
className: 'text-end',
placeholder: _("0.00"),
disabled: index === fields.length - 1
}}
hideLabel
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="flex justify-between gap-2">
<div className="flex gap-2 justify-end">
<div>
<Button size='sm' type='button' variant={'outline'} onClick={onAdd}><Plus /> {_("Add Row")}</Button>
</div>
{selectedRows.length > 0 && <div>
<Button size='sm' type='button' theme="red" onClick={onRemove}><Trash2 /> {_("Remove")}</Button>
</div>}
</div>
</div>
<div className="py-4">
<Separator />
</div>
<div className="flex flex-col gap-2">
<H4 className="text-base text-ink-gray-7">{_("Help")}</H4>
<Paragraph className="text-p-sm">{(_("You can set up the rule to split the transaction across multiple accounts."))}
<br />{_("You can also add credit or debit values to pre-fill - these support both static values (like 200) or formulas (like transaction_amount * 0.25).")}
<br />
<br />
<span className="font-medium">{_("Example")}:</span>
<br />
<span className="font-numeric text-sm">
transaction_amount * 0.25
</span>
<br />
<span>
{_("In this case, the amount will be calculated as 25% of the transaction amount. If the transaction amount is 200, then this will be calculated as 200 * 0.25 = 50.")}
</span>
</Paragraph>
</div>
</div>
</>
}
const PartyRowField = ({ index, onChange }: { index: number, onChange: (value: string, index: number) => void }) => {
const { control } = useFormContext<BankTransactionRule>()
const party_type = useWatch({
control,
name: `accounts.${index}.party_type`
})
if (!party_type) {
return <DataField
name={`accounts.${index}.party`}
label={_("Party")}
isRequired
inputProps={{
disabled: true,
className: 'rounded-s-none border-s-0 min-w-64'
}}
hideLabel
/>
}
return <LinkFormField
name={`accounts.${index}.party`}
label={_("Party")}
rules={{
onChange: (event) => {
onChange(event.target.value, index)
},
}}
hideLabel
buttonClassName="rounded-s-none border-s-0 min-w-64"
doctype={party_type}
/>
}

View File

@@ -1,73 +0,0 @@
import { useMemo } from 'react'
import { ArrowDownRight, ArrowUpRight, Calendar } from 'lucide-react'
import { formatCurrency } from '@/lib/numbers'
import { formatDate } from '@/lib/date'
import { UnreconciledTransaction, useGetBankAccounts } from './utils'
import { getCompanyCurrency } from '@/lib/company'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import _ from '@/lib/translate'
import BankLogo from '@/components/common/BankLogo'
type Props = {
transaction: UnreconciledTransaction,
showAccount?: boolean,
account?: string
}
const SelectedTransactionDetails = ({ transaction, showAccount = false, account }: Props) => {
const isWithdrawal = transaction.withdrawal && transaction.withdrawal > 0
const { banks } = useGetBankAccounts()
const bank = useMemo(() => {
if (transaction.bank_account) {
return banks?.find((bank) => bank.name === transaction.bank_account)
}
return null
}, [transaction.bank_account, banks])
const amount = transaction.withdrawal ? transaction.withdrawal : transaction.deposit
const currency = transaction.currency || getCompanyCurrency(transaction.company ?? '')
return (
<Card className='py-4'>
<CardContent className='px-4'>
<div className='flex flex-col gap-2'>
<div className='flex justify-between'>
<div className='flex flex-col gap-2'>
<div className='flex flex-col gap-1'>
<BankLogo bank={bank} iconSize='30px' imageClassName='h-10 max-w-20' />
<span className='font-medium text-sm'>{transaction.bank_account}</span>
</div>
<div className='flex items-center gap-1'>
<Calendar size='16px' />
<span className='text-sm'>{formatDate(transaction.date, 'Do MMM YYYY')}</span>
</div>
</div>
<div className='flex flex-col gap-1'>
<div className={cn('flex items-center gap-1 text-end px-0 justify-end py-1 rounded-sm',
isWithdrawal ? 'text-ink-red-3' : 'text-ink-green-3'
)}>
{isWithdrawal ? <ArrowUpRight className="w-5 h-5 text-ink-red-3" /> : <ArrowDownRight className="w-5 h-5 text-ink-green-3" />}
<span className='text-sm font-semibold uppercase'>{isWithdrawal ? _('Spent') : _('Received')}</span>
</div>
<span className='font-semibold font-numeric text-lg text-end pe-0.5'>{formatCurrency(amount, currency)}</span>
{transaction.unallocated_amount && transaction.unallocated_amount !== amount ? <span className='text-ink-gray-5'>{_("Unallocated")}: {formatCurrency(transaction.unallocated_amount)}</span> : null}
</div>
</div>
<div className='flex flex-col gap-1'>
<span className='text-sm'>{transaction.description}</span>
{transaction.reference_number ? <span className='text-sm text-ink-gray-5'>{_("Ref")}: {transaction.reference_number}</span> : null}
{showAccount && account ? <span className='text-sm text-ink-gray-5'>{_("GL Account")}: {account}</span> : null}
</div>
</div>
</CardContent >
</Card >
)
}
export default SelectedTransactionDetails

View File

@@ -1,47 +0,0 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import _ from '@/lib/translate'
import { useAtomValue } from 'jotai'
import { bankRecSelectedTransactionAtom, selectedBankAccountAtom } from './bankRecAtoms'
import { formatDate } from '@/lib/date'
import { formatCurrency } from '@/lib/numbers'
import { ArrowDownRight, ArrowUpRight } from 'lucide-react'
const SelectedTransactionsTable = () => {
const selectedBankAccount = useAtomValue(selectedBankAccountAtom)
const transactions = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? ''))
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>
{_("Date")}
</TableHead>
<TableHead>
{_("Description")}
</TableHead>
<TableHead className="text-end">
{_("Amount")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((transaction) => (
<TableRow key={transaction.name}>
<TableCell>{formatDate(transaction.date)}</TableCell>
<TableCell className="max-w-96 text-ellipsis overflow-hidden" title={transaction.description}>{transaction.description}</TableCell>
<TableCell className="text-end flex items-center justify-end gap-1">
{transaction.withdrawal && transaction.withdrawal > 0 ? <ArrowUpRight className="w-4 h-4 text-ink-red-3" /> : <ArrowDownRight className="w-4 h-4 text-ink-green-3" />}
<span className="font-numeric font-medium">
{formatCurrency(transaction.unallocated_amount, transaction.currency ?? '')}
</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}
export default SelectedTransactionsTable

View File

@@ -1,555 +0,0 @@
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { bankRecSelectedTransactionAtom, bankRecTransferModalAtom, bankRecUnreconcileModalAtom, SelectedBank, selectedBankAccountAtom } from './bankRecAtoms'
import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogClose, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import _ from '@/lib/translate'
import { UnreconciledTransaction, useGetBankAccounts, useGetRuleForTransaction, useRefreshUnreconciledTransactions, useUpdateActionLog } from './utils'
import { Button } from '@/components/ui/button'
import SelectedTransactionDetails from './SelectedTransactionDetails'
import { PaymentEntry } from '@/types/Accounts/PaymentEntry'
import { useForm, useFormContext, useWatch } from 'react-hook-form'
import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappePostCall } from 'frappe-react-sdk'
import { toast } from 'sonner'
import ErrorBanner from '@/components/ui/error-banner'
import { H4 } from '@/components/ui/typography'
import { cn } from '@/lib/utils'
import { ArrowRight, Banknote, BadgeCheck, Calendar, ArrowUpRight, ArrowDownRight, CheckIcon, CheckCircle, ArrowLeft } from 'lucide-react'
import { Separator } from '@/components/ui/separator'
import { Form } from '@/components/ui/form'
import { AccountFormField, DataField, DateField, SmallTextField } from '@/components/ui/form-elements'
import SelectedTransactionsTable from './SelectedTransactionsTable'
import { useCurrentCompany } from '@/hooks/useCurrentCompany'
import { formatDate } from '@/lib/date'
import { useContext, useMemo, useState } from 'react'
import { formatCurrency } from '@/lib/numbers'
import { Label } from '@/components/ui/label'
import { FileDropzone } from '@/components/ui/file-dropzone'
import FileUploadBanner from '@/components/common/FileUploadBanner'
import { BankTransaction } from '@/types/Accounts/BankTransaction'
import { useHotkeys } from 'react-hotkeys-hook'
import { useDirection } from '@/components/ui/direction'
import BankLogo from '@/components/common/BankLogo'
const TransferModal = () => {
const [isOpen, setIsOpen] = useAtom(bankRecTransferModalAtom)
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className='min-w-7xl'>
<DialogHeader>
<DialogTitle>{_("Transfer")}</DialogTitle>
<DialogDescription>
{_("Record an internal transfer to another bank/credit card/cash account.")}
</DialogDescription>
</DialogHeader>
<TransferModalContent />
</DialogContent>
</Dialog>
)
}
const TransferModalContent = () => {
const selectedBankAccount = useAtomValue(selectedBankAccountAtom)
const selectedTransaction = useAtomValue(bankRecSelectedTransactionAtom(selectedBankAccount?.name ?? ''))
if (!selectedTransaction || !selectedBankAccount || selectedTransaction.length === 0) {
return <div className='p-4'>
<span className='text-center'>{_("No transaction selected")}</span>
</div>
}
if (selectedTransaction.length === 1) {
return <InternalTransferForm
selectedBankAccount={selectedBankAccount}
selectedTransaction={selectedTransaction[0]} />
}
return <BulkInternalTransferForm transactions={selectedTransaction} />
}
const BulkInternalTransferForm = ({ transactions }: { transactions: UnreconciledTransaction[] }) => {
const form = useForm<{
bank_account: string
}>()
const setIsOpen = useSetAtom(bankRecTransferModalAtom)
const { call: createPaymentEntry, loading, error } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry }[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_bulk_internal_transfer')
const onReconcile = useRefreshUnreconciledTransactions()
const addToActionLog = useUpdateActionLog()
const onSubmit = (data: { bank_account: string }) => {
createPaymentEntry({
bank_transaction_names: transactions.map((transaction) => transaction.name),
bank_account: data.bank_account
}).then(({ message }) => {
addToActionLog({
type: 'transfer',
timestamp: (new Date()).getTime(),
isBulk: true,
items: message.map((item) => ({
bankTransaction: item.transaction,
voucher: {
reference_doctype: "Payment Entry",
reference_name: item.payment_entry.name,
posting_date: item.payment_entry.posting_date,
doc: item.payment_entry,
}
})),
bulkCommonData: {
bank_account: data.bank_account,
}
})
toast.success(_("Transfer Recorded"), {
duration: 4000,
closeButton: true,
})
onReconcile(transactions[transactions.length - 1])
setIsOpen(false)
})
}
const onAccountChange = (account: string) => {
form.setValue('bank_account', account)
}
const selectedAccount = useWatch({ control: form.control, name: 'bank_account' })
const currentCompany = useCurrentCompany()
const company = transactions && transactions.length > 0 ? transactions[0].company : (currentCompany ?? '')
console.log("This is here", transactions)
return <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='flex flex-col gap-4'>
{error && <ErrorBanner error={error} />}
<SelectedTransactionsTable />
<BankOrCashPicker company={company} bankAccount={transactions[0]?.bank_account ?? ''} onAccountChange={onAccountChange} selectedAccount={selectedAccount} />
<DialogFooter>
<DialogClose asChild>
<Button size='md' variant={'outline'} disabled={loading}>{_("Cancel")}</Button>
</DialogClose>
<Button size='md' type='submit' disabled={loading}>{_("Transfer")}</Button>
</DialogFooter>
</div>
</form>
</Form>
}
interface InternalTransferFormFields extends PaymentEntry {
mirror_transaction_name?: string
}
const InternalTransferForm = ({ selectedBankAccount, selectedTransaction }: { selectedBankAccount: SelectedBank, selectedTransaction: UnreconciledTransaction }) => {
const setIsOpen = useSetAtom(bankRecTransferModalAtom)
const onClose = () => {
setIsOpen(false)
}
const { data: rule } = useGetRuleForTransaction(selectedTransaction)
const isWithdrawal = (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) ? true : false
const form = useForm<InternalTransferFormFields>({
defaultValues: {
payment_type: 'Internal Transfer',
company: selectedTransaction?.company,
// If the transaction is a withdrawal, set the paid from to the selected bank account
paid_from: isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''),
// If the transaction is a deposit, set the paid to to the selected bank account
paid_to: !isWithdrawal ? selectedBankAccount.account : (rule?.account ?? ''),
// Set the amount to the amount of the selected transaction
paid_amount: selectedTransaction.unallocated_amount,
received_amount: selectedTransaction.unallocated_amount,
reference_date: selectedTransaction.date,
posting_date: selectedTransaction.date,
reference_no: (selectedTransaction.reference_number || selectedTransaction.description || '').slice(0, 140),
}
})
const onReconcile = useRefreshUnreconciledTransactions()
const { call: createPaymentEntry, loading, error, isCompleted } = useFrappePostCall<{ message: { transaction: BankTransaction, payment_entry: PaymentEntry } }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_internal_transfer')
const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom)
const addToActionLog = useUpdateActionLog()
const { file: frappeFile } = useContext(FrappeContext) as FrappeConfig
const [isUploading, setIsUploading] = useState(false)
const [uploadProgress, setUploadProgress] = useState(0)
const [files, setFiles] = useState<File[]>([])
const onSubmit = (data: InternalTransferFormFields) => {
createPaymentEntry({
bank_transaction_name: selectedTransaction.name,
...data,
custom_remarks: data.remarks ? true : false,
// Pass this to reconcile both at the same time
mirror_transaction_name: data.mirror_transaction_name
}).then(async ({ message }) => {
addToActionLog({
type: 'transfer',
timestamp: (new Date()).getTime(),
isBulk: false,
items: [
{
bankTransaction: message.transaction,
voucher: {
reference_doctype: "Payment Entry",
reference_name: message.payment_entry.name,
reference_no: message.payment_entry.reference_no,
reference_date: message.payment_entry.reference_date,
posting_date: message.payment_entry.posting_date,
doc: message.payment_entry,
}
}
]
})
toast.success(_("Transfer Recorded"), {
duration: 4000,
closeButton: true,
action: {
label: _("Undo"),
onClick: () => setBankRecUnreconcileModalAtom(selectedTransaction.name)
},
actionButtonStyle: {
backgroundColor: "rgb(0, 138, 46)"
}
})
if (files.length > 0) {
setIsUploading(true)
const uploadPromises = files.map(f => {
return frappeFile.uploadFile(f, {
isPrivate: true,
doctype: "Payment Entry",
docname: message.payment_entry.name,
}, (_bytesUploaded, _totalBytes, progress) => {
setUploadProgress((currentProgress) => {
//If there are multiple files, we need to add the progress to the current progress
return currentProgress + ((progress?.progress ?? 0) / files.length)
})
})
})
return Promise.all(uploadPromises).then(() => {
setUploadProgress(0)
setIsUploading(false)
})
} else {
return Promise.resolve()
}
}).then(() => {
setUploadProgress(0)
setIsUploading(false)
onReconcile(selectedTransaction)
onClose()
})
}
useHotkeys('meta+s', () => {
form.handleSubmit(onSubmit)()
}, {
enabled: true,
preventDefault: true,
enableOnFormTags: true
})
const onAccountChange = (account: string, is_mirror: boolean = false) => {
//If the transaction is a withdrawal, set the paid to to the selected account - since this is the account where the money is deposited into
if (selectedTransaction.withdrawal && selectedTransaction.withdrawal > 0) {
form.setValue('paid_to', account)
} else {
form.setValue('paid_from', account)
}
if (!is_mirror) {
// Reset the mirror transaction name
form.setValue('mirror_transaction_name', '')
}
}
const selectedAccount = useWatch({ control: form.control, name: (selectedTransaction.deposit && selectedTransaction.deposit > 0) ? 'paid_from' : 'paid_to' })
const direction = useDirection()
if (isUploading && isCompleted) {
return <FileUploadBanner uploadProgress={uploadProgress} />
}
return <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className='flex flex-col gap-4'>
{error && <ErrorBanner error={error} />}
<div className='grid grid-cols-2 gap-4'>
<SelectedTransactionDetails transaction={selectedTransaction} />
<div className='flex flex-col gap-4'>
<div className='grid grid-cols-2 gap-4'>
<DateField
name='posting_date'
label={_("Posting Date")}
isRequired
inputProps={{ autoFocus: false }}
/>
<DateField
name='reference_date'
label={_("Reference Date")}
isRequired
inputProps={{ autoFocus: false }}
/>
</div>
<DataField name='reference_no' label={_("Reference")} isRequired inputProps={{ autoFocus: false }} />
</div>
</div>
<div className='flex flex-col gap-2'>
<H4 className='text-base'>{isWithdrawal ? _('Transferred to') : _('Transferred from')}</H4>
<RecommendedTransferAccount transaction={selectedTransaction} onAccountChange={onAccountChange} />
<BankOrCashPicker company={selectedTransaction.company ?? ''} bankAccount={selectedTransaction.bank_account ?? ''} onAccountChange={onAccountChange} selectedAccount={selectedAccount} />
</div>
<div className='flex flex-col gap-2 py-2'>
<div className='flex items-end justify-between gap-4'>
<div className='flex-1'>
<AccountFormField
name="paid_from"
label={_("Paid From")}
account_type={['Bank', 'Cash']}
readOnly={isWithdrawal}
filterFunction={(account) => account.name !== selectedBankAccount.account}
isRequired
/>
</div>
<div className='pb-2'>
{direction === 'ltr' ? <ArrowRight /> : <ArrowLeft />}
</div>
<div className='flex-1'>
<AccountFormField
name="paid_to"
label={_("Paid To")}
account_type={['Bank', 'Cash']}
isRequired
readOnly={!isWithdrawal}
filterFunction={(account) => account.name !== selectedBankAccount.account}
/>
</div>
</div>
</div>
<Separator />
<div className='flex flex-col gap-2'>
<div className='grid grid-cols-2 gap-4'>
<SmallTextField
name='remarks'
label={_("Custom Remarks")}
formDescription={_("This will be auto-populated if not set.")}
/>
<div
data-slot="form-item"
className="flex flex-col gap-2"
>
<Label>{_("Attachments")}</Label>
<FileDropzone files={files} setFiles={setFiles} />
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button size='md' variant={'outline'} disabled={loading}>{_("Cancel")}</Button>
</DialogClose>
<Button size='md' type='submit' disabled={loading}>{_("Transfer")}</Button>
</DialogFooter>
</div>
</form>
</Form>
}
const BankOrCashPicker = ({ bankAccount, onAccountChange, selectedAccount, company }: { selectedAccount: string, bankAccount: string, onAccountChange: (account: string) => void, company: string }) => {
const { banks } = useGetBankAccounts(undefined, (bank) => bank.name !== bankAccount)
return <div className='grid grid-cols-4 gap-4'>
{banks.map((bank) => (
<div
className={cn('border p-2 rounded-md flex items-center gap-2 cursor-pointer outline-[0.5px] transition-all duration-200 hover:bg-surface-gray-1 dark:hover:bg-surface-gray-3',
selectedAccount === bank.account ? 'border-outline-gray-5 outline-outline-gray-5 bg-surface-gray-1 dark:bg-surface-gray-3' : 'border-outline-gray-2 outline-outline-gray-2'
)}
role='button'
key={bank.account}
onClick={() => onAccountChange(bank.account ?? '')}
>
<BankLogo bank={bank} iconSize='24px' imageClassName='w-12 h-12' />
<div className='flex flex-col gap-1'>
<span className='font-semibold text-sm'>{bank.account_name} {bank.bank_account_no && <span className='text-xs text-ink-gray-5'>({bank.bank_account_no})</span>}</span>
<span className='text-xs text-ink-gray-5'>{bank.account}</span>
</div>
</div>
))}
<CashPicker company={company ?? ''} selectedAccount={selectedAccount} setSelectedAccount={onAccountChange} />
</div>
}
const CashPicker = ({ company, selectedAccount, setSelectedAccount }: { company: string, selectedAccount: string, setSelectedAccount: (account: string) => void }) => {
const { data } = useFrappeGetCall('frappe.client.get_value', {
doctype: 'Company',
filters: company,
fieldname: 'default_cash_account'
}, undefined, {
revalidateOnFocus: false,
revalidateIfStale: false,
})
const account = data?.message?.default_cash_account
if (account) {
return <div className={cn('border p-2 rounded-md flex items-center gap-2 cursor-pointer outline-[0.5px] transition-all duration-200 hover:bg-surface-gray-1 dark:hover:bg-surface-gray-3',
selectedAccount === account ? 'border-outline-gray-5 outline-outline-gray-5 bg-surface-gray-1 dark:bg-surface-gray-3' : 'border-outline-gray-2 outline-outline-gray-2'
)}
role='button'
onClick={() => setSelectedAccount(account ?? '')}
>
<div className='flex items-center justify-center h-10 w-10'>
<Banknote size='24px' />
</div>
<div className='flex flex-col gap-1'>
<span className='font-semibold text-sm'>Cash</span>
<span className='text-xs text-ink-gray-5'>{data?.message?.default_cash_account}</span>
</div>
</div>
}
return null
}
const RecommendedTransferAccount = ({ transaction, onAccountChange }: { transaction: UnreconciledTransaction, onAccountChange: (account: string, is_mirror: boolean) => void }) => {
const { setValue, watch } = useFormContext<InternalTransferFormFields>()
const mirrorTransactionName = watch('mirror_transaction_name')
const paid_from = watch('paid_from')
const paid_to = watch('paid_to')
const { data } = useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.search_for_transfer_transaction', {
transaction_id: transaction.name
}, undefined, {
revalidateOnFocus: false,
revalidateIfStale: false,
})
// Get bank accounts to find the logo
const { banks } = useGetBankAccounts()
const bank = useMemo(() => {
if (data?.message?.bank_account && banks) {
return banks.find(bank => bank.name === data.message.bank_account)
}
return null
}, [data?.message?.bank_account, banks])
const selectTransaction = () => {
if (data?.message) {
setValue('mirror_transaction_name', data.message.name)
onAccountChange(data.message.account, true)
}
}
if (data?.message) {
const isWithdrawal = data.message.withdrawal && data.message.withdrawal > 0
const amount = isWithdrawal ? data.message.withdrawal : data.message.deposit
const currency = data.message.currency
const isAccountSelected = isWithdrawal ? paid_from === data.message.account : paid_to === data.message.account
const isSuggested = mirrorTransactionName === data?.message?.name && isAccountSelected
return (<div className='pb-2'>
<div className={cn("flex justify-between items-start gap-3 p-3 border rounded-lg shadow-sm",
isSuggested ? "border-outline-green-4 bg-surface-green-1" : "border-outline-violet-2 bg-surface-violet-2/50")}>
<div>
<div className='flex flex-col gap-3'>
<div className={cn("flex items-center gap-2 shrink-0",
isSuggested ? "text-ink-green-4" : "text-ink-violet-4"
)}>
<BadgeCheck className="w-4 h-4" />
<span className="text-sm font-medium">{_("Suggested Transfer to {0}", [data.message.account])}</span>
</div>
<div className='flex flex-col gap-1'>
<span className='text-p-sm'>{_("The system found a mirror transaction ({0}) in another account with the same amount and date.", [data.message.name])}</span>
<span className='text-p-sm'>{_("Accepting the suggestion will reconcile both transactions.")}</span>
</div>
<div className='flex flex-col gap-1.5'>
<div className='flex items-center gap-1'>
<Calendar size='16px' />
<span className='text-sm'>{formatDate(data.message.date, 'Do MMM YYYY')}</span>
</div>
<span className='text-sm line-clamp-1' title={data.message.description}>{data.message.description}</span>
</div>
</div>
</div>
<div className='flex flex-col items-end justify-between gap-2 h-full w-[30%]'>
<div className="flex items-center gap-2">
<BankLogo bank={bank} iconSize='24px' imageClassName='h-8 max-w-24' iconClassName={cn(isSuggested ? "text-ink-green-3" : "text-purple-600")} />
</div>
<div className='flex gap-1'>
<div className={cn('flex items-center gap-1 text-end px-0 justify-end py-1 rounded-sm',
isWithdrawal ? 'text-ink-red-3' : 'text-ink-green-3'
)}>
{isWithdrawal ? <ArrowUpRight className="w-5 h-5 text-ink-red-3" /> : <ArrowDownRight className="w-5 h-5 text-ink-green-3" />}
<span className='text-sm font-semibold uppercase'>{isWithdrawal ? _('Transferred Out') : _('Received')}</span>
</div>
</div>
<span className='font-semibold font-numeric text-lg text-end pe-0.5'>{formatCurrency(amount, currency)}</span>
<div className='pt-1'>
<Button
onClick={selectTransaction}
theme={isSuggested ? "green" : "violet"}
size="md"
type='button'
>
{isSuggested ? <CheckCircle /> : <CheckIcon />}
{isSuggested ? _("Accepted") : _("Use Suggestion")}
</Button>
</div>
</div>
</div>
</div>
)
}
return null
}
export default TransferModal

View File

@@ -1,85 +0,0 @@
import { BankAccount } from "@/types/Accounts/BankAccount";
import { getDatesForTimePeriod } from "@/lib/date";
import { atom } from "jotai";
import { atomWithStorage, createJSONStorage } from "jotai/utils";
import { atomFamily } from 'jotai-family'
import { UnreconciledTransaction } from "./utils";
import { BankTransaction } from "@/types/Accounts/BankTransaction";
import { PaymentEntry } from "@/types/Accounts/PaymentEntry";
import { JournalEntry } from "@/types/Accounts/JournalEntry";
export interface SelectedBank extends Pick<BankAccount, 'name' | 'bank' | 'is_credit_card' | 'company' | 'account_name' | 'bank_account_no' | 'account' | 'account_type' | 'integration_id' | 'is_default' | 'last_integration_date'> {
logo?: string,
logoDark?: string,
darkModeInvert?: boolean,
logoClassName?: string,
account_currency?: string
}
export const selectedBankAccountAtom = atomWithStorage<SelectedBank | null>('bank-rec-selected-bank', null, undefined, {
getOnInit: true
})
export const bankRecDateAtom = atomWithStorage<{ fromDate: string, toDate: string }>("bank-rec-date", {
fromDate: getDatesForTimePeriod('This Month').fromDate,
toDate: getDatesForTimePeriod('This Month').toDate
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const bankRecClosingBalanceAtom = atomFamily((_id: string) => {
return atom<{ value: number, stringValue: string | number | undefined }>({
value: 0,
stringValue: '0.00'
})
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const bankRecSelectedTransactionAtom = atomFamily((_id: string) => {
return atom<UnreconciledTransaction[]>([])
})
/** Action Modals */
export const bankRecTransferModalAtom = atom(false)
export const bankRecRecordPaymentModalAtom = atom(false)
export const bankRecRecordJournalEntryModalAtom = atom(false)
export const bankRecUnreconcileModalAtom = atom<string>('')
export const bankRecMatchFilters = atomWithStorage<string[]>('bank-rec-match-filters', ['payment_entry', 'journal_entry'])
export const bankRecSearchText = atom<string>('')
export const bankRecAmountFilter = atom<{ value: number, stringValue?: string | number }>({
value: 0,
stringValue: '0.00'
})
export const bankRecTransactionTypeFilter = atom<string>('All')
export interface ActionLog {
type: 'match' | 'payment' | 'transfer' | 'bank_entry'
isBulk: boolean
timestamp: number,
items: ActionLogItem[],
bulkCommonData?: {
party_type?: string,
party?: string,
account?: string,
bank_account?: string,
}
}
export interface ActionLogItem {
bankTransaction: BankTransaction,
voucher: {
reference_doctype: string,
reference_name: string,
reference_no?: string,
reference_date?: string,
posting_date: string,
doc?: PaymentEntry | JournalEntry
},
}
const actionLogStorage = createJSONStorage<ActionLog[]>(() => sessionStorage)
export const bankRecActionLog = atomWithStorage<ActionLog[]>('bank-rec-action-log', [], actionLogStorage, {
getOnInit: true,
})

View File

@@ -1,410 +0,0 @@
export const BANK_LOGOS: { keywords: string[], logo: string, locale?: string[], logoDark?: string, darkModeInvert?: boolean, logoClassName?: string }[] = [
// United States + International
{
keywords: ['American Express', 'Amex'],
logo: 'Amex.svg',
locale: ['Global', 'United States']
},
{
keywords: ['Bank of America', 'BOA'],
logo: 'Bank_of_America.png',
darkModeInvert: true,
locale: ['United States']
},
{
keywords: ['Barclays'],
logo: 'Barclays.svg',
locale: ['Global', 'United Kingdom'],
logoClassName: 'h-12',
},
{
keywords: ['BNP Paribas'],
logo: 'BNP_Paribas.svg',
logoDark: 'BNP_Paribas-Dark.svg',
locale: ['Global', 'France'],
logoClassName: 'max-w-24'
},
{
keywords: ['Bank of New York Mellon', 'BNY Mellon', 'BNY'],
logo: 'BNY_Mellon.svg',
locale: ['Global', 'United States'],
logoDark: 'BNY_Mellon-Dark.svg',
},
{
keywords: ['Capital One'],
logo: 'Capital_One.png',
locale: ['United States'],
darkModeInvert: true
},
{
keywords: ['Charles Schwab', 'Schwab'],
logo: 'Charles_Schwab.svg',
locale: ['United States'],
logoClassName: 'h-7'
},
{
keywords: ['Chase'],
logo: 'chase.svg',
locale: ['Global', 'United States'],
logoDark: 'chase-Dark.svg',
},
{
keywords: ['Citi', 'Citibank', 'Citi Group', 'Citi Financial Services'],
logo: 'Citi.svg',
locale: ['Global', 'United States']
},
{
keywords: ['Deutsche Bank'],
logo: 'Deutsche_Bank.svg',
locale: ['Global', 'Germany'],
darkModeInvert: true,
},
{
keywords: ['Goldman Sachs'],
logo: 'Goldman_Sachs.svg',
locale: ['Global', 'United States'],
darkModeInvert: true,
},
{
keywords: ['HSBC'],
logo: 'HSBC.svg',
locale: ['Global', 'United Kingdom'],
logoDark: 'HSBC-dark.svg',
},
{
keywords: ['JPMorgan Chase', 'JPMorgan', 'JP Morgan', 'JP Morgan Chase', 'JPMorgan Chase & Co', 'JPM', 'JPMC'],
logo: 'jpmc.svg',
locale: ['Global', 'United States'],
darkModeInvert: true,
},
{
keywords: ['Morgan Stanley'],
logo: 'Morgan_Stanley.png',
locale: ['Global', 'United States'],
darkModeInvert: true,
},
{
keywords: ['PNC', 'PNC Financial Services Group', 'PNC Financial Services', 'Pittsburgh National Corporation'],
logo: 'PNC.png',
locale: ['United States']
},
{
keywords: ['Santander'],
logo: 'Santander.svg',
locale: ['Global']
},
{
keywords: ['TD Bank', 'Toronto Dominion Bank'],
logo: 'Toronto_Dominion_Bank.png',
locale: ['Canada']
},
{
keywords: ['Truist'],
logo: 'Truist.svg',
locale: ['United States'],
darkModeInvert: true,
logoClassName: 'h-8'
},
{
keywords: ['UBS'],
logo: 'UBS.svg',
locale: ['Global', 'Switzerland'],
logoDark: 'UBS-dark.svg',
},
{
keywords: ['US Bank', 'USBank', 'U.S. Bank', 'U.S. Bancorp'],
logo: 'USBank.svg',
locale: ['United States'],
logoDark: 'USBank-dark.svg',
},
{
keywords: ['Wells Fargo', 'Wells Fargo'],
logo: 'Wells_Fargo.svg',
locale: ['United States'],
logoClassName: 'h-7'
},
{
keywords: ['OakStar', 'Oakstar', 'Oakstar'],
logo: 'Oakstar.png',
logoDark: 'Oakstar-dark.webp',
locale: ['United States'],
logoClassName: 'h-7'
},
{
keywords: ['PlainsCapital', 'Plains Capital'],
logo: 'PlainsCapitalBank.png',
locale: ['United States'],
logoClassName: 'h-7'
},
{
keywords: ["Standard Chartered"],
logo: 'Standard_Chartered.png',
logoDark: 'Standard_Chartered-dark.png',
locale: ['Global'],
},
// India
{
keywords: ['HDFC Bank', 'HDFC'],
logo: 'HDFC.svg',
locale: ['India'],
},
{
keywords: ['ICICI Bank', 'ICICI'],
logo: 'ICICI.svg',
logoDark: 'ICICI-dark.svg',
locale: ['India'],
},
{
keywords: ['SBI', 'State Bank of India'],
logo: 'State_Bank_of_India.svg',
logoDark: 'State_bank_of_India-Dark.svg',
locale: ['India'],
logoClassName: 'h-4.5'
},
{
keywords: ['Punjab National Bank', 'PNB'],
logo: 'Punjab_National_Bank.svg',
locale: ['India']
},
{
keywords: ['Union Bank of India', 'Union Bank'],
logo: 'Union_Bank_of_India.svg',
locale: ['India']
},
{
keywords: ['Yes Bank', 'Yes'],
logo: 'Yes_Bank.svg',
locale: ['India'],
logoDark: 'Yes_Bank-dark.svg',
},
{
keywords: ['RBL Bank', 'RBL'],
logo: 'RBL_Bank.svg',
locale: ['India'],
logoDark: 'RBL_Bank-dark.svg',
},
{
keywords: ['Axis Bank', 'Axis'],
logo: 'Axis_Bank.svg',
locale: ['India'],
darkModeInvert: true
},
{
keywords: ['Bank of Baroda', 'BOB'],
logo: 'Bank_of_Baroda.svg',
locale: ['India', 'Kenya'],
logoClassName: 'h-7'
},
{
keywords: ['Bank of India', 'BOI'],
logo: 'Bank_of_India.png',
locale: ['India'],
logoClassName: 'h-7'
},
{
keywords: ['Bank of Maharashtra', 'BOM'],
logo: 'Bank_of_Maharashtra.png',
locale: ['India'],
logoClassName: 'min-w-24'
},
{
keywords: ['Kotak Mahindra Bank', 'Kotak'],
logo: 'Kotak_Mahindra.svg',
locale: ['India']
},
{
keywords: ['IndusInd Bank', 'IndusInd'],
logo: 'IndusInd_Bank.svg',
locale: ['India'],
darkModeInvert: true,
},
{
keywords: ['IDBI Bank', 'IDBI'],
logo: 'IDBI_Bank.svg',
locale: ['India']
},
{
keywords: ['IDFC First Bank', 'IDFC First'],
logo: 'IDFC_First_Bank.svg',
locale: ['India']
},
{
keywords: ['Federal Bank'],
logo: 'Federal_Bank.png',
logoDark: 'Federal_Bank-dark.png',
locale: ['India']
},
{
keywords: ['Fi Bank'],
logo: 'Fi_Bank.svg',
locale: ['India']
},
{
keywords: ['RazorpayX', 'Razorpay'],
logo: 'Razorpay.svg',
logoDark: 'Razorpay-dark.svg',
locale: ['India']
},
{
keywords: ['Revolut'],
logo: 'Revolut.png',
locale: ['Global'],
darkModeInvert: true
},
{
keywords: ['Starling Bank'],
logo: 'Starling_Bank.png',
logoDark: 'Starling_Bank-dark.png',
locale: ['Global', 'UK'],
logoClassName: 'h-10'
},
// Australia and New Zealand
{
keywords: ["Commonwealth Bank", "CBA"],
logo: "Commonwealth_Bank.svg",
locale: ['Australia', 'New Zealand'],
},
{
keywords: ["Airwallex"],
logo: "Airwallex.png",
logoDark: "Airwallex-dark.png",
locale: ['Global']
},
{
keywords: ["Judo Bank"],
logo: "Judo_Bank.svg",
logoDark: "Judo_Bank-dark.svg",
locale: ['Australia', 'New Zealand']
},
{
keywords: ["Alpha"], // This might conflict with Alpha Bank in Greece
logo: "Alpha_Bank.svg",
darkModeInvert: true,
logoClassName: 'h-4.5',
locale: ['Australia', 'New Zealand']
},
{
keywords: ["Australian Tax Office", "Australian Taxation Office"],
logo: "Australian_Tax_Office.png",
darkModeInvert: true,
locale: ['Australia']
},
{
keywords: ["Westpac"],
logo: "Westpac.svg",
locale: ['Australia']
},
{
keywords: ["ANZ", "ANZ Bank", "Australia and New Zealand Banking Group"],
logo: "ANZ.png",
locale: ['Australia', 'New Zealand']
},
{
keywords: ["Macquarie Group", "Macquarie Bank"],
logo: "Macquarie.svg",
darkModeInvert: true,
locale: ['Australia']
},
// Nicaragua
{
keywords: ["Banco Atlantida", "Banco Atlántida"],
logo: "Banco_Atlantida.png",
locale: ['Nicaragua']
},
{
keywords: ["Banco de Finanzas"],
logo: "Banco_de_Finanzas.svg",
locale: ['Nicaragua'],
logoClassName: 'h-4.5'
},
{
keywords: ["Avanz"],
logo: "Avanz.svg",
logoDark: "Avanz-dark.svg",
locale: ['Nicaragua'],
logoClassName: 'h-7'
},
{
keywords: ["Ficohsa"],
logo: "Ficohsa.svg",
locale: ['Nicaragua']
},
{
keywords: ["BAC", "BAC Credomatic"],
logo: "BAC_Credomatic.svg",
locale: ['Nicaragua'],
logoClassName: 'h-4.5'
},
{
keywords: ["Banco Lafise"],
logo: "Banco_Lafise.png",
darkModeInvert: true,
locale: ['Nicaragua']
},
// German
{
keywords: ["Sparkasse"],
logo: "Sparkasse.png",
locale: ['Germany']
},
{
keywords: ["Volksbank", "Raiffeisenbank", "VR-Bank"],
logo: "Volksbanken_Raiffeisenbanken.svg",
locale: ['Germany'],
logoClassName: 'min-w-32'
},
// Kenya
{
keywords: ["KCB Bank", "KCB"],
logo: "KCB_Bank_Kenya.png",
locale: ['Kenya']
},
{
keywords: ["Equity Bank"],
logo: "Equity_Bank.png",
logoDark: "Equity_Bank-dark.png",
locale: ['Kenya'],
},
{
keywords: ["I&M"],
logo: "I&M.png",
locale: ['Kenya']
},
{
keywords: ["ABSA"],
logo: "ABSA.png",
locale: ['Kenya'],
darkModeInvert: true,
logoClassName: 'h-7'
},
{
keywords: ["Stanbic"],
logo: "Stanbic.png",
locale: ['Kenya'],
logoClassName: 'h-7'
},
{
keywords: ["DTB", "Diamond Trust Bank"],
logo: "Diamond_Trust_Bank.png",
locale: ['Kenya']
},
{
keywords: ["Prime Bank"],
logo: "Prime_Bank.png",
locale: ['Kenya'],
logoClassName: 'max-w-28'
},
{
keywords: ["Stripe"],
logo: "Stripe.svg",
locale: ['Global'],
logoClassName: 'h-6',
darkModeInvert: true,
},
{
keywords: ["PayPal"],
logo: "PayPal.png",
locale: ['Global'],
logoClassName: 'h-6',
}
]

View File

@@ -1,457 +0,0 @@
import { ActionLog, bankRecActionLog, bankRecAmountFilter, bankRecDateAtom, bankRecMatchFilters, bankRecSearchText, bankRecSelectedTransactionAtom, bankRecTransactionTypeFilter, bankRecUnreconcileModalAtom, SelectedBank, selectedBankAccountAtom } from './bankRecAtoms'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import { useMemo } from 'react'
import { SWRConfiguration, useFrappeGetCall, useFrappeGetDoc, useFrappePostCall, useSWRConfig } from 'frappe-react-sdk'
import { BankTransaction } from '@/types/Accounts/BankTransaction'
import { BankAccount } from '@/types/Accounts/BankAccount'
import dayjs from 'dayjs'
import { toast } from 'sonner'
import { BANK_LOGOS } from './logos'
import { getErrorMessage } from '@/lib/frappe'
import { useCurrentCompany } from '@/hooks/useCurrentCompany'
import _ from '@/lib/translate'
import { BankTransactionRule } from '@/types/Accounts/BankTransactionRule'
import { useRef } from 'react'
import type { DebouncedState } from 'usehooks-ts'
import { useDebounceCallback } from 'usehooks-ts'
import Fuse from 'fuse.js'
export const useGetAccountOpeningBalance = () => {
const companyID = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const args = useMemo(() => {
return {
bank_account: bankAccount?.name,
company: companyID,
till_date: dayjs(dates.fromDate).subtract(1, 'days').format('YYYY-MM-DD'),
}
}, [companyID, bankAccount?.name, dates.fromDate])
return useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance', args, undefined, {
revalidateOnFocus: false
})
}
export const useGetAccountClosingBalance = () => {
const companyID = useCurrentCompany()
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const args = useMemo(() => {
return {
bank_account: bankAccount?.name,
company: companyID,
till_date: dates.toDate,
}
}, [companyID, bankAccount?.name, dates.toDate])
return useFrappeGetCall('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance', args,
`bank-reconciliation-account-closing-balance-${bankAccount?.name}-${dates.toDate}`,
{
revalidateOnFocus: false
}
)
}
/**
* Hook to fetch the closing balance set in the database for the given bank and date
*/
export const useGetAccountClosingBalanceAsPerStatement = (swrConfig: SWRConfiguration = {}) => {
const dates = useAtomValue(bankRecDateAtom)
const bankAccount = useAtomValue(selectedBankAccountAtom)
return useFrappeGetCall<{ message: { balance: number, date?: string } }>("erpnext.accounts.doctype.bank_account.bank_account.get_closing_balance_as_per_statement", {
bank_account: bankAccount?.name,
date: dates.toDate
}, `bank-reconciliation-account-closing-balance-as-per-statement-${bankAccount?.name}-${dates.toDate}`, {
revalidateOnFocus: false,
...swrConfig
})
}
export type UnreconciledTransaction = Pick<BankTransaction, 'name' | 'matched_transaction_rule' | 'date' | 'withdrawal' | 'deposit' | 'currency' | 'description' | 'status' | 'transaction_type' | 'reference_number' | 'party_type' | 'party' | 'bank_account' | 'company' | 'unallocated_amount'>
export const useGetUnreconciledTransactions = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
return useFrappeGetCall<{ message: UnreconciledTransaction[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions', {
bank_account: bankAccount?.name,
from_date: dates.fromDate,
to_date: dates.toDate
}, bankAccount ? `bank-reconciliation-unreconciled-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}` : null, {
revalidateOnFocus: false,
revalidateIfStale: false
})
}
export interface LinkedPayment {
rank: number,
doctype: string,
name: string,
paid_amount: number,
reference_no: string,
reference_date: string,
posting_date: string,
party_type?: string,
party?: string,
currency: string
}
export const useGetBankTransactions = () => {
const bankAccount = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
return useFrappeGetCall<{ message: BankTransaction[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions', {
bank_account: bankAccount?.name,
from_date: dates.fromDate,
to_date: dates.toDate,
all_transactions: true
}, bankAccount ? `bank-reconciliation-bank-transactions-${bankAccount?.name}-${dates.fromDate}-${dates.toDate}` : null)
}
export const useGetVouchersForTransaction = (transaction: UnreconciledTransaction) => {
const dates = useAtomValue(bankRecDateAtom)
const matchFilters = useAtomValue(bankRecMatchFilters)
return useFrappeGetCall<{ message: LinkedPayment[] }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments', {
bank_transaction_name: transaction.name,
document_types: matchFilters ?? ['payment_entry', 'journal_entry'],
from_date: dates.fromDate,
to_date: dates.toDate,
filter_by_reference_date: 0
}, `bank-reconciliation-vouchers-${transaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`, {
revalidateOnFocus: false
})
}
/**
* Common hook to refresh the unreconciled transactions list after a transaction is reconciled
* @returns function to call to refresh the unreconciled transactions list AFTER the operation is done
*/
export const useRefreshUnreconciledTransactions = () => {
const selectedBank = useAtomValue(selectedBankAccountAtom)
const dates = useAtomValue(bankRecDateAtom)
const matchFilters = useAtomValue(bankRecMatchFilters)
const setSelectedTransaction = useSetAtom(bankRecSelectedTransactionAtom(selectedBank?.name || ''))
const { mutate } = useSWRConfig()
const searchString = useAtomValue(bankRecSearchText)
const typeFilter = useAtomValue(bankRecTransactionTypeFilter)
const amountFilter = useAtomValue(bankRecAmountFilter)
const { data: unreconciledTransactions } = useGetUnreconciledTransactions()
/**
* This function should be called after a transaction is reconciled
* It will get the next unreconciled transaction and select it
* And then refresh the balance + unreconciled transactions list
*/
const onReconcileTransaction = (transaction: UnreconciledTransaction, updatedTransaction?: BankTransaction) => {
// If the updated transaction has an unallocated amount of 0, then we need to select the next unreconciled transaction
if (updatedTransaction && updatedTransaction?.unallocated_amount !== 0) {
mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`)
mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`)
// Update the matching vouchers for the selected transaction
mutate(`bank-reconciliation-vouchers-${transaction.name}-${dates.fromDate}-${dates.toDate}-${matchFilters.join(',')}`)
return
}
// From unreconciled transactions list, first apply the filters based on the search criteria and other filters
const searchIndex = unreconciledTransactions ? new Fuse(unreconciledTransactions.message, {
keys: ['description', 'reference_number'],
threshold: 0.5,
includeScore: true
}) : null
const results = getSearchResults(searchIndex, searchString, typeFilter, amountFilter.value, unreconciledTransactions?.message)
const currentIndex = results.findIndex(t => t.name === transaction.name)
let nextTransaction = null
if (currentIndex !== -1) {
// Check if there is a next transaction
if (currentIndex < (results.length || 0) - 1) {
nextTransaction = results[currentIndex + 1]
}
}
// We need to select the next unreconciled transaction for a better UX
mutate(`bank-reconciliation-unreconciled-transactions-${selectedBank?.name}-${dates.fromDate}-${dates.toDate}`)
.then(res => {
if (nextTransaction) {
// Check if next transaction is there in the response
const nextTransactionObj = res?.message.find((t: UnreconciledTransaction) => t.name === nextTransaction.name)
if (nextTransactionObj) {
setSelectedTransaction([nextTransactionObj])
} else {
// If the next transaction is not there in the response, we need to clear the selection
setSelectedTransaction([])
}
} else {
// If there is no next transaction, we need to clear the selection
setSelectedTransaction([])
}
})
mutate(`bank-reconciliation-account-closing-balance-${selectedBank?.name}-${dates.toDate}`)
}
return onReconcileTransaction
}
export const useReconcileTransaction = () => {
const { call, loading } = useFrappePostCall<{ message: BankTransaction }>('erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers')
const onReconcileTransaction = useRefreshUnreconciledTransactions()
const setBankRecUnreconcileModalAtom = useSetAtom(bankRecUnreconcileModalAtom)
const addToActionLog = useUpdateActionLog()
const reconcileTransaction = (transaction: UnreconciledTransaction, voucher: LinkedPayment) => {
call({
bank_transaction_name: transaction.name,
vouchers: JSON.stringify([{
"payment_doctype": voucher.doctype,
"payment_name": voucher.name,
"amount": voucher.paid_amount
}])
}).then((res) => {
addToActionLog({
type: 'match',
timestamp: (new Date()).getTime(),
isBulk: false,
items: [
{
bankTransaction: res.message,
voucher: {
reference_doctype: voucher.doctype,
reference_name: voucher.name,
reference_no: voucher.reference_no,
reference_date: voucher.reference_date,
posting_date: voucher.posting_date,
}
}
]
})
onReconcileTransaction(transaction, res.message)
toast.success(_("Reconciled"), {
duration: 4000,
closeButton: true,
action: {
label: _("Undo"),
onClick: () => setBankRecUnreconcileModalAtom(transaction.name)
},
actionButtonStyle: {
backgroundColor: "rgb(0, 138, 46)"
}
})
}).catch((error) => {
console.error(error)
toast.error(_("Error"), {
duration: 5000,
description: getErrorMessage(error)
})
})
}
return { reconcileTransaction, loading }
}
interface BankAccountWithCurrency extends Pick<BankAccount, 'name' | 'bank' | 'account_name' | 'is_credit_card' | 'company' | 'account' | 'account_type' | 'account_subtype' | 'bank_account_no' | 'last_integration_date'> {
account_currency?: string
}
type BankLogoEntry = (typeof BANK_LOGOS)[number]
/** Prefer the longest keyword match so short tokens (e.g. "anz" in "finanzas") do not beat full bank names. */
function findBankLogoForName(bankName: string | undefined | null): BankLogoEntry | undefined {
if (!bankName) return undefined
const haystack = bankName.toLowerCase()
let best: BankLogoEntry | undefined
let bestKeywordLen = 0
for (const entry of BANK_LOGOS) {
for (const keyword of entry.keywords) {
const needle = keyword.toLowerCase()
if (needle.length === 0) continue
if (haystack.includes(needle) && needle.length > bestKeywordLen) {
bestKeywordLen = needle.length
best = entry
}
}
}
return best
}
export const useGetBankAccounts = (onSuccess?: (data?: Omit<SelectedBank, 'logo'>[]) => void, filterFn?: (bank: SelectedBank) => boolean) => {
const company = useCurrentCompany()
const { data, isLoading, error } = useFrappeGetCall<{ message: BankAccountWithCurrency[] }>('erpnext.accounts.doctype.bank_account.bank_account.get_list', {
company: company
}, undefined, {
revalidateOnFocus: false,
revalidateIfStale: false,
onSuccess: (data) => {
onSuccess?.(data?.message)
}
})
const banks = useMemo(() => {
// Match the bank account to the logo
const banksWithLogos = data?.message.map((bank) => {
const logo = findBankLogoForName(bank.bank)
return {
...bank,
logo: logo?.logo,
logoDark: logo?.logoDark,
darkModeInvert: logo?.darkModeInvert,
logoClassName: logo?.logoClassName
}
}) ?? []
if (filterFn) {
return banksWithLogos.filter(filterFn)
}
return banksWithLogos
}, [data, filterFn])
return {
banks,
isLoading,
error
}
}
export const useIsTransactionWithdrawal = (transaction: UnreconciledTransaction) => {
return useMemo(() => {
const isWithdrawal = transaction.withdrawal && transaction.withdrawal > 0
const isDeposit = transaction.deposit && transaction.deposit > 0
return {
amount: isWithdrawal ? transaction.withdrawal : transaction.deposit,
isWithdrawal,
isDeposit
}
}, [transaction])
}
export const useGetRuleForTransaction = (transaction: UnreconciledTransaction) => {
return useFrappeGetDoc<BankTransactionRule>('Bank Transaction Rule', transaction.matched_transaction_rule,
transaction.matched_transaction_rule ? undefined : null, {
revalidateOnFocus: false,
revalidateIfStale: false
}
)
}
/** Hook to handle the search input while maintaining debouncing and global state. */
export function useTransactionSearch(): [string, DebouncedState<(value: string) => void>] {
const delay = 500
const unwrappedInitialValue = ''
const eq = (left: string, right: string) => left === right
const [debouncedValue, setDebouncedValue] = useAtom(bankRecSearchText)
const previousValueRef = useRef<string | undefined>(unwrappedInitialValue)
const updateDebouncedValue = useDebounceCallback(
setDebouncedValue,
delay,
)
// Update the debounced value if the initial value changes
if (!eq(previousValueRef.current as string, unwrappedInitialValue)) {
updateDebouncedValue(unwrappedInitialValue)
previousValueRef.current = unwrappedInitialValue
}
return [debouncedValue, updateDebouncedValue]
}
/** Utility function to get the search results based on the search index, search string, type filter, amount filter and unreconciled transactions */
export const getSearchResults = (
/** Fuse index of the unreconciled transactions */
searchIndex: Fuse<UnreconciledTransaction> | null,
/** Search string */
search: string,
/** Type filter */
typeFilter: string,
/** Amount filter */
amountFilter: number,
/** Unreconciled transactions */
unreconciledTransactions?: UnreconciledTransaction[]) => {
let r = []
if (!searchIndex || !search) {
r = unreconciledTransactions ?? []
} else {
r = searchIndex.search(search).map((result) => result.item)
}
if (typeFilter !== 'All') {
r = r.filter((transaction) => {
if (typeFilter === 'Debits') {
return transaction.withdrawal && transaction.withdrawal > 0
}
if (typeFilter === 'Credits') {
return transaction.deposit && transaction.deposit > 0
}
})
}
if (amountFilter > 0) {
r = r.filter((transaction) => {
if (transaction.withdrawal && transaction.withdrawal > 0) {
return transaction.withdrawal === amountFilter
}
if (transaction.deposit && transaction.deposit > 0) {
return transaction.deposit === amountFilter
}
return false
})
}
return r
}
export const useUpdateActionLog = () => {
const setActionLog = useSetAtom(bankRecActionLog)
const addToActionLog = (action: ActionLog) => {
// Store at max 100 actions
setActionLog((prev) => {
const newActions = [action, ...prev]
if (newActions.length > 100) {
return newActions.slice(0, 100)
}
return newActions
})
}
return addToActionLog
}

View File

@@ -1,22 +0,0 @@
import CSVRawDataPreview from './CSVRawDataPreview'
import StatementDetails from './StatementDetails'
import _ from '@/lib/translate'
import { GetStatementDetailsResponse } from '../import_utils'
const CSVImport = ({ data }: { data: { message: GetStatementDetailsResponse } }) => {
return (
<div className="w-full flex">
<div className="w-[50%] p-4 h-[calc(100vh-72px)] overflow-scroll">
<StatementDetails data={data.message} />
</div>
<div className="w-[50%] border-s border-t pe-1 ps-0 border-outline-gray-2 h-[calc(100vh-72px)] overflow-scroll">
<CSVRawDataPreview data={data.message} />
</div>
</div>
)
}
export default CSVImport

View File

@@ -1,151 +0,0 @@
import { Table, TableBody, TableCell, TableHead, TableRow } from "@/components/ui/table"
import { cn } from "@/lib/utils"
import { ArrowDownRightIcon, ArrowUpDownIcon, ArrowUpRightIcon, BanknoteIcon, CalendarIcon, DollarSignIcon, FileTextIcon, ListIcon, ReceiptIcon } from "lucide-react"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import _ from "@/lib/translate"
import { GetStatementDetailsResponse } from "../import_utils"
import { useMemo } from "react"
import { BankStatementImportLogColumnMap } from "@/types/Accounts/BankStatementImportLogColumnMap"
const CSVRawDataPreview = ({ data }: { data: GetStatementDetailsResponse }) => {
const column_mapping: Record<StandardColumnTypes, number> = useMemo(() => {
const col_map: Record<string, number> = {}
data.doc.column_mapping?.forEach(col => {
if (col.maps_to && col.maps_to !== "Do not import") {
col_map[col.maps_to] = col.index;
}
})
return col_map
}, [data])
const validColumns = Object.values(column_mapping)
// Reverse the column mapping to get a map of column index to variable name
const columnIndexMap: Record<number, StandardColumnTypes> = Object.fromEntries(Object.entries(column_mapping).map(([variable, columnIndex]) => [columnIndex, variable as StandardColumnTypes]))
// Loop over the contents of the CSV file and show a preview - highlight the header row and the transaction rows
return (
<Table containerClassName="rounded-none">
<TableBody>
{data.raw_data.map((row, index) => {
const isHeaderRow = index === data.doc.detected_header_index;
const isTransactionRow = index >= (data.doc.detected_transaction_starting_index ?? 0) && index <= (data.doc.detected_transaction_ending_index ?? 0);
return <TableRow key={index}
title={isHeaderRow ? "Header Row" : ""}
className={cn({
// "bg-yellow-100": isHeaderRow,
// "hover:bg-yellow-100": isHeaderRow,
"bg-green-50 hover:bg-green-50 dark:bg-green-700 dark:hover:bg-green-700": isTransactionRow,
"text-ink-gray-5/70": !isTransactionRow && !isHeaderRow,
})}>
{isHeaderRow ? <TableHead className="bg-yellow-100 hover:bg-yellow-100 dark:bg-yellow-400 text-center font-semibold text-ink-gray-8">
{index + 1}
</TableHead> :
<TableCell className="text-center px-1 py-0.5">
{index + 1}
</TableCell>
}
{row.map((cell, cellIndex) => {
const isValidColumn = validColumns.includes(cellIndex);
const columnType = columnIndexMap[cellIndex];
const isAmountColumn = ["Amount", "Withdrawal", "Deposit", "Balance"].includes(columnType);
if (isHeaderRow) {
return <TableHead key={cellIndex} className={cn("max-w-[250px] w-fit overflow-hidden text-ellipsis py-0.5",
isValidColumn ? "bg-yellow-100 hover:bg-yellow-100 dark:bg-yellow-400" : "bg-surface-gray-2",
)}>
<div className={cn("flex items-center text-xs gap-1 px-1 text-ink-gray-8 font-medium", {
"justify-end": isAmountColumn && isValidColumn
})}>
{columnType && <Tooltip>
<TooltipTrigger>
<ColumnHeaderIcon columnType={columnType} />
</TooltipTrigger>
<TooltipContent>
{_(columnType)}
</TooltipContent>
</Tooltip>
}
{cell}
</div>
</TableHead>
} else {
return <TableCell key={cellIndex} className={cn("max-w-[200px] w-fit overflow-hidden text-ellipsis py-0.5",
{
"bg-green-100 dark:bg-green-400 hover:bg-green-100 dark:hover:bg-green-400": isValidColumn && isTransactionRow,
"text-ink-gray-5": !isValidColumn && isTransactionRow,
}
)} >
<div className={cn("min-h-5 flex items-center text-xs px-1", {
"justify-end": isAmountColumn && isValidColumn && isTransactionRow
})} title={cell}>
{cell}
</div>
</TableCell>
}
}
)}
</TableRow>
})}
</TableBody>
</Table >
)
}
type StandardColumnTypes = BankStatementImportLogColumnMap['maps_to'];
const ColumnHeaderIcon = ({ columnType }: { columnType?: StandardColumnTypes }) => {
if (!columnType) {
return null
}
if (columnType === 'Amount') {
return <DollarSignIcon className="w-4 h-4" />
}
if (columnType === 'Withdrawal') {
return <ArrowUpRightIcon className="w-4 h-4 text-ink-red-3" />
}
if (columnType === 'Deposit') {
return <ArrowDownRightIcon className="w-4 h-4 text-ink-green-3" />
}
if (columnType === 'Balance') {
return <BanknoteIcon className="w-4 h-4" />
}
if (columnType === 'Date') {
return <CalendarIcon className="w-4 h-4" />
}
if (columnType === 'Description') {
return <FileTextIcon className="w-4 h-4" />
}
if (columnType === 'Reference') {
return <ReceiptIcon className="w-4 h-4" />
}
if (columnType === 'Transaction Type') {
return <ListIcon className="w-4 h-4" />
}
if (columnType === 'Debit/Credit') {
return <ArrowUpDownIcon className="w-4 h-4" />
}
return null
}
export default CSVRawDataPreview

View File

@@ -1,351 +0,0 @@
import _ from '@/lib/translate'
import { GetStatementDetailsResponse } from '../import_utils'
import { flt, formatCurrency } from '@/lib/numbers'
import { formatDate } from '@/lib/date'
import { bankRecDateAtom } from '../../BankReconciliation/bankRecAtoms'
import { AlertCircleIcon, ChevronLeftIcon, ChevronRightIcon, ExternalLinkIcon, InfoIcon, Loader2Icon } from 'lucide-react'
import { H2, H3, Paragraph } from '@/components/ui/typography'
import { FileTypeIcon } from '@/components/ui/file-dropzone'
import { getFileExtension } from '@/lib/file'
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useFrappeEventListener, useFrappePostCall } from 'frappe-react-sdk'
import { toast } from 'sonner'
import ErrorBanner from '@/components/ui/error-banner'
import { Link, useNavigate } from 'react-router-dom'
import { useMemo, useState } from 'react'
import { Progress } from '@/components/ui/progress'
import { useSetAtom } from 'jotai'
import { useDirection } from '@/components/ui/direction'
import BankLogo from '@/components/common/BankLogo'
import { useGetBankAccounts } from '../../BankReconciliation/utils'
import { BankStatementImportLog } from '@/types/Accounts/BankStatementImportLog'
import { Badge } from '@/components/ui/badge'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
const parseDateFormat = (dateFormat: string) => {
const charMap = {
"%d": "DD",
"%m": "MM",
"%Y": "YYYY",
"%y": "YY",
"%b": "MMM",
"%B": "MMMM",
}
let label = dateFormat
Object.keys(charMap).forEach((char) => {
label = label.replace(char, charMap[char as keyof typeof charMap])
})
return dateFormat
}
type Props = {
data: GetStatementDetailsResponse,
}
const StatementDetails = ({ data }: Props) => {
const dateFormat = parseDateFormat(data.date_format)
const { call, loading, error } = useFrappePostCall<{ docs: BankStatementImportLog[] }>('run_doc_method')
const navigate = useNavigate()
const setDates = useSetAtom(bankRecDateAtom)
const direction = useDirection()
const onImport = () => {
call({
docs: data.doc,
method: 'insert_transactions'
}).then((response) => {
const doc = response.docs ? response.docs[0] : undefined
if (doc && doc.start_date && doc.end_date) {
setDates({
fromDate: doc.start_date,
toDate: doc.end_date,
})
}
toast.success(_("Bank statement imported."))
navigate(`/`)
}).catch(() => {
toast.error(_("There was an error while importing the bank statement."))
})
}
const [progress, setProgress] = useState(0)
useFrappeEventListener("bank-rec-statement-import-progress", (event) => {
setProgress(event.progress)
})
const file_name = data.doc.file.split("/").pop() ?? ""
const { banks } = useGetBankAccounts()
const bank = useMemo(() => {
return banks?.find((bank) => bank.name === data.doc.bank_account)
}, [data.doc.bank_account, banks])
return (
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-4'>
<div className='flex justify-between items-center'>
<Button size='sm' variant='outline' asChild>
<Link to="/statement-importer">
{direction === 'ltr' ? <ChevronLeftIcon /> : <ChevronRightIcon />}
{_("Back")}
</Link>
</Button>
{data.doc.status === 'Completed' ? <Badge theme='green'>{_("Completed")}</Badge> :
<Button onClick={onImport} disabled={loading || data.final_transactions?.length === 0} size='sm' type='button'>
{loading ? <Loader2Icon className='size-4 animate-spin' /> : null}
{loading ? _("Importing...") : _("Import {0} transactions", [data.final_transactions?.length?.toString() || "0"])}</Button>
}
</div>
<div className='flex items-start gap-4'>
<div className='flex flex-col gap-1'>
<H2 className='text-lg border-0 p-0'>{_("Statement Details")}</H2>
<Paragraph className='text-p-sm'><span>
{_("We've auto-detected the details of the statement file.")}
</span><br />
<span>
{_("Please review the details below and click the 'Import' button to proceed.")}
</span>
</Paragraph>
</div>
</div>
{progress > 0 && <div className='flex flex-col gap-2'><Progress value={progress} max={100} size="lg" />
<span className='text-sm'>{_("Importing {0} transactions", [progress.toString()])}
</span>
</div>}
{error && <ErrorBanner error={error} />}
<Table>
<TableBody>
<TableRow>
<TableHead>{_("Bank Account")}</TableHead>
<TableCell>
<div className='flex items-center gap-2'>
<BankLogo bank={bank} />
<span className="tracking-tight text-sm font-medium">{bank?.account_name}</span>
<span title="GL Account" className="text-sm">{bank?.account}</span>
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Statement File")}</TableHead>
<TableCell>
<div className='flex items-center gap-2'>
<FileTypeIcon fileType={getFileExtension(file_name)} size='md' showBackground={false} />
{file_name}
</div>
</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Transaction Dates")}</TableHead>
<TableCell>{_("{0} to {1}", [formatDate(data.doc.start_date, "Do MMMM YYYY"), formatDate(data.doc.end_date, "Do MMMM YYYY")])}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Number of Transactions")}</TableHead>
<TableCell>{data.doc.number_of_transactions}</TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Total Debits")}</TableHead>
<TableCell><span className='font-numeric'>{formatCurrency(flt(data.doc.total_debits, 2), data.currency)}</span> <span className='text-ink-gray-5 font-sans'>({data.doc.total_debit_transactions} {data.doc.total_debit_transactions === 1 ? _("transaction") : _("transactions")})</span></TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Total Credits")}</TableHead>
<TableCell><span className='font-numeric'>{formatCurrency(flt(data.doc.total_credits, 2), data.currency)}</span> <span className='text-ink-gray-5 font-sans'>({data.doc.total_credit_transactions} {data.doc.total_credit_transactions === 1 ? _("transaction") : _("transactions")})</span></TableCell>
</TableRow>
<TableRow>
<TableHead>{_("Closing Balance as of {}", [formatDate(data.doc.end_date, "Do MMMM YYYY")])}</TableHead>
<TableCell className='font-numeric'>{formatCurrency(flt(data.doc.closing_balance, 2), data.currency)}</TableCell>
</TableRow>
<TableRow>
<TableHead>
<div className='flex items-center gap-2'>
{_("Detected Amount Format")} <Tooltip>
<TooltipTrigger><InfoIcon size={16} /></TooltipTrigger>
<TooltipContent>
{_("The amount format detected in the statement file. This is used to parse the deposit and withdrawal values from each row.")}
</TooltipContent>
</Tooltip>
</div>
</TableHead>
<TableCell>{data.doc.detected_amount_format}</TableCell>
</TableRow>
<TableRow>
<TableHead>
<div className='flex items-center gap-2'>
{_("Detected Date Format")}
<Tooltip>
<TooltipTrigger><InfoIcon size={16} /></TooltipTrigger>
<TooltipContent>
{_("The date format detected in the statement file. This is used to parse the date values.")}
</TooltipContent>
</Tooltip>
</div>
</TableHead>
<TableCell>
{dateFormat || data.date_format} (e.g.{" "}
{formatDate(new Date(), dateFormat || "YYYY-MM-DD")})
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
{data.doc.status === "Not Started" ? <>
<ConflictingTransactions transactions={data.conflicting_transactions} />
<Separator />
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-1'>
<H3 className='text-base border-0 p-0'>{_("Preview Transactions")}</H3>
{data.final_transactions?.length === 1 ? (
<Paragraph className='text-p-sm'>{_("We've found 1 transaction in the statement file that will be imported into the system. Please review the details below and click the 'Import' button to proceed.")}</Paragraph>
) : (
<Paragraph className='text-p-sm'>{_("{0} transactions will be imported into the system. Please review the details below and click the 'Import' button to proceed.", [data.final_transactions?.length?.toString() || "0"])}</Paragraph>
)}
</div>
<div className='max-h-[400px] overflow-scroll pb-2'>
<Table>
<TableCaption>{_("Transactions to be imported into the system")}</TableCaption>
<TableHeader>
<TableRow>
<TableHead className='w-8'>#</TableHead>
<TableHead>{_("Date")}</TableHead>
<TableHead>{_("Description")}</TableHead>
<TableHead>{_("Ref.")}</TableHead>
<TableHead className='text-end'>{_("Withdrawal")}</TableHead>
<TableHead className='text-end'>{_("Deposit")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.final_transactions?.map((transaction, index) => (
<TableRow key={index}>
<TableCell className='w-8'>{index + 1}</TableCell>
<TableCell>{formatDate(transaction.date)}</TableCell>
<TableCell className='max-w-[200px] w-fit overflow-hidden text-ellipsis'>{transaction.description}</TableCell>
<TableCell className='max-w-[100px] w-fit overflow-hidden text-ellipsis'>{transaction.reference}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.withdrawal, data.currency)}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.deposit, data.currency)}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</> : null}
</div>
)
}
const ConflictingTransactions = ({ transactions }: { transactions: GetStatementDetailsResponse["conflicting_transactions"] }) => {
if (transactions.length === 0) {
return null
}
return <>
<Alert theme="red">
<AlertCircleIcon />
<AlertTitle>{_("Conflicting Transactions")}</AlertTitle>
<AlertDescription>
{transactions.length === 1 ? _("We've found 1 existing transaction in the system that conflicts with the transactions in the statement file. Are you sure you want to proceed with the import?")
: _("We've found {0} existing transactions in the system that conflict with the transactions in the statement file. Are you sure you want to proceed with the import?", [transactions.length.toString()])}
<div className='py-2'>
<Dialog>
<DialogTrigger asChild>
<Button
size='sm'
type='button'
theme='red'
variant='solid'>
<span>{transactions.length > 1 ? _("View transactions") : _("View transaction")}</span>
</Button>
</DialogTrigger>
<DialogContent className='min-w-7xl'>
<DialogHeader>
<DialogTitle>{_("Conflicting Transactions")}</DialogTitle>
<DialogDescription>
{transactions.length === 1 ? _("We've found 1 existing transaction in the system that conflicts with the transactions in the statement file. Are you sure you want to proceed with the import?")
: _("We've found {0} existing transactions in the system that conflict with the transactions in the statement file. Are you sure you want to proceed with the import?", [transactions.length.toString()])}
</DialogDescription>
</DialogHeader>
<div className='max-h-[400px] overflow-scroll pb-2'>
<Table>
<TableCaption>{_("Existing transactions in the system belonging to the same bank account and date range")}</TableCaption>
<TableHeader>
<TableRow>
<TableHead>{_("Date")}</TableHead>
<TableHead>{_("Description")}</TableHead>
<TableHead>{_("Ref.")}</TableHead>
<TableHead className='text-end'>{_("Withdrawal")}</TableHead>
<TableHead className='text-end'>{_("Deposit")}</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{transactions.map((transaction) => (
<TableRow key={transaction.name}>
<TableCell>{formatDate(transaction.date)}</TableCell>
<TableCell title={transaction.description} className='max-w-[200px] w-fit overflow-hidden text-ellipsis'>{transaction.description}</TableCell>
<TableCell title={transaction.reference_number} className='max-w-[100px] w-fit overflow-hidden text-ellipsis'>{transaction.reference_number ? transaction.reference_number : "-"}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.withdrawal, transaction.currency)}</TableCell>
<TableCell className='text-end font-numeric'>{formatCurrency(transaction.deposit, transaction.currency)}</TableCell>
<TableCell className='text-end'>
<Tooltip>
<TooltipTrigger asChild>
<Button variant='link' isIconButton asChild className='text-ink-gray-5 hover:text-black p-0 h-4'>
<a href={`/desk/bank-transaction/${transaction.name}`} target='_blank' rel='noopener noreferrer'>
<ExternalLinkIcon />
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
{_("Open {0} in a new tab", [transaction.name])}
</TooltipContent>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant={'outline'} size='md' type='button'>{_("Close")}</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</AlertDescription>
</Alert>
</>
}
export default StatementDetails

View File

@@ -1,42 +0,0 @@
import { BankStatementImportLog } from "@/types/Accounts/BankStatementImportLog"
import { useFrappeGetCall } from "frappe-react-sdk"
export interface GetStatementDetailsResponse {
doc: BankStatementImportLog,
conflicting_transactions: Array<{
name: string,
date: string,
withdrawal: number,
deposit: number,
description: string,
reference_number: string,
currency: string,
}>,
final_transactions: Array<{
date: string,
withdrawal: number,
deposit: number,
description: string,
reference: string,
transaction_type?: string,
debit_credit?: string,
included_fee?: number,
excluded_fee?: number,
party_name?: string,
party_account_number?: string,
party_iban?: string,
}>,
date_format: string,
raw_data: Array<Array<string>>,
currency: string,
}
export const useGetStatementDetails = (id: string) => {
return useFrappeGetCall<{ message: GetStatementDetailsResponse }>("erpnext.accounts.doctype.bank_statement_import_log.bank_statement_import_log.get_statement_details", {
statement_import_id: id,
}, undefined, {
revalidateOnFocus: false
})
}

View File

@@ -1,115 +0,0 @@
import { Badge } from '@/components/ui/badge'
import { Kbd, KbdGroup } from '@/components/ui/kbd'
import { KeyboardMetaKeyIcon } from '@/components/ui/keyboard-keys'
import { SettingsPanelDescription, SettingsPanelTitle, SettingsPanelHeader, SettingsPanelContent } from '@/components/ui/settings-dialog'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import _ from '@/lib/translate'
import { ArrowRightLeftIcon, HistoryIcon, LandmarkIcon, ReceiptIcon, SaveIcon, SettingsIcon, ZapIcon } from 'lucide-react'
const Shortcuts = [
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd>B</Kbd></KbdGroup>,
action: {
icon: <LandmarkIcon />,
label: _("Bank Entry"),
description: _("Record a bank journal entry for expenses, income or split transactions")
}
},
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd>P</Kbd></KbdGroup>,
action: {
icon: <ReceiptIcon />,
label: _("Record Payment"),
description: _("Record a payment against a customer or supplier")
}
},
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd>I</Kbd></KbdGroup>,
action: {
icon: <ArrowRightLeftIcon />,
label: _("Transfer"),
description: _("Record a transfer between two bank accounts")
}
},
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd>R</Kbd></KbdGroup>,
action: {
icon: <ZapIcon />,
label: _("Accept Matching Rule"),
description: _("Accept the rule for the selected transaction")
}
},
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd>S</Kbd></KbdGroup>,
action: {
icon: <SaveIcon />,
label: _("Save"),
description: _("Save the currently opened form")
}
},
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd>Z</Kbd></KbdGroup>,
action: {
icon: <HistoryIcon />,
label: _("Reconciliation History"),
description: _("View all reconciliation actions taken in this session")
}
},
{
shortcut: <KbdGroup><Kbd><KeyboardMetaKeyIcon /></Kbd><Kbd></Kbd><Kbd>G</Kbd></KbdGroup>,
action: {
icon: <SettingsIcon />,
label: _("Settings"),
description: _("Open the settings dialog")
}
}
]
const KeyboardShortcuts = () => {
return (
<>
<SettingsPanelHeader>
<SettingsPanelTitle>{_("Keyboard Shortcuts")}</SettingsPanelTitle>
<SettingsPanelDescription>{_("Get around the system quickly with keyboard shortcuts")}</SettingsPanelDescription>
</SettingsPanelHeader>
<SettingsPanelContent>
<div className='flex flex-col gap-3'>
<p className='text-p-sm text-ink-gray-6'>
{_("Transaction actions work when one or more unreconciled transactions are selected.")}
<br />
{_("To select more than one transaction at a time, press and hold the shift key.")}
</p>
<Table containerClassName='dark:border-outline-gray-2'>
<TableHeader>
<TableRow>
<TableHead>{_("Shortcut")}</TableHead>
<TableHead>{_("Action")}</TableHead>
<TableHead>{_("Description")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Shortcuts.map((shortcut) => (
<TableRow className='hover:bg-surface-gray-2'>
<TableCell>
{shortcut.shortcut}
</TableCell>
<TableCell>
<Badge size='lg' variant='outline'>
{shortcut.action.icon}
{shortcut.action.label}
</Badge>
</TableCell>
<TableCell>
<p className='text-p-sm text-ink-gray-6 text-wrap'>{shortcut.action.description}</p>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</SettingsPanelContent>
</>
)
}
export default KeyboardShortcuts

View File

@@ -1,46 +0,0 @@
import { Button } from '@/components/ui/button'
import { SettingsPanelTitle, SettingsPanelHeader, SettingsPanelDescription, SettingsPanelContent } from '@/components/ui/settings-dialog'
import _ from '@/lib/translate'
import { PlusIcon } from 'lucide-react'
import { useState } from 'react'
import RuleList, { RunRulesButton } from './Rules/RuleList'
import CreateNewRule from '../BankReconciliation/Rules/CreateNewRule'
import EditRule from '../BankReconciliation/Rules/EditRule'
const MatchingRules = () => {
const [selectedRule, setSelectedRule] = useState<string | null>(null)
const [isNewRule, setIsNewRule] = useState(false)
if (isNewRule) {
return <CreateNewRule onCreate={() => setIsNewRule(false)} />
}
if (selectedRule) {
return <EditRule onClose={() => setSelectedRule(null)} ruleID={selectedRule} />
}
return (
<>
<SettingsPanelHeader
actions={
<div className='flex gap-2 items-center'>
<RunRulesButton />
<Button type='button' onClick={() => setIsNewRule(true)}><PlusIcon /> {_("Add Rule")}</Button>
</div>
}
>
<SettingsPanelTitle>{_("Transaction Matching Rules")}</SettingsPanelTitle>
<SettingsPanelDescription>
{_("Set up rules to automatically classify transactions. Drag and drop rules to reorder their priority.")}
</SettingsPanelDescription>
</SettingsPanelHeader>
<SettingsPanelContent>
<RuleList setSelectedRule={setSelectedRule} />
</SettingsPanelContent>
</>
)
}
export default MatchingRules

View File

@@ -1,261 +0,0 @@
import ErrorBanner from "@/components/ui/error-banner"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { SettingsPanelDescription, SettingsPanelHeader, SettingsPanelTitle, SettingsPanelContent } from "@/components/ui/settings-dialog"
import { Switch } from "@/components/ui/switch"
import { useTheme } from "@/components/ui/theme-provider"
import _ from "@/lib/translate"
import { AccountsSettings } from "@/types/Accounts/AccountsSettings"
import { useFrappeGetDoc, useFrappeUpdateDoc } from "frappe-react-sdk"
import { toast } from "sonner"
export const Preferences = () => {
const { data: accountsSettings, mutate, error: fetchError, isLoading } = useFrappeGetDoc<AccountsSettings>("Accounts Settings", "Accounts Settings", undefined, {
revalidateOnFocus: false
})
const { updateDoc, error } = useFrappeUpdateDoc<AccountsSettings>()
const onUpdate = (field: keyof AccountsSettings, value: any) => {
mutate(updateDoc("Accounts Settings", "Accounts Settings", {
[field]: value
}), {
optimisticData: {
...accountsSettings as AccountsSettings,
[field]: value
},
revalidate: false,
}).then(() => {
toast.success(_("Preferences updated"), {
dismissible: true,
duration: 500,
})
})
}
return <>
<SettingsPanelHeader>
<SettingsPanelTitle>{_("Preferences")}</SettingsPanelTitle>
<SettingsPanelDescription>{_("Configure settings for the banking module")}</SettingsPanelDescription>
</SettingsPanelHeader>
<SettingsPanelContent>
<div className='flex flex-col gap-4 w-full'>
{fetchError && <ErrorBanner error={fetchError} />}
{error && <ErrorBanner error={error} />}
<div className="flex flex-col flex-1">
<ThemeSwitcher />
<div className="flex justify-between items-center gap-8 py-3">
<div className="flex flex-col">
<Label htmlFor="transfer_match_days" className="text-p-base text-ink-gray-6">{_("Number of days to match transfers")}</Label>
<p className="text-p-sm text-ink-gray-5">
{_("For example, if set to 4, the system will try to find matching transfer transactions in other banks 4 days before and after the transaction date. This is because transactions can clear on different days on different bank accounts.")}
</p>
</div>
<div className="min-w-40 flex justify-end">
<Select disabled={isLoading} onValueChange={(value) => onUpdate("transfer_match_days", Number(value))} value={accountsSettings?.transfer_match_days?.toString()}>
<SelectTrigger id="transfer_match_days" className="min-w-32">
<SelectValue placeholder={_("Select number of days")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">{_("Same day")}</SelectItem>
<SelectItem value="1">{_("Within 1 day")}</SelectItem>
<SelectItem value="2">{_("Within 2 days")}</SelectItem>
<SelectItem value="3">{_("Within 3 days")}</SelectItem>
<SelectItem value="4">{_("Within 4 days")}</SelectItem>
<SelectItem value="5">{_("Within 5 days")}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Separator />
<div className="flex justify-between items-center gap-8 py-3">
<div className="flex flex-col">
<Label htmlFor="automatically_run_rules_on_unreconciled_transactions" className="text-p-base text-ink-gray-6">{_("Automatically run rules on unreconciled transactions")}</Label>
<p className="text-p-sm text-ink-gray-5">
{_("This will automatically run transaction matching rules on unreconciled transactions every hour.")}
</p>
</div>
<div className="flex justify-end">
<Switch
id="automatically_run_rules_on_unreconciled_transactions"
className="dark:disabled:bg-surface-gray-2"
disabled={isLoading}
checked={accountsSettings?.automatically_run_rules_on_unreconciled_transactions === 1}
onCheckedChange={(checked) => onUpdate("automatically_run_rules_on_unreconciled_transactions", checked ? 1 : 0)}
/>
</div>
</div>
<Separator />
<div className="flex justify-between items-center gap-8 py-3">
<div className="flex flex-col">
<Label htmlFor="enable_party_matching" className="text-p-base text-ink-gray-6">{_("Enable automatic party matching")}</Label>
<p className="text-p-sm text-ink-gray-5">
{_("The system will attempt to automatically match a party to a bank transaction based on account number or IBAN.")}
</p>
</div>
<div className="flex justify-end">
<Switch
id="enable_party_matching"
className="dark:disabled:bg-surface-gray-2"
disabled={isLoading}
checked={accountsSettings?.enable_party_matching === 1}
onCheckedChange={(checked) => onUpdate("enable_party_matching", checked ? 1 : 0)}
/>
</div>
</div>
<Separator />
<div className="flex justify-between items-center gap-8 py-3">
<div className="flex flex-col">
<Label htmlFor="enable_fuzzy_matching" className="text-p-base text-ink-gray-6">{_("Enable party name/description fuzzy matching")}</Label>
<p className="text-p-sm text-ink-gray-5">
{_("If a party cannot be matched by account number or IBAN, the system will try fuzzy matching using the party name and transaction description.")}
</p>
</div>
<div className="flex justify-end">
<Switch
id="enable_fuzzy_matching"
className="dark:disabled:bg-surface-gray-2"
disabled={accountsSettings?.enable_party_matching !== 1 || isLoading}
checked={accountsSettings?.enable_fuzzy_matching === 1}
onCheckedChange={(checked) => onUpdate("enable_fuzzy_matching", checked ? 1 : 0)}
/>
</div>
</div>
</div>
{/* <DataField
name='transfer_match_days'
label={_("Number of days to match transfers")}
isRequired
inputProps={{
type: 'number',
inputMode: 'numeric',
}}
formDescription={_("For example, if set to 4, the system will try to find matching transactions in other banks 4 days before and after the transaction date. This is because transactions can clear on different days on different bank accounts.")}
/> */}
</div>
</SettingsPanelContent>
</>
}
const ThemeSwitcher = () => {
const { theme, setTheme } = useTheme()
const themeCards: Array<{ value: "Light" | "Dark" | "Automatic", label: string }> = [
{
value: "Light",
label: _("Light"),
},
{
value: "Dark",
label: _("Dark"),
},
{
value: "Automatic",
label: _("System"),
},
]
return <div className="flex flex-col gap-3 pb-3">
<div className="flex flex-col">
<Label className="text-p-base text-ink-gray-6">{_("Theme")}</Label>
<p className="text-p-sm text-ink-gray-5">
{_("Switch between light, dark, or system theme")}
</p>
</div>
<div className="flex gap-3">
{themeCards.map((option) => {
const selected = theme === option.value
return (
<button
key={option.value}
type="button"
onClick={() => setTheme(option.value)}
aria-pressed={selected}
className={`flex-1 basis-0 min-w-0 overflow-hidden rounded-lg border cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-outline-blue-4 ${selected ? "border-outline-gray-5" : "border-outline-gray-modals hover:border-outline-gray-4"}`}
>
{option.value === "Automatic" ? (
<div className="flex w-full min-w-0">
<ThemePreviewWindow theme="light" roundedClass="rounded-tl-[10.5px]" />
<ThemePreviewWindow theme="dark" roundedClass="rounded-tr-[10.5px]" />
</div>
) : (
<ThemePreviewWindow theme={option.value === "Light" ? "light" : "dark"} roundedClass="rounded-t-[10.5px]" />
)}
<div className="flex items-center justify-between px-3 py-2 border-t border-outline-gray-modals">
<div className="text-base text-ink-gray-7">{option.label}</div>
<span className={`rounded-full size-3.5 ${selected ? "border-4 border-outline-gray-5" : "border border-outline-gray-4"}`} />
</div>
</button>
)
})}
</div>
</div>
}
const ThemePreviewWindow = ({ theme, roundedClass }: { theme: "light" | "dark", roundedClass: string }) => {
const isLight = theme === "light"
const frameClass = isLight ? "bg-white border-gray-100" : "bg-gray-900 border-gray-800"
const subtleSurfaceClass = isLight ? "bg-gray-50" : "bg-gray-800"
const mutedLineClass = isLight ? "bg-gray-200" : "bg-gray-700"
const mutedLineStrongClass = isLight ? "bg-gray-300" : "bg-gray-600"
const dividerClass = isLight ? "border-gray-100" : "border-gray-800"
const cardClass = isLight ? "bg-white border-gray-200" : "bg-gray-900 border-gray-700"
return <div className={`flex flex-1 min-w-0 pl-5 pt-3.5 ${isLight ? "bg-surface-gray-2" : "bg-surface-gray-3"} ${roundedClass}`}>
<div className={`w-full rounded-tl-sm border ${frameClass}`}>
<div className={`flex gap-[3px] py-[3px] px-1 border-b ${dividerClass}`}>
<div className="size-1.5 bg-[#FF5F57] rounded-full" />
<div className="size-1.5 bg-[#FEBC2D] rounded-full" />
<div className="size-1.5 bg-[#28C840] rounded-full" />
</div>
<div className="p-1.5">
<div className={`flex items-center gap-1.5 p-1 rounded-sm border ${subtleSurfaceClass} ${dividerClass}`}>
<div className={`h-2 w-8 rounded-full ${mutedLineStrongClass}`} />
<div className={`h-2 w-6 rounded-full ${mutedLineClass}`} />
<div className={`h-2 w-7 rounded-full ml-auto ${mutedLineClass}`} />
</div>
<div className="grid grid-cols-2 gap-1 mt-1.5">
<div className={`rounded-sm border p-1 ${cardClass}`}>
<div className={`h-1.5 w-full rounded-full ${mutedLineStrongClass}`} />
<div className={`h-1.5 w-4/5 rounded-full mt-1 ${mutedLineClass}`} />
<div className={`h-1.5 w-3/5 rounded-full mt-1 ${mutedLineClass}`} />
</div>
<div className={`rounded-sm border p-1 ${cardClass}`}>
<div className="flex items-center justify-between gap-1">
<div className={`h-1.5 w-2/5 rounded-full ${mutedLineStrongClass}`} />
{/* <div className={`h-2.5 w-5 rounded-sm border ${chipClass}`} /> */}
</div>
<div className={`h-1.5 w-full rounded-full mt-1 ${mutedLineClass}`} />
<div className={`h-1.5 w-3/4 rounded-full mt-1 ${mutedLineClass}`} />
</div>
</div>
</div>
</div>
</div>
}

View File

@@ -1,314 +0,0 @@
import { Button } from "@/components/ui/button"
import ErrorBanner from "@/components/ui/error-banner"
import { Skeleton } from "@/components/ui/skeleton"
import { Badge } from "@/components/ui/badge"
import _ from "@/lib/translate"
import { BankTransactionRule } from "@/types/Accounts/BankTransactionRule"
import { FrappeConfig, FrappeContext, useFrappeGetCall, useFrappeGetDocList, useFrappePostCall } from "frappe-react-sdk"
import { ArrowDownRight, ArrowDownUp, ArrowUpRight, MoreVertical, Trash2, GripVertical, Play, RefreshCw, ZapIcon, CalendarSyncIcon } from "lucide-react"
import { useContext, useState } from "react"
import { toast } from "sonner"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuCheckboxItem } from "@/components/ui/dropdown-menu"
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import {
useSortable,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"
import { cn } from "@/lib/utils"
const useGetRuleList = () => {
return useFrappeGetDocList<BankTransactionRule>("Bank Transaction Rule", {
fields: ["name", "rule_name", "rule_description", "transaction_type", "priority"],
orderBy: {
field: 'priority',
order: 'asc'
},
limit: 100
})
}
export const RunRulesButton = () => {
const { data } = useGetRuleList()
const { call: runRuleEvaluation, loading: isRunningRules } = useFrappePostCall('erpnext.accounts.doctype.bank_transaction_rule.bank_transaction_rule.run_rule_evaluation')
const handleRunRules = async (forceEvaluate: boolean = false) => {
try {
await runRuleEvaluation({
force_evaluate: forceEvaluate
})
toast.success(forceEvaluate ? _("Rules evaluation started") : _("Rules evaluation completed"))
} catch (error) {
toast.error(_("Failed to run rules evaluation"))
console.error("Error running rules evaluation:", error)
}
}
if (!data || data.length === 0) {
return null
}
return <DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" disabled={isRunningRules}>
{isRunningRules ? (
<RefreshCw className="animate-spin" />
) : (
<Play />
)}
{isRunningRules ? _("Running...") : _("Run Rules")}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem onClick={() => handleRunRules(false)} disabled={isRunningRules} title={_("Run rules on unreconciled transactions that haven't been evaluated yet")}>
<Play />
{_("Run on new transactions")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleRunRules(true)} disabled={isRunningRules} title={_("Force re-evaluate all unreconciled transactions, even if they were previously evaluated")}>
<RefreshCw />
{_("Force evaluate all")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<AutoRunRuleItem />
</DropdownMenuContent>
</DropdownMenu>
}
const AutoRunRuleItem = () => {
const { db } = useContext(FrappeContext) as FrappeConfig
const { data: accountsSetting, mutate: setAutomaticallyRunRulesOnUnreconciledTransactions } = useFrappeGetCall("frappe.client.get_single_value", {
"doctype": "Accounts Settings",
"field": "automatically_run_rules_on_unreconciled_transactions"
})
const automaticallyRunRulesOnUnreconciledTransactions = accountsSetting?.message ? true : false
const onAutoClassifyTransactions = (checked: boolean) => {
toast.promise(db.setValue("Accounts Settings", "Accounts Settings", "automatically_run_rules_on_unreconciled_transactions", checked ? 1 : 0).then(() => {
setAutomaticallyRunRulesOnUnreconciledTransactions({
message: {
automatically_run_rules_on_unreconciled_transactions: checked ? 1 : 0,
}
}, {
revalidate: false
})
}), {
loading: _("Updating..."),
success: checked ? _("Scheduled job enabled. Transactions will be auto classified.") : _("Scheduled job disabled. Transactions will not be auto classified."),
error: _("Failed to update auto classify transactions settings")
})
}
return <DropdownMenuCheckboxItem
checked={automaticallyRunRulesOnUnreconciledTransactions}
onCheckedChange={onAutoClassifyTransactions}>
<CalendarSyncIcon />
{_("Run rules automatically")}
</DropdownMenuCheckboxItem>
}
const RuleList = ({ setSelectedRule }: { setSelectedRule: (rule: string) => void }) => {
const { data, error, isLoading, mutate } = useGetRuleList()
const { db } = useContext(FrappeContext) as FrappeConfig
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const onDeleteRule = (ruleID: string) => {
toast.promise(db.deleteDoc("Bank Transaction Rule", ruleID).then(() => {
mutate()
}), {
loading: _("Deleting rule..."),
success: _("Rule deleted."),
error: _("Failed to delete rule.")
})
}
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event
if (active.id !== over?.id && data) {
const oldIndex = data.findIndex((rule) => rule.name === active.id)
const newIndex = data.findIndex((rule) => rule.name === over?.id)
const newData = arrayMove(data, oldIndex, newIndex)
// Update priorities based on new order
const updatePromises = newData.map((rule, index) => {
const newPriority = index + 1
if (rule.priority !== newPriority) {
return db.setValue("Bank Transaction Rule", rule.name, "priority", newPriority)
}
return Promise.resolve()
})
try {
await Promise.all(updatePromises)
toast.success(_("Rule priorities updated"))
mutate() // Refresh the data
} catch (error) {
toast.error(_("Failed to update rule priorities"))
console.error("Error updating priorities:", error)
}
}
}
return (
<>
<div className="overflow-y-auto">
{isLoading && <div className="flex flex-col gap-2">
<Skeleton className="w-full h-10" />
<Skeleton className="w-full h-10" />
<Skeleton className="w-full h-10" />
<Skeleton className="w-full h-10" />
<Skeleton className="w-full h-10" />
</div>}
{error && <ErrorBanner error={error} />}
{data && data.length === 0 && <Empty className="h-96">
<EmptyMedia>
<ZapIcon />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{_("No rules setup yet")}</EmptyTitle>
<EmptyDescription>{_("Configure rules to save time when reconciling transactions.")}</EmptyDescription>
</EmptyHeader>
</Empty>}
{data && data.length > 0 && (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={data.map(rule => rule.name)}
strategy={verticalListSortingStrategy}
>
<ul className="space-2 divide-y divide-outline-gray-modals">
{data?.map((rule) => (
<SortableRuleItem
key={rule.name}
rule={rule}
setSelectedRule={setSelectedRule}
onDeleteRule={onDeleteRule}
/>
))}
</ul>
</SortableContext>
</DndContext>
)}
</div>
</>
)
}
const SortableRuleItem = ({
rule,
setSelectedRule,
onDeleteRule
}: {
rule: BankTransactionRule
setSelectedRule: (rule: string) => void
onDeleteRule: (ruleID: string) => void
}) => {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: rule.name })
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
}
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
return (
<li ref={setNodeRef} style={style}>
<div className={cn("flex justify-between items-center py-2 my-0.5 h-full hover:bg-surface-gray-1 pe-2 rounded", isDropdownOpen && "bg-surface-gray-1")}>
<div className="flex items-center gap-2">
<div
{...attributes}
{...listeners}
className="cursor-grab active:cursor-grabbing p-1 rounded"
title={_("Drag to reorder")}
>
<GripVertical className="w-4 h-4 text-ink-gray-5" />
</div>
<Badge theme="gray" className="font-numeric tabular-nums">
{rule.priority}
</Badge>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Button
variant='link'
size='sm'
className="p-0 h-fit text-start cursor-pointer no-underline hover:underline"
onClick={() => setSelectedRule(rule.name)}>
{rule.rule_name}
</Button>
<div title={rule.transaction_type === "Any" ? _("Applies to withdrawals and deposits") : rule.transaction_type === "Withdrawal" ? _("Applies to withdrawals") : _("Applies to deposits")}>
{rule.transaction_type === "Any" ? <ArrowDownUp className="text-ink-gray-5 w-4 h-4" /> : rule.transaction_type === "Withdrawal" ? <ArrowUpRight className="text-ink-red-3 w-5 h-5" /> : <ArrowDownRight className="text-ink-green-3 w-5 h-5" />}
</div>
</div>
<span className="text-sm text-ink-gray-5">
{rule.rule_description}
</span>
</div>
</div>
<div className="flex items-center gap-2 h-full justify-center">
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant='ghost' isIconButton className="hover:bg-transparent">
<MoreVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onDeleteRule(rule.name)}>
<Trash2 />
{_("Delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</li>
)
}
export default RuleList

View File

@@ -1,95 +0,0 @@
import { Button } from '@/components/ui/button'
import { Dialog, DialogTrigger } from '@/components/ui/dialog'
import {
SettingsDialog,
SettingsPanel,
SettingsPanels,
SettingsTabGroup,
SettingsTabItem,
SettingsTabs,
} from '@/components/ui/settings-dialog'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import _ from '@/lib/translate'
import { KeyboardIcon, SettingsIcon, SlidersVerticalIcon, ZapIcon } from 'lucide-react'
import { useState } from 'react'
import { Preferences } from './Preferences'
import MatchingRules from './MatchingRules'
import KeyboardShortcuts from './KeyboardShortcuts'
import { useHotkeys } from 'react-hotkeys-hook'
const Settings = () => {
const [isOpen, setIsOpen] = useState(false)
useHotkeys('shift+meta+g', () => {
setIsOpen(x => !x)
}, {
enabled: true,
preventDefault: true,
enableOnFormTags: false
})
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DialogTrigger asChild>
<Button variant={'outline'} isIconButton size='md'>
<SettingsIcon />
</Button>
</DialogTrigger>
</TooltipTrigger>
<TooltipContent>
{_("Settings")}
</TooltipContent>
</Tooltip>
<SettingsDialog defaultValue="preferences" onClose={() => setIsOpen(false)}>
<SettingsTabs>
<SettingsTabGroup header={_("Settings")}>
<SettingsTabItem
icon={<SlidersVerticalIcon />}
label={_("Preferences")}
value="preferences"
/>
<SettingsTabItem
icon={<ZapIcon />}
label={_("Matching Rules")}
value="rules"
/>
{/* <SettingsTabItem
icon={<LandmarkIcon />}
label={_("Bank Accounts")}
value="bank-accounts"
/>
<SettingsTabItem
icon={<ListIcon />}
label={_("Masters")}
value="masters"
/> */}
<SettingsTabItem
icon={<KeyboardIcon />}
label={_("Keyboard Shortcuts")}
value="keyboard-shortcuts"
/>
</SettingsTabGroup>
</SettingsTabs>
<SettingsPanels>
<SettingsPanel value="preferences">
<Preferences />
</SettingsPanel>
<SettingsPanel value="rules">
<MatchingRules />
</SettingsPanel>
<SettingsPanel value="bank-accounts" />
<SettingsPanel value="masters" />
<SettingsPanel value="keyboard-shortcuts">
<KeyboardShortcuts />
</SettingsPanel>
</SettingsPanels>
</SettingsDialog>
</Dialog >
)
}
export default Settings

View File

@@ -1,196 +0,0 @@
import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black-200 dark:bg-black-700",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
size = "default",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
size?: "default" | "sm"
}) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
data-size={size}
className={cn(
"bg-surface-modal shadow-xl rounded-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 p-6 duration-200 data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn(
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-start sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
className
)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn(
"text-2xl leading-6 text-ink-gray-8 font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
className
)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-ink-gray-7 text-p-base", className)}
{...props}
/>
)
}
function AlertDialogMedia({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn(
"bg-surface-gray-1 mb-2 inline-flex size-16 items-center justify-center rounded-md sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
className
)}
{...props}
/>
)
}
function AlertDialogAction({
className,
variant = "solid",
size = "md",
theme = "red",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size" | "theme">) {
return (
<Button variant={variant} size={size} theme={theme} asChild>
<AlertDialogPrimitive.Action
data-slot="alert-dialog-action"
className={cn(className)}
{...props}
/>
</Button>
)
}
function AlertDialogCancel({
className,
variant = "outline",
size = "md",
theme = "gray",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size" | "theme">) {
return (
<Button variant={variant} size={size} asChild>
<AlertDialogPrimitive.Cancel
data-slot="alert-dialog-cancel"
className={cn(className)}
{...props}
/>
</Button>
)
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
}

View File

@@ -1,104 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3.5 text-base grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-1 [&>svg]:text-current",
{
variants: {
variant: {
subtle: "bg-surface-white",
outline: "border border-outline-gray-3",
},
theme: {
gray: "text-ink-gray-8",
blue: "text-ink-blue-3",
green: "text-ink-green-3",
red: "text-ink-red-3",
amber: "text-ink-amber-3",
}
},
compoundVariants: [
// Subtle alerts
{
theme: "gray",
variant: "subtle",
className: "bg-surface-gray-2 border-outline-gray-1"
},
{
theme: "blue",
variant: "subtle",
className: "bg-surface-blue-2 border-surface-blue-2"
},
{
theme: "green",
variant: "subtle",
className: "bg-surface-green-2 border-surface-green-2"
},
{
theme: "red",
variant: "subtle",
className: "bg-surface-red-2 border-surface-red-2"
},
{
theme: "amber",
variant: "subtle",
className: "bg-surface-amber-2 border-surface-amber-2"
}
],
defaultVariants: {
variant: "subtle",
theme: "gray",
},
}
)
export type AlertProps = React.ComponentProps<"div"> & VariantProps<typeof alertVariants>
function Alert({
className,
variant,
theme,
...props
}: AlertProps) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant, theme }), className)}
{...props}
/>
)
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 min-h-4 text-ink-gray-8 font-medium text-p-base",
className
)}
{...props}
/>
)
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-ink-gray-6 col-start-2 grid justify-items-start gap-1 text-p-base",
className
)}
{...props}
/>
)
}
export { Alert, AlertTitle, AlertDescription }

View File

@@ -1,188 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center select-none rounded-full whitespace-nowrap gap-1 w-fit shrink-0 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
solid: "",
subtle: "",
outline: "bg-transparent border",
ghost: "bg-transparent",
},
size: {
sm: 'h-4 text-xs px-1.5 [&>svg]:size-2.5',
md: 'h-5 text-xs px-1.5 [&>svg]:size-3',
lg: 'h-6 text-sm px-2 [&>svg]:size-3',
},
theme: {
gray: "",
blue: "",
green: "",
red: "",
orange: "",
violet: "",
}
},
compoundVariants: [
// Solid badges
{
variant: "solid",
theme: "gray",
className: "text-ink-white bg-surface-gray-7 [a&]:hover:bg-surface-gray-8"
},
{
variant: "solid",
theme: "blue",
className: "text-ink-blue-1 bg-surface-blue-5 [a&]:hover:bg-surface-blue-6"
},
{
variant: "solid",
theme: "green",
className: "text-ink-green-1 bg-surface-green-5 [a&]:hover:bg-surface-green-6"
},
{
variant: "solid",
theme: "orange",
className: "text-ink-amber-1 bg-surface-amber-5 [a&]:hover:bg-surface-amber-6"
},
{
variant: "solid",
theme: "red",
className: "text-ink-red-1 bg-surface-red-5 [a&]:hover:bg-surface-red-6"
},
{
variant: "solid",
theme: "violet",
className: "text-ink-violet-1 bg-surface-violet-5 [a&]:hover:bg-surface-violet-6"
},
// Subtle badge
{
variant: "subtle",
theme: "gray",
className: "text-ink-gray-6 bg-surface-gray-2 [a&]:hover:bg-surface-gray-3"
},
{
variant: "subtle",
theme: "blue",
className: "text-ink-blue-4 bg-surface-blue-2 [a&]:hover:bg-surface-blue-3"
},
{
variant: "subtle",
theme: "green",
className: "text-ink-green-4 bg-surface-green-2 [a&]:hover:bg-surface-green-3"
},
{
variant: "subtle",
theme: "orange",
className: "text-ink-amber-4 bg-surface-amber-2 [a&]:hover:bg-surface-amber-3"
},
{
variant: "subtle",
theme: "red",
className: "text-ink-red-4 bg-surface-red-2 [a&]:hover:bg-surface-red-3"
},
{
variant: "subtle",
theme: "violet",
className: "text-ink-violet-4 bg-surface-violet-2 [a&]:hover:bg-surface-violet-3"
},
// Outline badge
{
variant: "outline",
theme: "gray",
className: "text-ink-gray-6 border-outline-gray-2 [a&]:hover:bg-surface-gray-2"
},
{
variant: "outline",
theme: "blue",
className: "text-ink-blue-4 border-outline-blue-2 [a&]:hover:bg-surface-blue-2"
},
{
variant: "outline",
theme: "green",
className: "text-ink-green-4 border-outline-green-2 [a&]:hover:bg-surface-green-2"
},
{
variant: "outline",
theme: "orange",
className: "text-ink-amber-4 border-outline-amber-2 [a&]:hover:bg-surface-amber-2"
},
{
variant: "outline",
theme: "red",
className: "text-ink-red-4 border-outline-red-2 [a&]:hover:bg-surface-red-2"
},
{
variant: "outline",
theme: "violet",
className: "text-ink-violet-4 border-outline-violet-2 [a&]:hover:bg-surface-violet-2"
},
// Ghost badge
{
variant: "ghost",
theme: "gray",
className: "text-ink-gray-6"
},
{
variant: "ghost",
theme: "blue",
className: "text-ink-blue-4"
},
{
variant: "ghost",
theme: "green",
className: "text-ink-green-4"
},
{
variant: "ghost",
theme: "orange",
className: "text-ink-amber-4"
},
{
variant: "ghost",
theme: "red",
className: "text-ink-red-4"
},
{
variant: "ghost",
theme: "violet",
className: "text-ink-violet-4"
}
],
defaultVariants: {
variant: "subtle",
size: "md",
theme: "gray",
},
}
)
function Badge({
className,
variant = "subtle",
size = "md",
theme = "gray",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
data-size={size}
data-theme={theme}
className={cn(badgeVariants({ variant, size, theme }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -1,109 +0,0 @@
import * as React from "react"
import { MoreHorizontal } from "lucide-react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-ink-gray-5 flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("text-ink-gray-5 font-medium text-lg hover:text-ink-gray-7 active:text-ink-gray-7 transition-colors", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-ink-gray-8 text-lg font-medium text-balance tracking-wide", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <span className="text-ink-gray-4 text-base">/</span>}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@@ -1,263 +0,0 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap transition-all disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none aria-invalid:shadow-focus-red aria-invalid:border-outline-red-3",
{
variants: {
variant: {
solid: "text-ink-white",
subtle: "",
ghost: "bg-transparent",
outline: "bg-surface-white border",
link: "bg-transparent underline-offset-4 underline",
},
size: {
sm: "h-7 text-base px-2 rounded [&_svg:not([class*='size-'])]:size-4",
md: "h-8 text-base font-medium px-2.5 rounded [&_svg:not([class*='size-'])]:size-4.5",
lg: "h-10 text-lg font-medium px-3 rounded-md [&_svg:not([class*='size-'])]:size-5",
xl: "h-11.5 text-xl font-medium px-3.5 rounded-lg [&_svg:not([class*='size-'])]:size-6",
"2xl": "h-13 text-2xl font-medium px-3.5 rounded-xl [&_svg:not([class*='size-'])]:size-6",
},
theme: {
gray: "focus-visible:shadow-focus-gray",
blue: "focus-visible:shadow-focus-blue",
green: "focus-visible:shadow-focus-green",
red: "focus-visible:shadow-focus-red",
amber: "focus-visible:shadow-focus-amber",
violet: "focus-visible:shadow-focus-violet",
},
isIconButton: {
true: "px-0",
false: ""
}
},
compoundVariants: [
// Icon only buttons - Sizes
{
isIconButton: true,
size: "sm",
className: "size-7"
},
{
isIconButton: true,
size: "md",
className: "size-8"
},
{
isIconButton: true,
size: "lg",
className: "size-10"
},
{
isIconButton: true,
size: "xl",
className: "size-11.5"
},
{
isIconButton: true,
size: "2xl",
className: "size-13"
},
// Solid buttons
{
variant: "solid",
theme: "gray",
className: "bg-surface-gray-7 hover:bg-surface-gray-6 active:bg-surface-gray-5 disabled:bg-surface-gray-2 disabled:text-ink-gray-4"
},
{
variant: "solid",
theme: "blue",
className: "bg-surface-blue-5 text-ink-blue-1 hover:bg-surface-blue-6 active:bg-surface-blue-7 disabled:bg-surface-blue-2 disabled:text-ink-blue-2"
},
{
variant: "solid",
theme: "green",
className: "bg-surface-green-5 text-ink-green-1 hover:bg-surface-green-6 active:bg-surface-green-7 disabled:bg-surface-green-2 disabled:text-ink-green-2"
},
{
variant: "solid",
theme: "red",
className: "bg-surface-red-5 text-ink-red-1 hover:bg-surface-red-6 active:bg-surface-red-7 disabled:bg-surface-red-2 disabled:text-ink-red-2"
},
{
variant: "solid",
theme: "violet",
className: "bg-surface-violet-5 text-ink-violet-1 hover:bg-surface-violet-6 active:bg-surface-violet-7 disabled:bg-surface-violet-2 disabled:text-ink-violet-2"
},
{
variant: "solid",
theme: "amber",
className: "bg-surface-amber-5 text-ink-amber-1 hover:bg-surface-amber-6 active:bg-surface-amber-7 disabled:bg-surface-amber-2 disabled:text-ink-amber-2"
},
// Subtle Buttons
{
variant: "subtle",
theme: "gray",
className: "text-ink-gray-7 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 disabled:bg-surface-gray-2 disabled:text-ink-gray-4"
},
{
variant: "subtle",
theme: "blue",
className: "text-ink-blue-4 bg-surface-blue-2 hover:bg-surface-blue-3 active:bg-surface-blue-4 disabled:bg-surface-blue-2 disabled:text-ink-blue-2"
},
{
variant: "subtle",
theme: "green",
className: "text-ink-green-4 bg-surface-green-2 hover:bg-surface-green-3 active:bg-surface-green-4 disabled:bg-surface-green-2 disabled:text-ink-green-2"
},
{
variant: "subtle",
theme: "red",
className: "text-ink-red-4 bg-surface-red-2 hover:bg-surface-red-3 active:bg-surface-red-4 disabled:bg-surface-red-2 disabled:text-ink-red-2"
},
{
variant: "subtle",
theme: "violet",
className: "text-ink-violet-4 bg-surface-violet-2 hover:bg-surface-violet-3 active:bg-surface-violet-4 disabled:bg-surface-violet-2 disabled:text-ink-violet-2"
},
{
variant: "subtle",
theme: "amber",
className: "text-ink-amber-4 bg-surface-amber-2 hover:bg-surface-amber-3 active:bg-surface-amber-4 disabled:bg-surface-amber-2 disabled:text-ink-amber-2"
},
// Outline buttons
{
variant: "outline",
theme: "gray",
className:
"text-ink-gray-7 border-outline-gray-2 hover:border-outline-gray-3 active:border-outline-gray-4 active:bg-surface-gray-4 disabled:bg-surface-gray-2 disabled:text-ink-gray-4 disabled:border-outline-gray-2"
},
{
variant: "outline",
theme: "blue",
className:
"text-ink-blue-4 border-outline-blue-2 hover:border-outline-blue-3 active:border-outline-blue-4 active:bg-surface-blue-4 disabled:bg-surface-blue-2 disabled:text-ink-blue-2 disabled:border-outline-blue-2"
},
{
variant: "outline",
theme: "green",
className:
"text-ink-green-4 border-outline-green-2 hover:border-outline-green-3 active:border-outline-green-4 active:bg-surface-green-4 disabled:bg-surface-green-2 disabled:text-ink-green-2 disabled:border-outline-green-2"
},
{
variant: "outline",
theme: "red",
className:
"text-ink-red-4 border-outline-red-2 hover:border-outline-red-3 active:border-outline-red-4 active:bg-surface-red-4 disabled:bg-surface-red-2 disabled:text-ink-red-2 disabled:border-outline-red-2"
},
{
variant: "outline",
theme: "violet",
className: "text-ink-violet-4 border-outline-violet-2 hover:border-outline-violet-3 active:border-outline-violet-4 active:bg-surface-violet-4 disabled:bg-surface-violet-2 disabled:text-ink-violet-2 disabled:border-outline-violet-2"
},
{
variant: "outline",
theme: "amber",
className: "text-ink-amber-4 border-outline-amber-2 hover:border-outline-amber-3 active:border-outline-amber-4 active:bg-surface-amber-4 disabled:bg-surface-amber-2 disabled:text-ink-amber-2 disabled:border-outline-amber-2"
},
// Ghost buttons
{
variant: "ghost",
theme: "gray",
className:
"text-ink-gray-7 hover:bg-surface-gray-3 active:bg-surface-gray-4 disabled:text-ink-gray-4"
},
{
variant: "ghost",
theme: "blue",
className:
"text-ink-blue-4 hover:bg-surface-blue-3 active:bg-surface-blue-4 disabled:text-ink-blue-2"
},
{
variant: "ghost",
theme: "green",
className:
"text-ink-green-4 hover:bg-surface-green-3 active:bg-surface-green-4 disabled:text-ink-green-2"
},
{
variant: "ghost",
theme: "red",
className:
"text-ink-red-4 hover:bg-surface-red-3 active:bg-surface-red-4 disabled:text-ink-red-2"
},
{
variant: "ghost",
theme: "violet",
className: "text-ink-violet-4 hover:bg-surface-violet-3 active:bg-surface-violet-4 disabled:text-ink-violet-2"
},
{
variant: "ghost",
theme: "amber",
className: "text-ink-amber-4 hover:bg-surface-amber-3 active:bg-surface-amber-4 disabled:text-ink-amber-2"
},
//Link buttons
{
variant: "link",
theme: "gray",
className: "text-ink-gray-8 hover:text-ink-gray-8 active:text-ink-gray-8 disabled:text-ink-gray-4"
},
{
variant: "link",
theme: "blue",
className: "text-ink-blue-3 hover:text-ink-blue-4 active:text-ink-blue-4 disabled:text-ink-blue-link"
},
{
variant: "link",
theme: "green",
className: "text-ink-green-3 hover:text-ink-green-4 active:text-ink-green-4 disabled:text-ink-green-2"
},
{
variant: "link",
theme: "red",
className: "text-ink-red-3 hover:text-ink-red-4 active:text-red-4 disabled:text-ink-red-2"
},
{
variant: "link",
theme: "violet",
className: "text-ink-violet-3 hover:text-ink-violet-4 active:text-ink-violet-4 disabled:text-ink-violet-2"
},
{
variant: "link",
theme: "amber",
className: "text-ink-amber-3 hover:text-ink-amber-4 active:text-ink-amber-4 disabled:text-ink-amber-2"
}
],
defaultVariants: {
variant: "solid",
size: "sm",
theme: "gray",
},
}
)
function Button({
className,
variant = "solid",
size = "sm",
theme = "gray",
isIconButton = false,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
data-theme={theme}
className={cn(buttonVariants({ variant, size, theme, className, isIconButton }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@@ -1,218 +0,0 @@
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-surface-modal group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-outline-gray-1 border border-outline-gray-2 shadow-xs has-focus:ring-outline-gray-1/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-surface-modal inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md ps-2 pe-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-ink-gray-5 [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-ink-gray-5 rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-ink-gray-5",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-e-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-s-md"
: "[&:first-child[data-selected=true]_button]:rounded-s-md",
defaultClassNames.day
),
range_start: cn(
"rounded-s-md bg-surface-gray-1",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-e-md bg-surface-gray-1", defaultClassNames.range_end),
today: cn(
"bg-surface-gray-1 text-ink-gray-8 rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-ink-gray-5 aria-selected:text-ink-gray-5",
defaultClassNames.outside
),
disabled: cn(
"text-ink-gray-5 opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
isIconButton
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-surface-gray-7 data-[selected-single=true]:text-ink-white data-[range-middle=true]:bg-surface-gray-1 data-[range-middle=true]:text-ink-gray-8 data-[range-start=true]:bg-surface-gray-7 data-[range-start=true]:text-ink-white data-[range-end=true]:bg-surface-gray-7 data-[range-end=true]:text-ink-white group-data-[focused=true]/day:border-outline-gray-1 group-data-[focused=true]/day:ring-outline-gray-1/50 dark:hover:text-ink-gray-8 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-e-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-s-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }

View File

@@ -1,92 +0,0 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-surface-cards text-ink-gray-8 flex flex-col gap-6 rounded-xl border py-6 shadow-xs",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-ink-gray-5 text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -1,44 +0,0 @@
import * as React from "react"
import { CheckIcon } from "lucide-react"
import { Checkbox as CheckboxPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Checkbox({
className,
size = "md",
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root> & { size?: "sm" | "md" }) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border data-[state=checked]:text-ink-white shrink-0 transition-shadow outline-none align-middle",
"rounded-[4px]",
"border-ink-gray-4 data-[state=checked]:bg-ink-gray-8 data-[state=checked]:border-ink-gray-8",
// Hover state
"hover:border-ink-gray-5 hover:shadow-checkbox-hover hover:data-[state=checked]:bg-ink-gray-7 hover:data-[state=checked]:border-ink-gray-7",
// Active state
"active:border-ink-gray-6 active:data-[state=checked]:bg-ink-gray-6 active:data-[state=checked]:border-ink-gray-6",
// Focus state
"focus-visible:border-ink-gray-8 focus-visible:shadow-focus-gray focus-visible:data-[state=checked]:bg-ink-gray-8 focus-visible:data-[state=checked]:border-ink-gray-8",
// Disabled state
"disabled:border-ink-gray-3 disabled:bg-surface-gray-1 disabled:cursor-not-allowed disabled:data-[state=checked]:bg-surface-gray-3 disabled:data-[state=checked]:border-surface-gray-3 disabled:text-ink-gray-4",
// Invalid state
"aria-invalid:border-red-500",
size === "sm" ? "size-3.5" : "size-4",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className={size === 'sm' ? "size-2.5" : "size-3"} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -1,183 +0,0 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-surface-modal flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-ink-gray-4 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex items-center gap-2 m-1.5 h-8 rounded px-2.5 py-2 border border-transparent transition-all bg-surface-gray-2 not-focus-within:hover:bg-surface-gray-3 text-ink-gray-7 focus-within:bg-surface-white focus-within:border-outline-gray-4 focus-within:shadow-focus-gray"
>
<SearchIcon className="size-4 shrink-0 text-ink-gray-4" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"flex w-full bg-transparent outline-hidden text-base placeholder:text-ink-gray-4",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-ink-gray-6 [&_[cmdk-group-heading]]:text-ink-gray-4 overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-outline-gray-modals mx-0.5 h-px my-1", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"py-1.5 px-2 flex cursor-default text-ink-gray-6 items-center gap-2 rounded text-base relative outline-hidden select-none",
"data-[selected=true]:bg-surface-gray-2 [&_svg:not([class*='text-'])]:text-ink-gray-6 data-[disabled=true]:pointer-events-none data-[disabled=true]:text-ink-gray-3 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-ink-gray-5 ms-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -1,156 +0,0 @@
import * as React from "react"
import { XIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black-200 dark:bg-black-700",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-surface-modal shadow-xl rounded-xl data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 p-6 duration-200 outline-none sm:max-w-lg max-h-[90vh] overflow-y-auto",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="data-[state=open]:bg-surface-gray-1 data-[state=open]:text-ink-gray-8 absolute top-4 ltr:right-4 rtl:left-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon className="w-4 h-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 sm:text-start", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-2xl leading-6 text-ink-gray-8 font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-ink-gray-7 text-p-base", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -1,20 +0,0 @@
import * as React from "react"
import { Direction } from "radix-ui"
function DirectionProvider({
dir,
direction,
children,
}: React.ComponentProps<typeof Direction.DirectionProvider> & {
direction?: React.ComponentProps<typeof Direction.DirectionProvider>["dir"]
}) {
return (
<Direction.DirectionProvider dir={direction ?? dir}>
{children}
</Direction.DirectionProvider>
)
}
const useDirection = Direction.useDirection
export { DirectionProvider, useDirection }

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