Compare commits

...

113 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
0868eeaa0e Merge pull request #353 from ANasouf/BIG-112-Customer-emails-addresses-gets-the-.-removed-when-a-new-one-is-added
fix: gmail email addresses dots gets removed
2024-02-10 23:39:18 +02:00
a.nasouf
17dbe9713b fix: remove normalizeEmail function 2024-02-10 19:59:12 +02:00
Ahmed Bouhuolia
ecaf8c99bb Merge pull request #354 from ANasouf/BIG-109-some-keywords-are-not-localized
fix: some keywords are not localized
2024-02-08 20:12:27 +02:00
Ahmed Bouhuolia
90bde20674 Merge pull request #358 from bigcapitalhq/big-127-amount-decimals-are-set-to-zero-when-creating-an-new-payment
fix: payment receive subtotal shouldn't be rounded
2024-02-06 20:39:27 +02:00
Ahmed Bouhuolia
0c61f85707 chore: remove format number from estimate quantity 2024-02-06 20:38:25 +02:00
Ahmed Bouhuolia
0f678e61c5 fix: Decimal amounts are rounded when create a new transaction on some transactions types 2024-02-06 20:31:48 +02:00
Ahmed Bouhuolia
374f1acf8a fix: payment receive subtotal shouldn't be rounded 2024-02-06 10:54:41 +02:00
Ahmed Bouhuolia
ada1428193 Merge pull request #357 from bigcapitalhq/big-139-trial-balance-total-row-shouldnt-show-if-accounts-have-no
fix(server): Trial balance total row shouldn't show if accounts have no balances
2024-02-05 23:05:02 +02:00
Ahmed Bouhuolia
528d447443 fix(server): Trial balance total row shouldn't show if accounts have no balances 2024-02-05 23:04:02 +02:00
Ahmed Bouhuolia
d6d67b9a51 Merge pull request #355 from bigcapitalhq/big-126-invalid-bill-payment-amount-on-editing-bill-payment
fix: Invalid bill payment amount on editing bill payment
2024-02-05 22:39:52 +02:00
Ahmed Bouhuolia
12740223a8 fix: Invalid bill payment amount on editing bill payment 2024-02-05 22:38:56 +02:00
a.nasouf
f7f77b12c9 fix: file formatting 2024-02-05 20:05:10 +02:00
a.nasouf
a6db4fb6df fix: some keywords are not localized 2024-02-05 19:52:48 +02:00
a.nasouf
b38020d397 fix: gmail email addresses dots gets removed 2024-02-05 18:58:02 +02:00
Ahmed Bouhuolia
d090d5a026 Merge pull request #352 from bigcapitalhq/big-138-revert-the-paid-amount-to-bill-transaction-after-editing
fix(server): Revert the paid amount to bill transaction after editing bill payment amount
2024-02-05 18:52:09 +02:00
Ahmed Bouhuolia
7b5287ee80 fix(server): Revert the paid amount to bill transaction after editing bill payment amount 2024-02-05 18:50:34 +02:00
Ahmed Bouhuolia
ba3ea93a2d chore: dump CHANGELOG.md 2024-01-30 00:28:23 +02:00
Ahmed Bouhuolia
6b77bda77f Merge pull request #342 from bigcapitalhq/big-130-edited-transactions-does-not-reflect-on-reports
hotfix: editing sales and expense transactions don't reflect GL entries
2024-01-29 23:27:42 +02:00
Ahmed Bouhuolia
ba387e81f7 hotfix: editing sales and expense transactions don't reflect GL entries 2024-01-29 23:25:37 +02:00
Ahmed Bouhuolia
0414c090ea Merge pull request #335 from bigcapitalhq/big-128-inconsistency-in-currency-display-on-the-quick-find-feature
fix(webapp): inconsistency in currency of universal search items
2024-01-29 19:24:16 +02:00
Ahmed Bouhuolia
a52f3a933f fix(webapp): inconsistency in currency of universal search items 2024-01-29 19:19:46 +02:00
Ahmed Bouhuolia
d01eacf8d9 feat(server): add formattedAmount to purchase invoice to POJO 2024-01-29 19:18:59 +02:00
Ahmed Bouhuolia
0da151faaa Merge pull request #340 from bigcapitalhq/big-65-get-realtime-exchange-rate-from-third-party-service
feat: get latest exchange rate from third party services
2024-01-29 18:44:05 +02:00
Ahmed Bouhuolia
f93c8b46dc chore: change env name in .env.example 2024-01-29 18:42:54 +02:00
Ahmed Bouhuolia
59c5c8979d feat(webapp): add default exchange rate value to 1 in cause the ex. rate service returned error or failed. 2024-01-29 18:39:28 +02:00
Ahmed Bouhuolia
74a07847a4 feat(server): ability to assign the base currency as api query when getting latest ex. rate 2024-01-28 20:11:44 +02:00
Ahmed Bouhuolia
1740226294 feat(webapp): hook up latest exchange rate api 2024-01-28 18:48:35 +02:00
Ahmed Bouhuolia
1b20d1b073 feat(server): add application layer to exchange rate service 2024-01-28 18:47:49 +02:00
Ahmed Bouhuolia
ac7175d83b feat: get latest exchange rate from third party services 2024-01-28 15:52:54 +02:00
Ahmed Bouhuolia
d72d8e60d6 Merge pull request #339 from bigcapitalhq/big-122-bug-expense-details-amount-should-not-be-rounded
fix: Expense amounts should not be rounded
2024-01-26 23:52:30 +02:00
Ahmed Bouhuolia
de5920f910 fix: Expense amounts should not be rounded 2024-01-26 23:46:45 +02:00
Ahmed Bouhuolia
475c4e9967 fix(webapp): inconsistency in currency of universal search items 2024-01-26 00:05:24 +02:00
Ahmed Bouhuolia
21eb88ef53 chore: dump CHANGELOG.md file 2024-01-24 00:26:56 +02:00
Ahmed Bouhuolia
429159acf9 feat: export purchases by items to csv/xlsx (#327) 2024-01-23 12:33:43 +02:00
Ahmed Bouhuolia
7eb84474a5 hotfix(server): Unhandled thrown errors of services (#329) 2024-01-22 21:57:14 +02:00
allcontributors[bot]
e42adcae63 docs: add asenawritescode as a contributor for bug (#325)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-01-21 14:46:11 +02:00
Ahmed Bouhuolia
abffdd1029 fix(server): hotbug retireving empty results of inventory valuation and sales by items sheets 2024-01-21 14:34:36 +02:00
allcontributors[bot]
d052c23560 docs: add xprnio as a contributor for bug (#324)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-01-21 00:21:46 +02:00
Ragnar Laud
f02afd3c9f fix(webapp): AccountActivateAlert import (#322) 2024-01-21 00:18:31 +02:00
Ahmed Bouhuolia
3df17390e2 Merge pull request #318 from bigcapitalhq/big-120-get-cashflow-transaction-broken-cause-transaction-type
fix: `BIG-120` get cashflow transaction broken cause transaction type
2024-01-20 18:04:51 +02:00
Ahmed Bouhuolia
df38f8893d fix(server): get cashflow transaction type 2024-01-20 18:03:43 +02:00
Ahmed Bouhuolia
8882bc677e fix(webapp): undefined transactionNumber function 2024-01-20 18:03:26 +02:00
Ahmed Bouhuolia
f03d01113c Merge pull request #315 from bigcapitalhq/hotfix-pdf-printing
fix(server): the invoice and payment receipt printing
2024-01-20 15:53:59 +02:00
Ahmed Bouhuolia
8f431597d9 fix(server): hotbug the invoice and payment receipt printing 2024-01-20 15:51:57 +02:00
Ahmed Bouhuolia
03bc78a068 feat(webapp): remove the un-used functions 2024-01-19 23:33:15 +02:00
Ahmed Bouhuolia
ecf5d60db0 Merge pull request #310 from bigcapitalhq/big-99-purchases-by-items
feat: sales by items export csv & xlsx
2024-01-19 11:41:56 +02:00
Ahmed Bouhuolia
3a5fd2782a fix: sales by items issues 2024-01-19 11:39:00 +02:00
Ahmed Bouhuolia
1d8416ebfe fix: sales by items TS types 2024-01-19 11:25:19 +02:00
Ahmed Bouhuolia
3672abe7a5 feat: inventory valuation csv and xlsx export (#308)
* feat: inventory valuation csv and xlsx export

* feat(server): inventory valuation sheet exporting

* feat(webapp): inventory valuation sheet dyanmic columns

* feat: inventory valuation dynamic columns

* feat: inventory valuation TS types
2024-01-18 20:16:29 +02:00
Ahmed Bouhuolia
2753908b83 feat: sales by items dynamic columns 2024-01-18 19:52:11 +02:00
Asena
8495990ec2 Show customer / vendor balance. (#311)
* feat : Update the Customer select prop to balance
* feat : Update the Vendor select prop to balance
* feat: Update balance to formatted_balance
2024-01-18 14:47:07 +02:00
Ahmed Bouhuolia
471ce1b7af feat(webapp): dynamic columns of sales by items sheet 2024-01-18 14:39:56 +02:00
Ahmed Bouhuolia
4a920176f4 feat(server): sales by items table 2024-01-17 18:49:38 +02:00
Ahmed Bouhuolia
74fd76ce77 feat(server): sales by items export csv & xlsx 2024-01-17 00:24:13 +02:00
Ahmed Bouhuolia
c9f57d9a75 chore: update CHANGELOG.md file 2024-01-15 00:30:08 +02:00
Ahmed Bouhuolia
6c9810ea4c Merge pull request #270 from bigcapitalhq/big-44-auto-re-calculate-the-items-rate-once-changing-the-invoice
feat: Auto re-calculate the items rate once changing the invoice exchange rate.
2024-01-14 16:08:56 +02:00
Ahmed Bouhuolia
1e4b29f83c fix: content tweaks in rates re-calc popover 2024-01-14 15:59:25 +02:00
Ahmed Bouhuolia
2b03ac7f16 feat: implement auto entries rates re-calculation after change the exchange rate 2024-01-14 14:44:48 +02:00
Ahmed Bouhuolia
2cb8c2932f Merge branch 'develop' into big-44-auto-re-calculate-the-items-rate-once-changing-the-invoice 2024-01-11 20:27:42 +02:00
Ahmed Bouhuolia
2814d23f75 Update README.md 2024-01-10 20:24:11 +02:00
Ahmed Bouhuolia
35fa2389e1 feat: add headless accounting on README.md file 2024-01-10 20:21:46 +02:00
Ahmed Bouhuolia
c18b450129 Merge pull request #303 from bigcapitalhq/journal-sheet-export
Export general ledger & Journal to CSV and XLSX
2024-01-10 17:45:04 +02:00
Ahmed Bouhuolia
8db6e9f049 fix: remove blank files 2024-01-10 17:43:26 +02:00
Ahmed Bouhuolia
b63a2006d3 Merge pull request #305 from bigcapitalhq/all-contributors/add-ANasouf
docs: add ANasouf as a contributor for code
2024-01-09 22:23:15 +02:00
allcontributors[bot]
8c1ca503bd docs: update .all-contributorsrc [skip ci] 2024-01-09 20:22:36 +00:00
allcontributors[bot]
0ac79e661c docs: update README.md [skip ci] 2024-01-09 20:22:35 +00:00
Ahmed Bouhuolia
66ba4d35aa Merge pull request #304 from ANasouf/BIG-53-Add-approve-reject-buttons-on-action-bar-of-the-estimate-details-drawer
feat(webapp): add approve/reject to action bar of estimate details dr…
2024-01-09 22:17:43 +02:00
Ahmed Bouhuolia
46d25dff4c fix: move approve/reject estimate buttons to more menu 2024-01-09 22:14:10 +02:00
Ahmed Bouhuolia
5ef99f2cb3 feat: general ledger and journal exporting 2024-01-07 18:48:14 +02:00
Ahmed Bouhuolia
7aee76e461 feat: general ledger and journal rows types 2024-01-06 22:30:41 +02:00
Ahmed Bouhuolia
79f3f1b63d feat: wip journal and general ledger dyanmic columns 2024-01-06 20:16:22 +02:00
a.nasouf
26e104b9f1 feat(webapp): add approve/reject to action bar of estimate details drawer 2024-01-06 15:28:02 +02:00
Ahmed Bouhuolia
c71836ec27 feat: wip general ledger table 2024-01-04 21:43:57 +02:00
Ahmed Bouhuolia
60b1bc9ed7 feat(server): wip journal and general ledger table layer 2024-01-04 17:22:13 +02:00
Ahmed Bouhuolia
e6a3daa2c3 feat(server): general ledger exporting to csv/xlsx 2024-01-02 21:54:10 +02:00
Ahmed Bouhuolia
276ef1c907 feat(server): journal sheet csv/xlsx export 2024-01-02 21:53:37 +02:00
Ahmed Bouhuolia
83e48cce42 feat: remove hint popup of mail notification form 2024-01-01 20:59:31 +02:00
Ahmed Bouhuolia
99ca683d13 chore: update CHANGELOG file 2024-01-01 20:57:08 +02:00
Ahmed Bouhuolia
5062d891e1 feat: journal sheet export 2024-01-01 20:43:16 +02:00
Ahmed Bouhuolia
92d1fc670b Merge pull request #292 from bigcapitalhq/big-83-send-an-invoice-to-the-customer-email
feat: Send mail notifications of the sales transactions
2023-12-30 19:14:31 +02:00
Ahmed Bouhuolia
ab7abfea35 feat: mail notifications of sales transactions 2023-12-30 17:49:02 +02:00
Ahmed Bouhuolia
0d15c16d40 feat(server): contact mail notification service 2023-12-29 17:35:34 +02:00
Ahmed Bouhuolia
2a85fe2f3c feat(webapp): the mail notifications dialogs 2023-12-29 17:31:51 +02:00
Ahmed Bouhuolia
dc762567b5 feat(webapp): send mail notififcation of sale transactions 2023-12-28 17:53:51 +02:00
Ahmed Bouhuolia
a676e09537 feat: rich editor component 2023-12-27 23:56:34 +02:00
Ahmed Bouhuolia
c46948049c feat: add send mail icon 2023-12-26 22:57:41 +02:00
Ahmed Bouhuolia
de1b7f132c feat(webapp): send mail notification dialogs 2023-12-26 15:52:38 +02:00
allcontributors[bot]
90b552d5c9 docs: add cschuijt as a contributor for bug (#295)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2023-12-26 15:46:04 +02:00
Casper Schuijt
39115b3000 fix: do not validate numericality on postal codes of customer/vendor form (#294) 2023-12-26 15:42:29 +02:00
Ahmed Bouhuolia
3c8c3d8253 feat: add endpoints to retrieving the default mail options 2023-12-25 18:21:02 +02:00
Ahmed Bouhuolia
657400c671 feat: tweak mail notifications content 2023-12-24 23:41:04 +02:00
Ahmed Bouhuolia
6356cb5e63 feat: send mail notifications of sale transactions 2023-12-24 21:53:37 +02:00
Ahmed Bouhuolia
13c6e7a62d feat: send mail notifications of payment 2023-12-24 21:51:23 +02:00
Ahmed Bouhuolia
b6d99b1d4b feat: send mail notifications of sale receipts 2023-12-24 21:49:59 +02:00
Ahmed Bouhuolia
f0e15d43d3 feat: send sale estimate mail notification 2023-12-22 23:56:37 +02:00
Ahmed Bouhuolia
50d5ddba8e feat: send invoice notifications 2023-12-22 02:45:44 +02:00
Ahmed Bouhuolia
d2c63878ed feat: send invoices and reminder notifications to the customers 2023-12-21 22:57:17 +02:00
Ahmed Bouhuolia
ef52eaf91a Merge branch 'develop' into big-83-send-an-invoice-to-the-customer-email 2023-12-19 16:08:21 +02:00
Ahmed Bouhuolia
cd71900bdd feat: send invoice through mail 2023-12-18 21:28:53 +02:00
Ahmed Bouhuolia
34e97fc56b chore: update CHANGELOG.md 2023-12-17 23:07:25 +02:00
Ahmed Bouhuolia
e6e119682b Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2023-12-17 23:05:21 +02:00
Ahmed Bouhuolia
e096db2ab8 chore: update CAHNGELOG.md 2023-12-17 23:04:54 +02:00
Ahmed Bouhuolia
cdff641cdb Merge pull request #293 from bigcapitalhq/big-84-fix-the-transaction-tax-rate-currency
fix: the currency code of transaction tax rate entry
2023-12-17 17:52:14 +02:00
Ahmed Bouhuolia
d0126ffc2c fix: the currency code of transaction tax rate entry 2023-12-17 17:51:17 +02:00
Ahmed Bouhuolia
fe043eb594 Merge pull request #291 from bigcapitalhq/BIG-81
feat: Add default customer message and terms conditions to sales transactions
2023-12-16 22:41:58 +02:00
Ahmed Bouhuolia
eb1ff7c151 chore: doc comments 2023-12-16 22:34:36 +02:00
Ahmed Bouhuolia
a862ee9ccf feat: localize the preferences menu 2023-12-16 19:37:20 +02:00
Ahmed Bouhuolia
6953f7c4a3 feat: assign default default messages on sales transactions 2023-12-16 19:26:41 +02:00
Ahmed Bouhuolia
ad53ddb9dd feat: inject default message value to sales forms 2023-12-15 20:15:42 +02:00
Ahmed Bouhuolia
cfd4540a65 feat: wip send an invoice mail the customer email 2023-12-14 23:49:23 +02:00
Ahmed Bouhuolia
217321380a feat: wip default message to sales transactions. 2023-12-14 20:47:52 +02:00
Ahmed Bouhuolia
33e5d1a979 feat: Add default customer message and terms and conditions to the transactions. 2023-12-14 11:01:25 +02:00
Ahmed Bouhuolia
9531730d7a feat: Auto re-calculate the items rate once changing the invoice exchange rate. 2023-10-16 19:14:27 +02:00
341 changed files with 10350 additions and 3039 deletions

View File

@@ -69,6 +69,42 @@
"contributions": [
"code"
]
},
{
"login": "cschuijt",
"name": "Casper Schuijt",
"avatar_url": "https://avatars.githubusercontent.com/u/5460015?v=4",
"profile": "http://cschuijt.nl",
"contributions": [
"bug"
]
},
{
"login": "ANasouf",
"name": "ANasouf",
"avatar_url": "https://avatars.githubusercontent.com/u/19536487?v=4",
"profile": "https://github.com/ANasouf",
"contributions": [
"code"
]
},
{
"login": "xprnio",
"name": "Ragnar Laud",
"avatar_url": "https://avatars.githubusercontent.com/u/3042904?v=4",
"profile": "https://ragnarlaud.dev",
"contributions": [
"bug"
]
},
{
"login": "asenawritescode",
"name": "Asena",
"avatar_url": "https://avatars.githubusercontent.com/u/67445192?v=4",
"profile": "https://github.com/asenawritescode",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@@ -56,5 +56,11 @@ GOTENBERG_URL=http://gotenberg:3000
GOTENBERG_DOCS_URL=http://server:3000/public/
# Gotenberg API - (development)
# GOTENBERG_URL=http://gotenberg:3000
# GOTENBERG_DOCS_URL=http://server:3000/public/
# GOTENBERG_URL=http://localhost:9000
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/
# Exchange Rate Service
EXCHANGE_RATE_SERVICE=open-exchange-rate
# Open Exchange Rate
OPEN_EXCHANGE_RATE_APP_ID=

View File

@@ -2,11 +2,77 @@
All notable changes to Bigcapital server-side will be in this file.
# [0.10.1] - 25-09-2023
## [0.14.0] - 30-01-2024
* feat: purchases by items exporting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/327
* fix: expense amounts should not be rounded by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/339
* feat: get latest exchange rate from third party services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/340
* fix(webapp): inconsistency in currency of universal search items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/335
* hotfix: editing sales and expense transactions don't reflect GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/342
## [0.13.3] - 22-01-2024
* hotfix(server): Unhandled thrown errors of services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/329
## [0.13.2] - 21-01-2024
* feat: show customer / vendor balance. by @asenawritescode in https://github.com/bigcapitalhq/bigcapital/pull/311
* feat: inventory valuation csv and xlsx export by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/308
* feat: sales by items export csv & xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/310
* fix(server): the invoice and payment receipt printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/315
* fix: get cashflow transaction broken cause transaction type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/318
* fix: `AccountActivateAlert` import by @xprnio in https://github.com/bigcapitalhq/bigcapital/pull/322
## [0.13.1] - 15-01-2024
* feat(webapp): add approve/reject to action bar of estimate details dr… by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/304
* docs: add ANasouf as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/305
* feat: Export general ledger & Journal to CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/303
* feat: Auto re-calculate the items rate once changing the invoice exchange rate. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/270
## [0.13.0] - 31-12-2023
* feat: Send an invoice mail the customer email by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/292
* fix: Allow non-numeric postal codes by @cschuijt in https://github.com/bigcapitalhq/bigcapital/pull/294
* docs: add cschuijt as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/295
## [0.12.1] - 17-11-2023
* feat: Add default customer message and terms conditions to the transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/291
* fix: The currency code of transaction tax rate entry by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/293
## [0.12.0] - 04-11-2023
* feat: Export reports via CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/286
* fix: Axios upgrade by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/288
* fix(server): Allow decimal amount in sale/purchase transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/289
* feat: Optimize invoice documents printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/280
* chore(deps): bump axios from 0.20.0 to 1.6.0 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/284
* chore(deps): bump axios from 0.20.0 to 1.6.0 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/283
## [0.11.0] - 28-10-2023
* feat: Migrate to pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/253
* feat: Integrate tax rates to bills by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/260
* feat: Assign default sell/purchase tax rates to items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/261
* chore(deps-dev): bump @babel/traverse from 7.23.0 to 7.23.2 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/272
* feat: Improve financial statements rows color by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/276
* fix: Trial balance sheet adjusted balance by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/273
* feat: Adds tax numbers to organization and customers by @kochie in https://github.com/bigcapitalhq/bigcapital/pull/269
* docs: Add kochie as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/277
* feat: Computed Net Income under Equity in Balance Sheet report. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/271
* fix: Change Dockerfile files with new pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/278
## [0.10.2] - 02-10-2023
fix(webapp): Disable tax rates from item entries editor table services do not support tax rates (https://github.com/bigcapitalhq/bigcapital/commit/69afa07e3ba45495a4cab3490c15f2b0c40c4790) by @abouolia
fix(server): Add missing method in ItemEntry model (https://github.com/bigcapitalhq/bigcapital/commit/07628ddc37f46c98959ced0323f28752e0a98944) by @abouolia
## [0.10.1] - 25-09-2023
* Fix: Running tenants migration on Docker migration container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/242
# [0.10.0] - 24-09-2023
## [0.10.0] - 24-09-2023
* Added: Tax rates service by @abouolia @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/204
* Added: Sales Tax Liability Summary report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
@@ -16,7 +82,7 @@ All notable changes to Bigcapital server-side will be in this file.
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/199
* chore(deps): bump mongoose from 5.13.15 to 5.13.20 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/197
# [0.9.12] - 29-08-2023
## [0.9.12] - 29-08-2023
* Refactor: split the services to multiple service classes. (by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/202)
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
@@ -30,18 +96,18 @@ All notable changes to Bigcapital server-side will be in this file.
* Fix: filter by customers, vendors and items in reports do not work by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/224
https://github.com/bigcapitalhq/bigcapital/pull/225
# [0.9.11] - 23-07-2023
## [0.9.11] - 23-07-2023
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198
* fix: Expose and expand the rate limit to the env variables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/195
# [0.9.10] - 18-07-2023
## [0.9.10] - 18-07-2023
* feat(e2e): E2E onboarding process by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/176
* fix(webapp): Show loading message of cost computing job on financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/196
* fix(webapp): Change the currency code of sales and purchases transactions with foreign contacts.
# [0.9.9] - 28-06-2023
## [0.9.9] - 28-06-2023
* refactor: Customer and vendor select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/171
* chore: Move auto-increment components in separate files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/170
@@ -53,7 +119,7 @@ https://github.com/bigcapitalhq/bigcapital/pull/225
* fix: No currency in amount field on money in/out dialogs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/179
* fix: No default branch for customer/vendor opening balance branch by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/182
# [0.9.8] - 19-06-2023
## [0.9.8] - 19-06-2023
`bigcapitalhq/webapp`

View File

@@ -62,6 +62,12 @@ To get started locally, we have a [guide to help you](https://github.com/bigcapi
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/new/#https://github.com/bigcapitalhq/bigcapital)
## Headless Accounting
You can integrate Bigcapital API with your system to organize your transactions in double-entry system to get the best financial reports.
[![Run in Postman](https://run.pstmn.io/button.svg)](https://www.postman.com/bigcapital/workspace/bigcapital-api)
# Resources
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
@@ -107,6 +113,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View File

@@ -26,27 +26,27 @@ export default class ContactsController extends BaseController {
[...this.autocompleteQuerySchema],
this.validationResult,
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
this.dynamicListService.handlerErrorsToResponse
this.dynamicListService.handlerErrorsToResponse,
);
router.get(
'/:id',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.getContact.bind(this))
this.asyncMiddleware(this.getContact.bind(this)),
);
router.post(
'/:id/inactivate',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.inactivateContact.bind(this)),
this.handlerServiceErrors
this.handlerServiceErrors,
);
router.post(
'/:id/activate',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.activateContact.bind(this)),
this.handlerServiceErrors
this.handlerServiceErrors,
);
return router;
}
@@ -77,7 +77,7 @@ export default class ContactsController extends BaseController {
try {
const contact = await this.contactsService.getContact(
tenantId,
contactId
contactId,
);
return res.status(200).send({
customer: this.transfromToResponse(contact),
@@ -105,7 +105,7 @@ export default class ContactsController extends BaseController {
try {
const contacts = await this.contactsService.autocompleteContacts(
tenantId,
filter
filter,
);
return res.status(200).send({ contacts });
} catch (error) {
@@ -153,7 +153,6 @@ export default class ContactsController extends BaseController {
check('email')
.optional({ nullable: true })
.isString()
.normalizeEmail()
.isEmail()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('website')
@@ -380,7 +379,7 @@ export default class ContactsController extends BaseController {
error: Error,
req: Request,
res: Response,
next: NextFunction
next: NextFunction,
) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {

View File

@@ -1,19 +1,16 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { query, oneOf } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import { ServiceError } from '@/exceptions';
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { EchangeRateErrors } from '@/lib/ExchangeRate/types';
import { ExchangeRateApplication } from '@/services/ExchangeRates/ExchangeRateApplication';
@Service()
export default class ExchangeRatesController extends BaseController {
@Inject()
exchangeRatesService: ExchangeRatesService;
@Inject()
dynamicListService: DynamicListingService;
private exchangeRatesApp: ExchangeRateApplication;
/**
* Constructor method.
@@ -22,164 +19,40 @@ export default class ExchangeRatesController extends BaseController {
const router = Router();
router.get(
'/',
[...this.exchangeRatesListSchema],
'/latest',
[
oneOf([
query('to_currency').exists().isString().isISO4217(),
query('from_currency').exists().isString().isISO4217(),
]),
],
this.validationResult,
asyncMiddleware(this.exchangeRates.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.handleServiceError,
);
router.post(
'/',
[...this.exchangeRateDTOSchema],
this.validationResult,
asyncMiddleware(this.addExchangeRate.bind(this)),
this.handleServiceError
);
router.post(
'/:id',
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
this.validationResult,
asyncMiddleware(this.editExchangeRate.bind(this)),
this.handleServiceError
);
router.delete(
'/:id',
[...this.exchangeRateIdSchema],
this.validationResult,
asyncMiddleware(this.deleteExchangeRate.bind(this)),
asyncMiddleware(this.latestExchangeRate.bind(this)),
this.handleServiceError
);
return router;
}
get exchangeRatesListSchema() {
return [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
];
}
get exchangeRateDTOSchema() {
return [
check('exchange_rate').exists().isNumeric().toFloat(),
check('currency_code').exists().trim().escape(),
check('date').exists().isISO8601(),
];
}
get exchangeRateEditDTOSchema() {
return [check('exchange_rate').exists().isNumeric().toFloat()];
}
get exchangeRateIdSchema() {
return [param('id').isNumeric().toInt()];
}
get exchangeRatesIdsSchema() {
return [
query('ids').isArray({ min: 2 }),
query('ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve exchange rates.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async exchangeRates(req: Request, res: Response, next: NextFunction) {
private async latestExchangeRate(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = {
page: 1,
pageSize: 12,
filterRoles: [],
columnSortBy: 'created_at',
sortOrder: 'asc',
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
tenantId,
filter
);
return res.status(200).send({ exchange_rates: exchangeRates });
} catch (error) {
next(error);
}
}
/**
* Adds a new exchange rate on the given date.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const exchangeRateDTO = this.matchedBodyData(req);
const exchangeRateQuery = this.matchedQueryData(req);
try {
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
const exchangeRate = await this.exchangeRatesApp.latest(
tenantId,
exchangeRateDTO
exchangeRateQuery
);
return res.status(200).send({ id: exchangeRate.id });
} catch (error) {
next(error);
}
}
/**
* Edit the given exchange rate.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: exchangeRateId } = req.params;
const exchangeRateDTO = this.matchedBodyData(req);
try {
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
tenantId,
exchangeRateId,
exchangeRateDTO
);
return res.status(200).send({
id: exchangeRateId,
message: 'The exchange rate has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Delete the given exchange rate from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: exchangeRateId } = req.params;
try {
await this.exchangeRatesService.deleteExchangeRate(
tenantId,
exchangeRateId
);
return res.status(200).send({ id: exchangeRateId });
return res.status(200).send(exchangeRate);
} catch (error) {
next(error);
}
@@ -192,26 +65,56 @@ export default class ExchangeRatesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceError(
private handleServiceError(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
return res.status(404).send({
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
if (EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY === error.errorType) {
return res.status(400).send({
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
errors: [
{
type: EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
code: 100,
message: 'The given base currency is invalid.',
},
],
});
}
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
} else if (
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED === error.errorType
) {
return res.status(400).send({
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
errors: [
{
type: EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
code: 200,
message: 'The service is not allowed',
},
],
});
} else if (
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED === error.errorType
) {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
code: 300,
message: 'The API key is required',
},
],
});
} else if (EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED === error.errorType) {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED,
code: 400,
message: 'The API rate limit has been exceeded',
},
],
});
}
}

View File

@@ -2,15 +2,16 @@ import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService';
import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { GeneralLedgerApplication } from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication';
@Service()
export default class GeneralLedgerReportController extends BaseFinancialReportController {
@Inject()
generalLedgetService: GeneralLedgerService;
private generalLedgerApplication: GeneralLedgerApplication;
/**
* Router constructor.
@@ -61,20 +62,43 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
* @param {Response} res -
*/
async generalLedger(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const { tenantId } = req;
const filter = this.matchedQueryData(req);
const accept = this.accepts(req);
try {
const { data, query, meta } =
await this.generalLedgetService.generalLedger(tenantId, filter);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
// Retrieves the table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.generalLedgerApplication.table(tenantId, filter);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.generalLedgerApplication.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.generalLedgerApplication.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.generalLedgerApplication.sheet(tenantId, filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -3,14 +3,15 @@ import { query, ValidationChain } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import InventoryValuationService from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { InventoryValuationSheetApplication } from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class InventoryValuationReportController extends BaseFinancialReportController {
@Inject()
inventoryValuationService: InventoryValuationService;
private inventoryValuationApp: InventoryValuationSheetApplication;
/**
* Router constructor.
@@ -71,19 +72,45 @@ export default class InventoryValuationReportController extends BaseFinancialRep
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.inventoryValuationService.inventoryValuationSheet(
tenantId,
filter
);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.inventoryValuationApp.table(tenantId, filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV == acceptType) {
const buffer = await this.inventoryValuationApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xslx buffer format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.inventoryValuationApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the json format.
} else {
const { data, query, meta } = await this.inventoryValuationApp.sheet(
tenantId,
filter
);
return res.status(200).send({ meta, data, query });
}
}
}

View File

@@ -3,14 +3,15 @@ import { Request, Response, Router, NextFunction } from 'express';
import { castArray } from 'lodash';
import { query, oneOf } from 'express-validator';
import BaseFinancialReportController from './BaseFinancialReportController';
import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { JournalSheetApplication } from '@/services/FinancialStatements/JournalSheet/JournalSheetApplication';
@Service()
export default class JournalSheetController extends BaseFinancialReportController {
@Inject()
journalService: JournalSheetService;
private journalSheetApp: JournalSheetApplication;
/**
* Router constructor.
@@ -57,28 +58,49 @@ export default class JournalSheetController extends BaseFinancialReportControlle
* @param {Request} req -
* @param {Response} res -
*/
async journal(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
private async journal(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
let filter = this.matchedQueryData(req);
filter = {
...filter,
accountsIds: castArray(filter.accountsIds),
};
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
try {
const { data, query, meta } = await this.journalService.journalSheet(
tenantId,
filter
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.journalSheetApp.table(tenantId, filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.journalSheetApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.journalSheetApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.journalSheetApp.sheet(tenantId, filter);
return res.status(200).send({
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
} catch (error) {
next(error);
return res.status(200).send(sheet);
}
}
}

View File

@@ -1,17 +1,18 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { PurchasesByItemsService } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { PurcahsesByItemsApplication } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication';
@Service()
export default class PurchasesByItemReportController extends BaseFinancialReportController {
@Inject()
purchasesByItemsService: PurchasesByItemsService;
private purchasesByItemsApp: PurcahsesByItemsApplication;
/**
* Router constructor.
@@ -63,20 +64,47 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
* @param {Request} req -
* @param {Response} res -
*/
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
public async purchasesByItems(req: Request, res: Response) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
]);
// JSON table response format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.purchasesByItemsApp.table(tenantId, filter);
return res.status(200).send(table);
// CSV response format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.purchasesByItemsApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Xlsx response format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.purchasesByItemsApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Json response format.
} else {
const sheet = await this.purchasesByItemsApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -1,17 +1,17 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { query, ValidationChain, ValidationSchema } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import SalesByItemsReportService from '@/services/FinancialStatements/SalesByItems/SalesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { SalesByItemsApplication } from '@/services/FinancialStatements/SalesByItems/SalesByItemsApplication';
@Service()
export default class SalesByItemsReportController extends BaseFinancialReportController {
@Inject()
salesByItemsService: SalesByItemsReportService;
salesByItemsApp: SalesByItemsApplication;
/**
* Router constructor.
@@ -24,13 +24,14 @@ export default class SalesByItemsReportController extends BaseFinancialReportCon
CheckPolicies(ReportsAction.READ_SALES_BY_ITEMS, AbilitySubject.Report),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.purchasesByItems.bind(this))
asyncMiddleware(this.salesByItems.bind(this))
);
return router;
}
/**
* Validation schema.
* @returns {ValidationChain[]}
*/
private get validationSchema(): ValidationChain[] {
return [
@@ -60,26 +61,44 @@ export default class SalesByItemsReportController extends BaseFinancialReportCon
* @param {Request} req -
* @param {Response} res -
*/
private async purchasesByItems(
req: Request,
res: Response,
next: NextFunction
) {
private async salesByItems(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
const accept = this.accepts(req);
try {
const { data, query, meta } = await this.salesByItemsService.salesByItems(
tenantId,
filter
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
]);
// Retrieves the csv format.
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.salesByItemsApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json table format.
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.salesByItemsApp.table(tenantId, filter);
return res.status(200).send(table);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = this.salesByItemsApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.salesByItemsApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
}
}
}

View File

@@ -303,7 +303,7 @@ export default class BillsController extends BaseController {
try {
const bill = await this.billsApplication.getBill(tenantId, billId);
return res.status(200).send(this.transfromToResponse({ bill }));
return res.status(200).send({ bill });
} catch (error) {
next(error);
}
@@ -348,14 +348,11 @@ export default class BillsController extends BaseController {
};
try {
const { bills, pagination, filterMeta } =
await this.billsApplication.getBills(tenantId, filter);
return res.status(200).send({
bills: this.transfromToResponse(bills),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
const billsWithPagination = await this.billsApplication.getBills(
tenantId,
filter
);
return res.status(200).send(billsWithPagination);
} catch (error) {
next(error);
}
@@ -563,6 +560,16 @@ export default class BillsController extends BaseController {
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 1900 }],
});
}
if (error.errorType === 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT') {
return res.boom.badRequest(null, {
errors: [
{
type: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT',
code: 2000,
},
],
});
}
}
next(error);
}

View File

@@ -158,15 +158,11 @@ export default class BillsPayments extends BaseController {
const { tenantId } = req;
const { vendorId } = this.matchedQueryData(req);
try {
const entries = await this.billPaymentsPages.getNewPageEntries(
tenantId,
vendorId
);
return res.status(200).send({
entries: this.transfromToResponse(entries),
});
} catch (error) {}
const entries = await this.billPaymentsPages.getNewPageEntries(
tenantId,
vendorId
);
return res.status(200).send({ entries });
}
/**
@@ -183,16 +179,12 @@ export default class BillsPayments extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
const { billPayment, entries } =
const billPaymentsWithEditEntries =
await this.billPaymentsPages.getBillPaymentEditPage(
tenantId,
paymentReceiveId
);
return res.status(200).send({
bill_payment: this.transfromToResponse(billPayment),
entries: this.transfromToResponse(entries),
});
return res.status(200).send(billPaymentsWithEditEntries);
} catch (error) {
next(error);
}
@@ -304,9 +296,7 @@ export default class BillsPayments extends BaseController {
tenantId,
billPaymentId
);
return res.status(200).send({
bill_payment: this.transfromToResponse(billPayment),
});
return res.status(200).send({ billPayment });
} catch (error) {
next(error);
}
@@ -359,17 +349,12 @@ export default class BillsPayments extends BaseController {
};
try {
const { billPayments, pagination, filterMeta } =
const billPaymentsWithPagination =
await this.billPaymentsApplication.getBillPayments(
tenantId,
billPaymentsFilter
);
return res.status(200).send({
bill_payments: this.transfromToResponse(billPayments),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
return res.status(200).send(billPaymentsWithPagination);
} catch (error) {
next(error);
}

View File

@@ -320,20 +320,19 @@ export default class VendorCreditController extends BaseController {
res: Response,
next: NextFunction
) => {
const { id: billId } = req.params;
const { id: vendorCreditId } = req.params;
const { tenantId, user } = req;
const vendorCreditEditDTO: IVendorCreditEditDTO = this.matchedBodyData(req);
try {
await this.editVendorCreditService.editVendorCredit(
tenantId,
billId,
vendorCreditEditDTO,
user
vendorCreditId,
vendorCreditEditDTO
);
return res.status(200).send({
id: billId,
id: vendorCreditId,
message: 'The vendor credit has been edited successfully.',
});
} catch (error) {

View File

@@ -26,6 +26,7 @@ import GetCreditNoteAssociatedInvoicesToApply from '@/services/CreditNotes/GetCr
import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCreditNoteAssociatedAppliedInvoices';
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
import { ACCEPT_TYPE } from '@/interfaces/Http';
/**
* Credit notes controller.
* @service
@@ -293,7 +294,7 @@ export default class PaymentReceivesController extends BaseController {
return [
check('from_account_id').exists().isNumeric().toInt(),
check('description').optional(),
check('amount').exists().isNumeric().toFloat(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -438,7 +439,7 @@ export default class PaymentReceivesController extends BaseController {
};
/**
* Retrieve the payment receive details.
* Retrieve the credit note details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
@@ -451,38 +452,28 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req;
const { id: creditNoteId } = req.params;
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
tenantId,
creditNoteId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} else {
const creditNote = await this.getCreditNoteService.getCreditNote(
tenantId,
creditNoteId
);
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
// Response formatter.
res.format({
// Json content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res
.status(200)
.send({ credit_note: this.transfromToResponse(creditNote) });
},
// Pdf content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
tenantId,
creditNote
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
return res.status(200).send({ creditNote });
}
};

View File

@@ -1,10 +1,11 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain } from 'express-validator';
import { body, check, param, query, ValidationChain } from 'express-validator';
import {
AbilitySubject,
IPaymentReceiveDTO,
PaymentReceiveAction,
PaymentReceiveMailOptsDTO,
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -13,6 +14,7 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ServiceError } from '@/exceptions';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class PaymentReceivesController extends BaseController {
@@ -117,6 +119,25 @@ export default class PaymentReceivesController extends BaseController {
asyncMiddleware(this.deletePaymentReceive.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.paymentReceiveValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.sendPaymentReceiveByMail.bind(this),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.paymentReceiveValidation],
asyncMiddleware(this.getPaymentDefaultMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -328,17 +349,12 @@ export default class PaymentReceivesController extends BaseController {
};
try {
const { paymentReceives, pagination, filterMeta } =
const paymentsReceivedWithPagination =
await this.paymentReceiveApplication.getPaymentReceives(
tenantId,
filter
);
return res.status(200).send({
payment_receives: this.transfromToResponse(paymentReceives),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
return res.status(200).send(paymentsReceivedWithPagination);
} catch (error) {
next(error);
}
@@ -415,38 +431,34 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Response in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceiveId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Response in json format.
} else {
const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive(
tenantId,
paymentReceiveId
);
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send({
payment_receive: this.transfromToResponse(paymentReceive),
});
},
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceive
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
return res.status(200).send({
payment_receive: paymentReceive,
});
} catch (error) {
next(error);
}
}
@@ -480,7 +492,7 @@ export default class PaymentReceivesController extends BaseController {
};
/**
*
* Retrieves the sms details of the given payment receive.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
@@ -508,13 +520,73 @@ export default class PaymentReceivesController extends BaseController {
};
/**
* Handles service errors.
* @param error
* @param req
* @param res
* @param next
* Sends mail invoice of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceErrors(
public sendPaymentReceiveByMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
const paymentMailDTO: PaymentReceiveMailOptsDTO = this.matchedBodyData(
req,
{
includeOptionals: false,
}
);
try {
await this.paymentReceiveApplication.notifyPaymentByMail(
tenantId,
paymentReceiveId,
paymentMailDTO
);
return res.status(200).send({
code: 200,
message: 'The payment notification has been sent successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Retrieves the default mail options of the given payment transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public getPaymentDefaultMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
try {
const data = await this.paymentReceiveApplication.getPaymentMailOptions(
tenantId,
paymentReceiveId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private handleServiceErrors(
error: Error,
req: Request,
res: Response,

View File

@@ -1,10 +1,11 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { body, check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import {
AbilitySubject,
ISaleEstimateDTO,
SaleEstimateAction,
SaleEstimateMailOptionsDTO,
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -121,6 +122,27 @@ export default class SalesEstimatesController extends BaseController {
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse
);
router.post(
'/:id/mail',
[
...this.validateSpecificEstimateSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleEstimateMail.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.getSaleEstimateMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -312,7 +334,6 @@ export default class SalesEstimatesController extends BaseController {
tenantId,
estimateId
);
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been approved successfully.',
@@ -341,7 +362,6 @@ export default class SalesEstimatesController extends BaseController {
tenantId,
estimateId
);
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been rejected successfully.',
@@ -361,33 +381,30 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params;
const { tenantId } = req;
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimateId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves estimates in json format.
} else {
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId,
estimateId
);
// Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent =
await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimate
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
return res.status(200).send({ estimate });
}
}
@@ -405,22 +422,11 @@ export default class SalesEstimatesController extends BaseController {
pageSize: 12,
...this.matchedQueryData(req),
};
try {
const { salesEstimates, pagination, filterMeta } =
const salesEstimatesWithPagination =
await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter);
res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(
this.transfromToResponse({
salesEstimates,
pagination,
filterMeta,
})
);
},
});
return res.status(200).send(salesEstimatesWithPagination);
} catch (error) {
next(error);
}
@@ -478,6 +484,65 @@ export default class SalesEstimatesController extends BaseController {
}
};
/**
* Send the sale estimate mail.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private sendSaleEstimateMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: invoiceId } = req.params;
const saleEstimateDTO: SaleEstimateMailOptionsDTO = this.matchedBodyData(
req,
{
includeOptionals: false,
}
);
try {
await this.saleEstimatesApplication.sendSaleEstimateMail(
tenantId,
invoiceId,
saleEstimateDTO
);
return res.status(200).send({
code: 200,
message: 'The sale estimate mail has been sent successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Retrieves the default mail options of the given sale estimate.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private getSaleEstimateMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: invoiceId } = req.params;
try {
const data = await this.saleEstimatesApplication.getSaleEstimateMail(
tenantId,
invoiceId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error

View File

@@ -1,5 +1,5 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { body, check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import BaseController from '../BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -10,14 +10,12 @@ import {
ISaleInvoiceCreateDTO,
SaleInvoiceAction,
AbilitySubject,
SendInvoiceMailDTO,
} from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SaleInvoicesController extends BaseController {
@Inject()
@@ -145,6 +143,48 @@ export default class SaleInvoicesController extends BaseController {
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse
);
router.get(
'/:id/mail-reminder',
this.specificSaleInvoiceValidation,
this.validationResult,
asyncMiddleware(this.getSaleInvoiceMailReminder.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail-reminder',
[
...this.specificSaleInvoiceValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleInvoiceMailReminder.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.specificSaleInvoiceValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleInvoiceMail.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.specificSaleInvoiceValidation],
this.validationResult,
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -360,7 +400,6 @@ export default class SaleInvoicesController extends BaseController {
saleInvoiceId,
user
);
return res.status(200).send({
id: saleInvoiceId,
message: 'The sale invoice has been deleted successfully.',
@@ -375,43 +414,35 @@ export default class SaleInvoicesController extends BaseController {
* @param {Request} req - Request object.
* @param {Response} res - Response object.
*/
private async getSaleInvoice(
req: Request,
res: Response,
next: NextFunction
) {
private async getSaleInvoice(req: Request, res: Response) {
const { id: saleInvoiceId } = req.params;
const { tenantId, user } = req;
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoiceId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves invoice in json format.
} else {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId,
saleInvoiceId,
user
);
// Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res
.status(200)
.send(this.transfromToResponse({ saleInvoice }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoice
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
return res.status(200).send({ saleInvoice });
}
}
/**
@@ -434,14 +465,10 @@ export default class SaleInvoicesController extends BaseController {
...this.matchedQueryData(req),
};
try {
const { salesInvoices, filterMeta, pagination } =
const salesInvoicesWithPagination =
await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter);
return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
return res.status(200).send(salesInvoicesWithPagination);
} catch (error) {
next(error);
}
@@ -468,9 +495,7 @@ export default class SaleInvoicesController extends BaseController {
tenantId,
customerId
);
return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices),
});
return res.status(200).send({ salesInvoices });
} catch (error) {
next(error);
}
@@ -498,7 +523,6 @@ export default class SaleInvoicesController extends BaseController {
invoiceId,
writeoffDTO
);
return res.status(200).send({
id: saleInvoice.id,
message: 'The given sale invoice has been written-off successfully.',
@@ -630,6 +654,119 @@ export default class SaleInvoicesController extends BaseController {
}
};
/**
* Sends mail invoice of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async sendSaleInvoiceMail(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { id: invoiceId } = req.params;
const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req, {
includeOptionals: false,
});
try {
await this.saleInvoiceApplication.sendSaleInvoiceMail(
tenantId,
invoiceId,
invoiceMailDTO
);
return res.status(200).send({
code: 200,
message: 'The sale invoice mail has been sent successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retreivers the sale invoice reminder options.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleInvoiceMailReminder(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { id: invoiceId } = req.params;
try {
const data = await this.saleInvoiceApplication.getSaleInvoiceMailReminder(
tenantId,
invoiceId
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/**
* Sends mail invoice of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async sendSaleInvoiceMailReminder(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { id: invoiceId } = req.params;
const invoiceMailDTO: SendInvoiceMailDTO = this.matchedBodyData(req, {
includeOptionals: false,
});
try {
await this.saleInvoiceApplication.sendSaleInvoiceMailReminder(
tenantId,
invoiceId,
invoiceMailDTO
);
return res.status(200).send({
code: 200,
message: 'The sale invoice mail reminder has been sent successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieves the default mail options of the given sale invoice.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleInvoiceMail(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { id: invoiceId } = req.params;
try {
const data = await this.saleInvoiceApplication.getSaleInvoiceMail(
tenantId,
invoiceId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param {Error} error

View File

@@ -1,14 +1,19 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { body, check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '../BaseController';
import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
import {
ISaleReceiptDTO,
SaleReceiptMailOpts,
SaleReceiptMailOptsDTO,
} from '@/interfaces/SaleReceipt';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class SalesReceiptsController extends BaseController {
@@ -46,6 +51,27 @@ export default class SalesReceiptsController extends BaseController {
this.saleReceiptSmsDetails,
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.specificReceiptValidationSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_receipt').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleReceiptMail.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.specificReceiptValidationSchema],
this.validationResult,
asyncMiddleware(this.getSaleReceiptMail.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id',
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
@@ -205,7 +231,6 @@ export default class SalesReceiptsController extends BaseController {
tenantId,
saleReceiptId
);
return res.status(200).send({
id: saleReceiptId,
message: 'Sale receipt has been deleted successfully.',
@@ -294,15 +319,10 @@ export default class SalesReceiptsController extends BaseController {
...this.matchedQueryData(req),
};
try {
const { data, pagination, filterMeta } =
const salesReceiptsWithPagination =
await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter);
const response = this.transfromToResponse({
data,
pagination,
filterMeta,
});
return res.status(200).send(response);
return res.status(200).send(salesReceiptsWithPagination);
} catch (error) {
next(error);
}
@@ -314,36 +334,34 @@ export default class SalesReceiptsController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
public async getSaleReceipt(req: Request, res: Response) {
const { id: saleReceiptId } = req.params;
const { tenantId } = req;
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceiptId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves receipt in json format.
} else {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId,
saleReceiptId
);
res.format({
'application/json': () => {
return res
.status(200)
.send(this.transfromToResponse({ saleReceipt }));
},
'application/pdf': async () => {
const pdfContent =
await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceipt
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
return res.status(200).send({ saleReceipt });
}
}
@@ -405,6 +423,64 @@ export default class SalesReceiptsController extends BaseController {
}
};
/**
* Sends mail notification of the given sale receipt.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public sendSaleReceiptMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: receiptId } = req.params;
const receiptMailDTO: SaleReceiptMailOptsDTO = this.matchedBodyData(req, {
includeOptionals: false,
});
try {
await this.saleReceiptsApplication.sendSaleReceiptMail(
tenantId,
receiptId,
receiptMailDTO
);
return res.status(200).send({
code: 200,
message:
'The sale receipt notification via sms has been sent successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Retrieves the default mail options of the given sale receipt.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public getSaleReceiptMail = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: receiptId } = req.params;
try {
const data = await this.saleReceiptsApplication.getSaleReceiptMail(
tenantId,
receiptId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error

View File

@@ -58,6 +58,7 @@ module.exports = {
secure: !!parseInt(process.env.MAIL_SECURE, 10),
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
from: process.env.MAIL_FROM_ADDRESS,
},
/**
@@ -168,4 +169,14 @@ module.exports = {
* to application detarmines to upgrade.
*/
databaseBatch: 4,
/**
* Exchange rate.
*/
exchangeRate: {
service: 'open-exchange-rate',
openExchangeRate: {
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
}
}
};

View File

@@ -59,6 +59,12 @@ export default {
auto_increment: {
type: 'boolean',
},
customer_notes: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
sales_receipts: {
next_number: {
@@ -73,6 +79,12 @@ export default {
preferred_deposit_account: {
type: 'number',
},
receipt_message: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
sales_invoices: {
next_number: {
@@ -84,6 +96,12 @@ export default {
auto_increment: {
type: 'boolean',
},
customer_notes: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
payment_receives: {
next_number: {
@@ -147,6 +165,12 @@ export default {
auto_increment: {
type: 'boolean',
},
customer_notes: {
type: 'string',
},
terms_conditions: {
type: 'string',
},
},
vendor_credit: {
next_number: {

View File

@@ -1,13 +1,10 @@
import {
IAgingPeriod,
IAgingPeriodTotal,
IAgingAmount,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
} from './AgingReport';
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {

View File

@@ -1,36 +1,10 @@
import { IFilterRole } from './DynamicFilter';
export interface ExchangeRateLatestDTO {
toCurrency: string;
fromCurrency: string;
}
export interface IExchangeRate {
id: number,
currencyCode: string,
exchangeRate: number,
date: Date,
createdAt: Date,
updatedAt: Date,
};
export interface IExchangeRateDTO {
currencyCode: string,
exchangeRate: number,
date: Date,
};
export interface IExchangeRateEditDTO {
exchangeRate: number,
};
export interface IExchangeRateFilter {
page: number,
pageSize: number,
filterRoles?: IFilterRole[];
columnSortBy: string;
sortOrder: string;
};
export interface IExchangeRatesService {
newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise<IExchangeRate>;
editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise<void>;
deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise<void>;
listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise<void>;
};
export interface EchangeRateLatestPOJO {
baseCurrency: string;
toCurrency: string;
exchangeRate: number;
}

View File

@@ -1,3 +1,4 @@
import { IFinancialTable } from "./Table";
export interface IGeneralLedgerSheetQuery {
@@ -36,6 +37,7 @@ export interface IGeneralLedgerSheetAccountTransaction {
referenceType?: string,
date: Date|string,
dateFormatted: string;
};
export interface IGeneralLedgerSheetAccountBalance {
@@ -56,6 +58,8 @@ export interface IGeneralLedgerSheetAccount {
closingBalance: IGeneralLedgerSheetAccountBalance,
}
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];
export interface IAccountTransaction {
id: number,
index: number,
@@ -78,4 +82,11 @@ export interface IGeneralLedgerMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
};
fromDate: string;
toDate: string;
};
export interface IGeneralLedgerTableData extends IFinancialTable {
meta: IGeneralLedgerMeta;
query: IGeneralLedgerSheetQuery;
}

View File

@@ -1,4 +1,5 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IInventoryValuationReportQuery {
asDate: Date | string;
@@ -39,9 +40,19 @@ export interface IInventoryValuationTotal {
quantityFormatted: string;
}
export type IInventoryValuationStatement =
| {
items: IInventoryValuationItem[];
total: IInventoryValuationTotal;
}
| {};
export type IInventoryValuationStatement = {
items: IInventoryValuationItem[];
total: IInventoryValuationTotal;
};
export type IInventoryValuationSheetData = IInventoryValuationStatement;
export interface IInventoryValuationSheet {
data: IInventoryValuationStatement;
meta: IInventoryValuationSheetMeta;
query: IInventoryValuationReportQuery;
}
export interface IInventoryValuationTable extends IFinancialTable {
meta: IInventoryValuationSheetMeta;
query: IInventoryValuationReportQuery;
}

View File

@@ -1,36 +1,52 @@
import { IJournalEntry } from './Journal';
import { IFinancialTable } from './Table';
export interface IJournalReportQuery {
fromDate: Date | string,
toDate: Date | string,
fromDate: Date | string;
toDate: Date | string;
numberFormat: {
noCents: boolean,
divideOn1000: boolean,
},
transactionType: string,
transactionId: string,
noCents: boolean;
divideOn1000: boolean;
};
transactionType: string;
transactionId: string;
accountsIds: number | number[],
fromRange: number,
toRange: number,
accountsIds: number | number[];
fromRange: number;
toRange: number;
}
export interface IJournalReportEntriesGroup {
id: string,
entries: IJournalEntry[],
currencyCode: string,
credit: number,
debit: number,
formattedCredit: string,
formattedDebit: string,
id: string;
date: Date;
dateFormatted: string;
entries: IJournalEntry[];
currencyCode: string;
credit: number;
debit: number;
formattedCredit: string;
formattedDebit: string;
}
export interface IJournalReport {
entries: IJournalReportEntriesGroup[],
entries: IJournalReportEntriesGroup[];
}
export interface IJournalSheetMeta {
isCostComputeRunning: boolean,
organizationName: string,
baseCurrency: string,
}
isCostComputeRunning: boolean;
organizationName: string;
baseCurrency: string;
}
export interface IJournalTable extends IFinancialTable {
query: IJournalReportQuery;
meta: IJournalSheetMeta;
}
export type IJournalTableData = IJournalReportEntriesGroup[];
export interface IJournalSheet {
data: IJournalTableData;
query: IJournalReportQuery;
meta: IJournalSheetMeta;
}

View File

@@ -1,9 +1,17 @@
export type IMailAttachment = MailAttachmentPath | MailAttachmentContent;
export interface MailAttachmentPath {
filename: string;
path: string;
cid: string;
}
export interface MailAttachmentContent {
filename: string;
content: Buffer;
}
export interface IMailable {
constructor(
view: string,
data?: { [key: string]: string | number },
);
constructor(view: string, data?: { [key: string]: string | number });
send(): Promise<any>;
build(): void;
setData(data: { [key: string]: string | number }): IMailable;
@@ -13,4 +21,27 @@ export interface IMailable {
setView(view: string): IMailable;
render(data?: { [key: string]: string | number }): string;
getViewContent(): string;
}
}
export interface AddressItem {
label: string;
mail: string;
primary?: boolean;
}
export interface CommonMailOptions {
toAddresses: AddressItem[];
fromAddresses: AddressItem[];
from: string;
to: string | string[];
subject: string;
body: string;
data?: Record<string, any>;
}
export interface CommonMailOptionsDTO {
to?: string | string[];
from?: string;
subject?: string;
body?: string;
}

View File

@@ -1,5 +1,9 @@
import { Knex } from 'knex';
import { ISystemUser } from '@/interfaces';
import {
CommonMailOptions,
CommonMailOptionsDTO,
ISystemUser,
} from '@/interfaces';
import { ILedgerEntry } from './Ledger';
import { ISaleInvoice } from './SaleInvoice';
@@ -19,7 +23,7 @@ export interface IPaymentReceive {
createdAt: Date;
updatedAt: Date;
localAmount?: number;
branchId?: number
branchId?: number;
}
export interface IPaymentReceiveCreateDTO {
customerId: number;
@@ -165,3 +169,7 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'createdAt'
| 'branchId'
>;
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}

View File

@@ -0,0 +1,54 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IPurchasesByItemsReportQuery {
fromDate: Date | string;
toDate: Date | string;
itemsIds: number[];
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
onlyActive: boolean;
}
export interface IPurchasesByItemsSheetMeta {
organizationName: string;
baseCurrency: string;
}
export interface IPurchasesByItemsItem {
id: number;
name: string;
code: string;
quantitySold: number;
soldCost: number;
averageSellPrice: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
averageSellPriceFormatted: string;
currencyCode: string;
}
export interface IPurchasesByItemsTotal {
quantitySold: number;
soldCost: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
currencyCode: string;
}
export type IPurchasesByItemsSheetData = {
items: IPurchasesByItemsItem[];
total: IPurchasesByItemsTotal;
};
export interface IPurchasesByItemsSheet {
data: IPurchasesByItemsSheetData;
query: IPurchasesByItemsReportQuery;
meta: IPurchasesByItemsSheetMeta;
}
export interface IPurchasesByItemsTable extends IFinancialTable {
query: IPurchasesByItemsReportQuery;
meta: IPurchasesByItemsSheetMeta;
}

View File

@@ -1,6 +1,7 @@
import { Knex } from 'knex';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
export interface ISaleEstimate {
id?: number;
@@ -124,3 +125,11 @@ export interface ISaleEstimateApprovedEvent {
saleEstimate: ISaleEstimate;
trx: Knex.Transaction;
}
export interface SaleEstimateMailOptions extends CommonMailOptions {
attachEstimate?: boolean;
}
export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
attachEstimate?: boolean;
}

View File

@@ -1,5 +1,6 @@
import { Knex } from 'knex';
import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
@@ -186,3 +187,17 @@ export enum SaleInvoiceAction {
Writeoff = 'Writeoff',
NotifyBySms = 'NotifyBySms',
}
export interface SaleInvoiceMailOptions extends CommonMailOptions {
attachInvoice: boolean;
}
export interface SendInvoiceMailDTO extends CommonMailOptionsDTO {
attachInvoice?: boolean;
}
export interface ISaleInvoiceNotifyPayload {
tenantId: number;
saleInvoiceId: number;
messageDTO: SendInvoiceMailDTO;
}

View File

@@ -1,5 +1,6 @@
import { Knex } from 'knex';
import { IItemEntry } from './ItemEntry';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
export interface ISaleReceipt {
id?: number;
@@ -134,3 +135,11 @@ export interface ISaleReceiptDeletingPayload {
oldSaleReceipt: ISaleReceipt;
trx: Knex.Transaction;
}
export interface SaleReceiptMailOpts extends CommonMailOptions {
attachReceipt: boolean;
}
export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO {
attachReceipt?: boolean;
}

View File

@@ -1,45 +1,54 @@
import {
INumberFormatQuery,
} from './FinancialStatements';
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface ISalesByItemsReportQuery {
fromDate: Date | string;
toDate: Date | string;
itemsIds: number[],
itemsIds: number[];
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
onlyActive: boolean;
};
onlyActive: boolean;
}
export interface ISalesByItemsSheetMeta {
organizationName: string,
baseCurrency: string,
};
organizationName: string;
baseCurrency: string;
}
export interface ISalesByItemsItem {
id: number,
name: string,
code: string,
quantitySold: number,
soldCost: number,
averageSellPrice: number,
id: number;
name: string;
code: string;
quantitySold: number;
soldCost: number;
averageSellPrice: number;
quantitySoldFormatted: string,
soldCostFormatted: string,
averageSellPriceFormatted: string,
currencyCode: string,
};
quantitySoldFormatted: string;
soldCostFormatted: string;
averageSellPriceFormatted: string;
currencyCode: string;
}
export interface ISalesByItemsTotal {
quantitySold: number,
soldCost: number,
quantitySoldFormatted: string,
soldCostFormatted: string,
currencyCode: string,
quantitySold: number;
soldCost: number;
quantitySoldFormatted: string;
soldCostFormatted: string;
currencyCode: string;
}
export type ISalesByItemsSheetData = {
items: ISalesByItemsItem[];
total: ISalesByItemsTotal;
};
export type ISalesByItemsSheetStatement = {
items: ISalesByItemsItem[],
total: ISalesByItemsTotal
} | {};
export interface ISalesByItemsSheet {
data: ISalesByItemsSheetData;
query: ISalesByItemsReportQuery;
meta: ISalesByItemsSheetMeta;
}
export interface ISalesByItemsTable extends IFinancialTable {
query: ISalesByItemsReportQuery;
meta: ISalesByItemsSheetMeta;
}

View File

@@ -0,0 +1,45 @@
import { OpenExchangeRate } from './OpenExchangeRate';
import { ExchangeRateServiceType, IExchangeRateService } from './types';
export class ExchangeRate {
private exchangeRateService: IExchangeRateService;
private exchangeRateServiceType: ExchangeRateServiceType;
/**
* Constructor method.
* @param {ExchangeRateServiceType} service
*/
constructor(service: ExchangeRateServiceType) {
this.exchangeRateServiceType = service;
this.initService();
}
/**
* Initialize the exchange rate service based on the service type.
*/
private initService() {
if (
this.exchangeRateServiceType === ExchangeRateServiceType.OpenExchangeRate
) {
this.setExchangeRateService(new OpenExchangeRate());
}
}
/**
* Sets the exchange rate service.
* @param {IExchangeRateService} service
*/
private setExchangeRateService(service: IExchangeRateService) {
this.exchangeRateService = service;
}
/**
* Gets the latest exchange rate.
* @param {string} baseCurrency
* @param {string} toCurrency
* @returns {number}
*/
public latest(baseCurrency: string, toCurrency: string): Promise<number> {
return this.exchangeRateService.latest(baseCurrency, toCurrency);
}
}

View File

@@ -0,0 +1,81 @@
import Axios, { AxiosError } from 'axios';
import {
EchangeRateErrors,
IExchangeRateService,
OPEN_EXCHANGE_RATE_LATEST_URL,
} from './types';
import config from '@/config';
import { ServiceError } from '@/exceptions';
export class OpenExchangeRate implements IExchangeRateService {
/**
* Gets the latest exchange rate.
* @param {string} baseCurrency
* @param {string} toCurrency
* @returns {Promise<number}
*/
public async latest(
baseCurrency: string,
toCurrency: string
): Promise<number> {
// Vaclidates the Open Exchange Rate api id early.
this.validateApiIdExistance();
try {
const result = await Axios.get(OPEN_EXCHANGE_RATE_LATEST_URL, {
params: {
app_id: config.exchangeRate.openExchangeRate.appId,
base: baseCurrency,
symbols: toCurrency,
},
});
return result.data.rates[toCurrency] || (1 as number);
} catch (error) {
this.handleLatestErrors(error);
}
}
/**
* Validates the Open Exchange Rate api id.
* @throws {ServiceError}
*/
private validateApiIdExistance() {
const apiId = config.exchangeRate.openExchangeRate.appId;
if (!apiId) {
throw new ServiceError(
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.'
);
}
}
/**
* Handles the latest errors.
* @param {any} error
* @throws {ServiceError}
*/
private handleLatestErrors(error: any) {
if (error.response.data?.message === 'missing_app_id') {
throw new ServiceError(
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.'
);
} else if (error.response.data?.message === 'invalid_app_id') {
throw new ServiceError(
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.'
);
} else if (error.response.data?.message === 'not_allowed') {
throw new ServiceError(
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
'Getting the exchange rate from the given base currency to the given currency is not allowed.'
);
} else if (error.response.data?.message === 'invalid_base') {
throw new ServiceError(
EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
'The given base currency is invalid.'
);
}
}
}

View File

@@ -0,0 +1,17 @@
export interface IExchangeRateService {
latest(baseCurrency: string, toCurrency: string): Promise<number>;
}
export enum ExchangeRateServiceType {
OpenExchangeRate = 'OpenExchangeRate',
}
export enum EchangeRateErrors {
EX_RATE_SERVICE_NOT_ALLOWED = 'EX_RATE_SERVICE_NOT_ALLOWED',
EX_RATE_LIMIT_EXCEEDED = 'EX_RATE_LIMIT_EXCEEDED',
EX_RATE_SERVICE_API_KEY_REQUIRED = 'EX_RATE_SERVICE_API_KEY_REQUIRED',
EX_RATE_INVALID_BASE_CURRENCY = 'EX_RATE_INVALID_BASE_CURRENCY',
}
export const OPEN_EXCHANGE_RATE_LATEST_URL =
'https://openexchangerates.org/api/latest.json';

View File

@@ -2,18 +2,13 @@ import fs from 'fs';
import Mustache from 'mustache';
import { Container } from 'typedi';
import path from 'path';
import { IMailable } from '@/interfaces';
interface IMailAttachment {
filename: string;
path: string;
cid: string;
}
import { IMailAttachment } from '@/interfaces';
export default class Mail {
view: string;
subject: string;
to: string;
subject: string = '';
content: string = '';
to: string | string[];
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
data: { [key: string]: string | number };
attachments: IMailAttachment[];
@@ -21,16 +16,24 @@ export default class Mail {
/**
* Mail options.
*/
private get mailOptions() {
public get mailOptions() {
return {
to: this.to,
from: this.from,
subject: this.subject,
html: this.render(this.data),
html: this.html,
attachments: this.attachments,
};
}
/**
* Retrieves the html content of the mail.
* @returns {string}
*/
public get html() {
return this.view ? Mail.render(this.view, this.data) : this.content;
}
/**
* Sends the given mail to the target address.
*/
@@ -52,7 +55,7 @@ export default class Mail {
* Set send mail to address.
* @param {string} to -
*/
setTo(to: string) {
setTo(to: string | string[]) {
this.to = to;
return this;
}
@@ -62,11 +65,16 @@ export default class Mail {
* @param {string} from
* @return {}
*/
private setFrom(from: string) {
setFrom(from: string) {
this.from = from;
return this;
}
/**
* Set attachments to the mail.
* @param {IMailAttachment[]} attachments
* @returns {Mail}
*/
setAttachments(attachments: IMailAttachment[]) {
this.attachments = attachments;
return this;
@@ -95,21 +103,26 @@ export default class Mail {
return this;
}
setContent(content: string) {
this.content = content;
return this;
}
/**
* Renders the view template with the given data.
* @param {object} data
* @return {string}
*/
render(data): string {
const viewContent = this.getViewContent();
static render(view: string, data: Record<string, any>): string {
const viewContent = Mail.getViewContent(view);
return Mustache.render(viewContent, data);
}
/**
* Retrieve view content from the view directory.
*/
private getViewContent(): string {
const filePath = path.join(global.__views_dir, `/${this.view}`);
static getViewContent(view: string): string {
const filePath = path.join(global.__views_dir, `/${view}`);
return fs.readFileSync(filePath, 'utf8');
}
}

View File

@@ -5,6 +5,11 @@ import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries';
import UserInviteMailJob from 'jobs/UserInviteMail';
import OrganizationSetupJob from 'jobs/OrganizationSetup';
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailJob';
import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob';
import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob';
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda);
@@ -13,6 +18,11 @@ export default ({ agenda }: { agenda: Agenda }) => {
new RewriteInvoicesJournalEntries(agenda);
new OrganizationSetupJob(agenda);
new OrganizationUpgrade(agenda);
new SendSaleInvoiceMailJob(agenda);
new SendSaleInvoiceReminderMailJob(agenda);
new SendSaleEstimateMailJob(agenda);
new SaleReceiptMailNotificationJob(agenda);
new PaymentReceiveMailNotificationJob(agenda);
agenda.start();
};

View File

@@ -7,8 +7,12 @@ import {
} from '@/services/Cashflow/utils';
import AccountTransaction from './AccountTransaction';
import { CASHFLOW_DIRECTION } from '@/services/Cashflow/constants';
import { getTransactionTypeLabel } from '@/utils/transactions-types';
export default class CashflowTransaction extends TenantModel {
transactionType: string;
amount: number;
exchangeRate: number;
/**
* Table name.
*/
@@ -55,9 +59,10 @@ export default class CashflowTransaction extends TenantModel {
/**
* Transaction type formatted.
* @returns {string}
*/
get transactionTypeFormatted() {
return AccountTransaction.getReferenceTypeFormatted(this.transactionType);
return getTransactionTypeLabel(this.transactionType);
}
get typeMeta() {

View File

@@ -2,6 +2,9 @@ import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Contact extends TenantModel {
email: string;
displayName: string;
/**
* Table name
*/

View File

@@ -24,6 +24,9 @@ export default class Customer extends mixin(TenantModel, [
CustomViewBaseModel,
ModelSearchable,
]) {
email: string;
displayName: string;
/**
* Query builder.
*/
@@ -76,6 +79,19 @@ export default class Customer extends mixin(TenantModel, [
return 'debit';
}
/**
*
*/
get contactAddresses() {
return [
{
mail: this.email,
label: this.displayName,
primary: true
},
].filter((c) => c.mail);
}
/**
* Model modifiers.
*/

View File

@@ -2,6 +2,8 @@ import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ExpenseCategory extends TenantModel {
amount: number;
/**
* Table name
*/

View File

@@ -3,8 +3,10 @@ import {
IAccount,
IAccountCreateDTO,
IAccountEditDTO,
IAccountResponse,
IAccountsFilter,
IAccountsTransactionsFilter,
IFilterMeta,
IGetAccountTransactionPOJO,
} from '@/interfaces';
import { CreateAccount } from './CreateAccount';
@@ -14,6 +16,7 @@ import { ActivateAccount } from './ActivateAccount';
import { GetAccounts } from './GetAccounts';
import { GetAccount } from './GetAccount';
import { GetAccountTransactions } from './GetAccountTransactions';
@Service()
export class AccountsApplication {
@Inject()
@@ -113,19 +116,22 @@ export class AccountsApplication {
/**
* Retrieves the accounts list.
* @param {number} tenantId
* @param {IAccountsFilter} filterDTO
* @returns
* @param {number} tenantId
* @param {IAccountsFilter} filterDTO
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
*/
public getAccounts = (tenantId: number, filterDTO: IAccountsFilter) => {
public getAccounts = (
tenantId: number,
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
return this.getAccountsService.getAccountsList(tenantId, filterDTO);
};
/**
* Retrieves the given account transactions.
* @param {number} tenantId
* @param {IAccountsTransactionsFilter} filter
* @returns {Promise<IGetAccountTransactionPOJO[]>}
* @param {number} tenantId
* @param {IAccountsTransactionsFilter} filter
* @returns {Promise<IGetAccountTransactionPOJO[]>}
*/
public getAccountsTransactions = (
tenantId: number,

View File

@@ -14,6 +14,7 @@ export class CreditNoteTransformer extends Transformer {
'formattedCreditNoteDate',
'formattedAmount',
'formattedCreditsUsed',
'formattedSubtotal',
'entries',
];
};
@@ -60,6 +61,15 @@ export class CreditNoteTransformer extends Transformer {
});
};
/**
* Retrieves the formatted subtotal.
* @param {ICreditNote} credit
* @returns {string}
*/
protected formattedSubtotal = (credit): string => {
return formatNumber(credit.amount, { money: false });
};
/**
* Retrieves the entries of the credit note.
* @param {ICreditNote} credit

View File

@@ -80,7 +80,7 @@ export default class EditCreditNote extends BaseCreditNotes {
} as ICreditNoteEditingPayload);
// Saves the credit note graph to the storage.
const creditNote = await CreditNote.query(trx).upsertGraph({
const creditNote = await CreditNote.query(trx).upsertGraphAndFetch({
id: creditNoteId,
...creditNoteModel,
});

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
import GetCreditNote from './GetCreditNote';
@Service()
export default class GetCreditNotePdf {
@@ -10,11 +11,19 @@ export default class GetCreditNotePdf {
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getCreditNoteService: GetCreditNote;
/**
* Retrieve sale invoice pdf content.
* @param {} saleInvoice -
* @param {number} tenantId - Tenant id.
* @param {number} creditNoteId - Credit note id.
*/
public async getCreditNotePdf(tenantId: number, creditNote) {
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
const creditNote = await this.getCreditNoteService.getCreditNote(
tenantId,
creditNoteId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/credit-note-standard',

View File

@@ -0,0 +1,21 @@
import { Inject } from 'typedi';
import { ExchangeRatesService } from './ExchangeRatesService';
import { EchangeRateLatestPOJO, ExchangeRateLatestDTO } from '@/interfaces';
export class ExchangeRateApplication {
@Inject()
private exchangeRateService: ExchangeRatesService;
/**
* Gets the latest exchange rate.
* @param {number} tenantId
* @param {ExchangeRateLatestDTO} exchangeRateLatestDTO
* @returns {Promise<EchangeRateLatestPOJO>}
*/
public latest(
tenantId: number,
exchangeRateLatestDTO: ExchangeRateLatestDTO
): Promise<EchangeRateLatestPOJO> {
return this.exchangeRateService.latest(tenantId, exchangeRateLatestDTO);
}
}

View File

@@ -1,193 +1,37 @@
import moment from 'moment';
import { difference } from 'lodash';
import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import {
IExchangeRateDTO,
IExchangeRate,
IExchangeRatesService,
IExchangeRateEditDTO,
IExchangeRateFilter,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
const ERRORS = {
NOT_FOUND_EXCHANGE_RATES: 'NOT_FOUND_EXCHANGE_RATES',
EXCHANGE_RATE_PERIOD_EXISTS: 'EXCHANGE_RATE_PERIOD_EXISTS',
EXCHANGE_RATE_NOT_FOUND: 'EXCHANGE_RATE_NOT_FOUND',
};
import { Service } from 'typedi';
import { ExchangeRate } from '@/lib/ExchangeRate/ExchangeRate';
import { ExchangeRateServiceType } from '@/lib/ExchangeRate/types';
import { EchangeRateLatestPOJO, ExchangeRateLatestDTO } from '@/interfaces';
import { TenantMetadata } from '@/system/models';
@Service()
export default class ExchangeRatesService implements IExchangeRatesService {
@Inject('logger')
logger: any;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@Inject()
tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
export class ExchangeRatesService {
/**
* Creates a new exchange rate.
* Gets the latest exchange rate.
* @param {number} tenantId
* @param {IExchangeRateDTO} exchangeRateDTO
* @returns {Promise<IExchangeRate>}
* @param {number} exchangeRateLatestDTO
* @returns {EchangeRateLatestPOJO}
*/
public async newExchangeRate(
public async latest(
tenantId: number,
exchangeRateDTO: IExchangeRateDTO
): Promise<IExchangeRate> {
const { ExchangeRate } = this.tenancy.models(tenantId);
exchangeRateLatestDTO: ExchangeRateLatestDTO
): Promise<EchangeRateLatestPOJO> {
const organization = await TenantMetadata.query().findOne({ tenantId });
this.logger.info('[exchange_rates] trying to insert new exchange rate.', {
tenantId,
exchangeRateDTO,
});
await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO);
// Assign the organization base currency as a default currency
// if no currency is provided
const fromCurrency =
exchangeRateLatestDTO.fromCurrency || organization.baseCurrency;
const toCurrency =
exchangeRateLatestDTO.toCurrency || organization.baseCurrency;
const exchangeRate = await ExchangeRate.query().insertAndFetch({
...exchangeRateDTO,
date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'),
});
this.logger.info('[exchange_rates] inserted successfully.', {
tenantId,
exchangeRateDTO,
});
return exchangeRate;
}
const exchange = new ExchangeRate(ExchangeRateServiceType.OpenExchangeRate);
const exchangeRate = await exchange.latest(fromCurrency, toCurrency);
/**
* Edits the exchange rate details.
* @param {number} tenantId - Tenant id.
* @param {number} exchangeRateId - Exchange rate id.
* @param {IExchangeRateEditDTO} editExRateDTO - Edit exchange rate DTO.
*/
public async editExchangeRate(
tenantId: number,
exchangeRateId: number,
editExRateDTO: IExchangeRateEditDTO
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying to edit exchange rate.', {
tenantId,
exchangeRateId,
editExRateDTO,
});
await this.validateExchangeRateExistance(tenantId, exchangeRateId);
await ExchangeRate.query()
.where('id', exchangeRateId)
.update({ ...editExRateDTO });
this.logger.info('[exchange_rates] exchange rate edited successfully.', {
tenantId,
exchangeRateId,
editExRateDTO,
});
}
/**
* Deletes the given exchange rate.
* @param {number} tenantId - Tenant id.
* @param {number} exchangeRateId - Exchange rate id.
*/
public async deleteExchangeRate(
tenantId: number,
exchangeRateId: number
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId);
await this.validateExchangeRateExistance(tenantId, exchangeRateId);
await ExchangeRate.query().findById(exchangeRateId).delete();
}
/**
* Listing exchange rates details.
* @param {number} tenantId - Tenant id.
* @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter.
*/
public async listExchangeRates(
tenantId: number,
exchangeRateFilter: IExchangeRateFilter
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
ExchangeRate,
exchangeRateFilter
);
// Retrieve exchange rates by the given query.
const exchangeRates = await ExchangeRate.query()
.onBuild((query) => {
dynamicFilter.buildQuery()(query);
})
.pagination(exchangeRateFilter.page - 1, exchangeRateFilter.pageSize);
return exchangeRates;
}
/**
* Validates period of the exchange rate existance.
* @param {number} tenantId - Tenant id.
* @param {IExchangeRateDTO} exchangeRateDTO - Exchange rate DTO.
* @return {Promise<void>}
*/
private async validateExchangeRatePeriodExistance(
tenantId: number,
exchangeRateDTO: IExchangeRateDTO
): Promise<void> {
const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info('[exchange_rates] trying to validate period existance.', {
tenantId,
});
const foundExchangeRate = await ExchangeRate.query()
.where('currency_code', exchangeRateDTO.currencyCode)
.where('date', exchangeRateDTO.date);
if (foundExchangeRate.length > 0) {
this.logger.info('[exchange_rates] given exchange rate period exists.', {
tenantId,
});
throw new ServiceError(ERRORS.EXCHANGE_RATE_PERIOD_EXISTS);
}
}
/**
* Validate the given echange rate id existance.
* @param {number} tenantId - Tenant id.
* @param {number} exchangeRateId - Exchange rate id.
* @returns {Promise<void>}
*/
private async validateExchangeRateExistance(
tenantId: number,
exchangeRateId: number
) {
const { ExchangeRate } = this.tenancy.models(tenantId);
this.logger.info(
'[exchange_rates] trying to validate exchange rate id existance.',
{ tenantId, exchangeRateId }
);
const foundExchangeRate = await ExchangeRate.query().findById(
exchangeRateId
);
if (!foundExchangeRate) {
this.logger.info('[exchange_rates] exchange rate not found.', {
tenantId,
exchangeRateId,
});
throw new ServiceError(ERRORS.EXCHANGE_RATE_NOT_FOUND);
}
return {
baseCurrency: fromCurrency,
toCurrency: exchangeRateLatestDTO.toCurrency,
exchangeRate,
};
}
}

View File

@@ -136,7 +136,7 @@ export class EditExpense {
} as IExpenseEventEditingPayload);
// Upsert the expense object with expense entries.
const expense: IExpense = await Expense.query(trx).upsertGraph({
const expense: IExpense = await Expense.query(trx).upsertGraphAndFetch({
id: expenseId,
...expenseObj,
});

View File

@@ -0,0 +1,25 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { ExpenseCategory } from '@/models';
import { formatNumber } from '@/utils';
export class ExpenseCategoryTransformer extends Transformer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['amountFormatted'];
};
/**
* Retrieves the formatted amount.
* @param {ExpenseCategory} category
* @returns {string}
*/
protected amountFormatted(category: ExpenseCategory) {
return formatNumber(category.amount, {
currencyCode: this.context.currencyCode,
money: false,
});
}
}

View File

@@ -1,6 +1,7 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { IExpense } from '@/interfaces';
import { ExpenseCategoryTransformer } from './ExpenseCategoryTransformer';
export class ExpenseTransfromer extends Transformer {
/**
@@ -12,7 +13,8 @@ export class ExpenseTransfromer extends Transformer {
'formattedAmount',
'formattedLandedCostAmount',
'formattedAllocatedCostAmount',
'formattedDate'
'formattedDate',
'categories',
];
};
@@ -56,5 +58,16 @@ export class ExpenseTransfromer extends Transformer {
*/
protected formattedDate = (expense: IExpense): string => {
return this.formatDate(expense.paymentDate);
}
};
/**
* Retrieves the transformed expense categories.
* @param {IExpense} expense
* @returns {}
*/
protected categories = (expense: IExpense) => {
return this.item(expense.categories, new ExpenseCategoryTransformer(), {
currencyCode: expense.currencyCode,
});
};
}

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import { ExpenseGLEntries } from './ExpenseGLEntries';
@Service()

View File

@@ -70,10 +70,10 @@ export class ExpensesWriteGLSubscriber {
authorizedUser,
trx,
}: IExpenseEventEditPayload) => {
// In case expense published, write journal entries.
if (expense.publishedAt) return;
// Cannot continue if the expense is not published.
if (!expense.publishedAt) return;
await this.expenseGLEntries.writeExpenseGLEntries(
await this.expenseGLEntries.rewriteExpenseGLEntries(
tenantId,
expense.id,
trx

View File

@@ -10,6 +10,7 @@ import {
IContact,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import moment from 'moment';
/**
* General ledger sheet.
@@ -88,8 +89,10 @@ export default class GeneralLedgerSheet extends FinancialSheet {
const newEntry = {
date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
entryId: entry.id,
transactionNumber: entry.transactionNumber,
referenceType: entry.referenceType,
referenceId: entry.referenceId,
referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted),

View File

@@ -0,0 +1,66 @@
import { Inject } from 'typedi';
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from '@/interfaces';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
import { GeneralLedgerService } from './GeneralLedgerService';
export class GeneralLedgerApplication {
@Inject()
private GLTable: GeneralLedgerTableInjectable;
@Inject()
private GLExport: GeneralLedgerExportInjectable;
@Inject()
private GLSheet: GeneralLedgerService;
/**
* Retrieves the G/L sheet in json format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
*/
public sheet(tenantId: number, query: IGeneralLedgerSheetQuery) {
return this.GLSheet.generalLedger(tenantId, query);
}
/**
* Retrieves the G/L sheet in table format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public table(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<IGeneralLedgerTableData> {
return this.GLTable.table(tenantId, query);
}
/**
* Retrieves the G/L sheet in xlsx format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {}
*/
public xlsx(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<Buffer> {
return this.GLExport.xlsx(tenantId, query);
}
/**
* Retrieves the G/L sheet in csv format.
* @param {number} tenantId -
* @param {IGeneralLedgerSheetQuery} query -
*/
public csv(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<string> {
return this.GLExport.csv(tenantId, query);
}
}

View File

@@ -0,0 +1,43 @@
import { IGeneralLedgerSheetQuery } from '@/interfaces';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { Inject, Service } from 'typedi';
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
@Service()
export class GeneralLedgerExportInjectable {
@Inject()
private generalLedgerTable: GeneralLedgerTableInjectable;
/**
* Retrieves the general ledger sheet in XLSX format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: IGeneralLedgerSheetQuery) {
const table = await this.generalLedgerTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the general ledger sheet in CSV format.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<string> {
const table = await this.generalLedgerTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -15,7 +15,7 @@ const ERRORS = {
};
@Service()
export default class GeneralLedgerService {
export class GeneralLedgerService {
@Inject()
tenancy: TenancyService;
@@ -64,7 +64,7 @@ export default class GeneralLedgerService {
* @param {number} tenantId -
* @returns {IGeneralLedgerMeta}
*/
reportMetadata(tenantId: number): IGeneralLedgerMeta {
reportMetadata(tenantId: number, filter): IGeneralLedgerMeta {
const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService
@@ -78,11 +78,15 @@ export default class GeneralLedgerService {
group: 'organization',
key: 'base_currency',
});
const fromDate = moment(filter.fromDate).format('YYYY MMM DD');
const toDate = moment(filter.toDate).format('YYYY MMM DD');
return {
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
organizationName,
baseCurrency
baseCurrency,
fromDate,
toDate
};
}
@@ -166,7 +170,7 @@ export default class GeneralLedgerService {
return {
data: reportData,
query: filter,
meta: this.reportMetadata(tenantId),
meta: this.reportMetadata(tenantId, filter),
};
}
}

View File

@@ -0,0 +1,256 @@
import * as R from 'ramda';
import {
IColumnMapperMeta,
IGeneralLedgerMeta,
IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountTransaction,
IGeneralLedgerSheetData,
IGeneralLedgerSheetQuery,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { FinancialTable } from '../FinancialTable';
import { tableRowMapper } from '@/utils';
import { ROW_TYPE } from './utils';
export class GeneralLedgerTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IGeneralLedgerSheetData;
private query: IGeneralLedgerSheetQuery;
private meta: IGeneralLedgerMeta;
/**
* Creates an instance of `GeneralLedgerTable`.
* @param {IGeneralLedgerSheetData} data
* @param {IGeneralLedgerSheetQuery} query
*/
constructor(
data: IGeneralLedgerSheetData,
query: IGeneralLedgerSheetQuery,
meta: IGeneralLedgerMeta
) {
super();
this.data = data;
this.query = query;
this.meta = meta;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private accountColumnsAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'name' },
{ key: 'account_name', accessor: '_empty_' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'amount.formattedAmount' },
{ key: 'running_balance', accessor: 'closingBalance.formattedAmount' },
];
}
/**
* Retrieves the transaction column accessors.
* @returns {ITableColumnAccessor[]}
*/
private transactionColumnAccessors(): ITableColumnAccessor[] {
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'account_name', accessor: 'account.name' },
{ key: 'reference_type', accessor: 'referenceTypeFormatted' },
{ key: 'reference_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'amount', accessor: 'formattedAmount' },
{ key: 'running_balance', accessor: 'formattedRunningBalance' },
];
}
/**
* Retrieves the opening row column accessors.
* @returns {ITableRowIColumnMapperMeta[]}
*/
private openingBalanceColumnsAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: this.meta.fromDate },
{ key: 'account_name', value: 'Opening Balance' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'openingBalance.formattedAmount' },
{ key: 'running_balance', accessor: 'openingBalance.formattedAmount' },
];
}
/**
* Closing balance row column accessors.
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceColumnAccessors(): IColumnMapperMeta[] {
return [
{ key: 'date', value: this.meta.toDate },
{ key: 'account_name', value: 'Closing Balance' },
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'closingBalance.formattedAmount' },
{ key: 'running_balance', accessor: 'closingBalance.formattedAmount' },
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'account_name', label: 'Account Name' },
{ key: 'reference_type', label: 'Transaction Type' },
{ key: 'reference_number', label: 'Transaction #' },
{ key: 'description', label: 'Description' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
{ key: 'amount', label: 'Amount' },
{ key: 'running_balance', label: 'Running Balance' },
];
}
/**
* Maps the given transaction node to table row.
* @param {IGeneralLedgerSheetAccountTransaction} transaction
* @returns {ITableRow}
*/
private transactionMapper = R.curry(
(
account: IGeneralLedgerSheetAccount,
transaction: IGeneralLedgerSheetAccountTransaction
): ITableRow => {
const columns = this.transactionColumnAccessors();
const data = { ...transaction, account };
const meta = {
rowTypes: [ROW_TYPE.TRANSACTION],
};
return tableRowMapper(data, columns, meta);
}
);
/**
* Maps the given transactions nodes to table rows.
* @param {IGeneralLedgerSheetAccountTransaction[]} transactions
* @returns {ITableRow[]}
*/
private transactionsMapper = (
account: IGeneralLedgerSheetAccount
): ITableRow[] => {
const transactionMapper = this.transactionMapper(account);
return R.map(transactionMapper)(account.transactions);
};
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private openingBalanceMapper = (
account: IGeneralLedgerSheetAccount
): ITableRow => {
const columns = this.openingBalanceColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.OPENING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to closing balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => {
const columns = this.closingBalanceColumnAccessors();
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to transactions table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow[]}
*/
private transactionsNode = (
account: IGeneralLedgerSheetAccount
): ITableRow[] => {
const openingBalance = this.openingBalanceMapper(account);
const transactions = this.transactionsMapper(account);
const closingBalance = this.closingBalanceMapper(account);
return R.when(
R.always(R.not(R.isEmpty(transactions))),
R.prepend(openingBalance)
)([...transactions, closingBalance]) as ITableRow[];
};
/**
* Maps the given account node to the table rows.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private accountMapper = (account: IGeneralLedgerSheetAccount): ITableRow => {
const columns = this.accountColumnsAccessors();
const transactions = this.transactionsNode(account);
const meta = {
rowTypes: [ROW_TYPE.ACCOUNT],
};
const row = tableRowMapper(account, columns, meta);
return R.assoc('children', transactions)(row);
};
/**
* Maps the given account node to table rows.
* @param {IGeneralLedgerSheetAccount[]} accounts
* @returns {ITableRow[]}
*/
private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[]
): ITableRow[] => {
return this.mapNodesDeep(accounts, this.accountMapper);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
return R.compose(this.accountsMapper)(this.data);
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,45 @@
import {
IGeneralLedgerSheetQuery,
IGeneralLedgerTableData,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { GeneralLedgerService } from './GeneralLedgerService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { GeneralLedgerTable } from './GeneralLedgerTable';
@Service()
export class GeneralLedgerTableInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private GLSheet: GeneralLedgerService;
/**
* Retrieves the G/L table.
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @returns {Promise<IGeneralLedgerTableData>}
*/
public async table(
tenantId: number,
query: IGeneralLedgerSheetQuery
): Promise<IGeneralLedgerTableData> {
const {
data: sheetData,
query: sheetQuery,
meta: sheetMeta,
} = await this.GLSheet.generalLedger(tenantId, query);
const table = new GeneralLedgerTable(sheetData, sheetQuery, sheetMeta);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query: sheetQuery,
meta: sheetMeta,
};
}
}

View File

@@ -0,0 +1,6 @@
export enum ROW_TYPE {
ACCOUNT = 'ACCOUNT',
OPENING_BALANCE = 'OPENING_BALANCE',
TRANSACTION = 'TRANSACTION',
CLOSING_BALANCE = 'CLOSING_BALANCE',
}

View File

@@ -11,7 +11,7 @@ import {
} from '@/interfaces';
import { allPassedConditionsPass, transformToMap } from 'utils';
export default class InventoryValuationSheet extends FinancialSheet {
export class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery;
readonly items: IItem[];
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;
@@ -259,6 +259,6 @@ export default class InventoryValuationSheet extends FinancialSheet {
const items = this.itemsSection();
const total = this.totalSection(items);
return items.length > 0 ? { items, total } : {};
return { items, total };
}
}

View File

@@ -0,0 +1,76 @@
import {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
IInventoryValuationTable,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { InventoryValuationSheetExportable } from './InventoryValuationSheetExportable';
@Service()
export class InventoryValuationSheetApplication {
@Inject()
private inventoryValuationSheet: InventoryValuationSheetService;
@Inject()
private inventoryValuationTable: InventoryValuationSheetTableInjectable;
@Inject()
private inventoryValuationExport: InventoryValuationSheetExportable;
/**
* Retrieves the inventory valuation json format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public sheet(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<IInventoryValuationSheet> {
return this.inventoryValuationSheet.inventoryValuationSheet(
tenantId,
query
);
}
/**
* Retrieves the inventory valuation json table format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<IInventoryValuationTable>}
*/
public table(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<IInventoryValuationTable> {
return this.inventoryValuationTable.table(tenantId, query);
}
/**
* Retrieves the inventory valuation xlsx format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public xlsx(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<Buffer> {
return this.inventoryValuationExport.xlsx(tenantId, query);
}
/**
* Retrieves the inventory valuation csv format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns
*/
public csv(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<string> {
return this.inventoryValuationExport.csv(tenantId, query);
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { IInventoryValuationReportQuery } from '@/interfaces';
import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
@Service()
export class InventoryValuationSheetExportable {
@Inject()
private inventoryValuationTable: InventoryValuationSheetTableInjectable;
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<Buffer> {
const table = await this.inventoryValuationTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the trial balance sheet in CSV format.
* @param {number} tenantId
* @param {IInventoryValuationReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<string> {
const table = await this.inventoryValuationTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -3,15 +3,16 @@ import moment from 'moment';
import { isEmpty } from 'lodash';
import {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import InventoryValuationSheet from './InventoryValuationSheet';
import { InventoryValuationSheet } from './InventoryValuationSheet';
import InventoryService from '@/services/Inventory/Inventory';
import { Tenant } from '@/system/models';
@Service()
export default class InventoryValuationSheetService {
export class InventoryValuationSheetService {
@Inject()
tenancy: TenancyService;
@@ -80,7 +81,7 @@ export default class InventoryValuationSheetService {
public async inventoryValuationSheet(
tenantId: number,
query: IInventoryValuationReportQuery
) {
): Promise<IInventoryValuationSheet> {
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()

View File

@@ -0,0 +1,105 @@
import * as R from 'ramda';
import {
IInventoryValuationItem,
IInventoryValuationSheetData,
IInventoryValuationTotal,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import FinancialSheet from '../FinancialSheet';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { FinancialTable } from '../FinancialTable';
import { ROW_TYPE } from './_constants';
export class InventoryValuationSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private readonly data: IInventoryValuationSheetData;
/**
* Constructor method.
* @param {IInventoryValuationSheetData} data
*/
constructor(data: IInventoryValuationSheetData) {
super();
this.data = data;
}
/**
* Retrieves the common columns accessors.
* @returns {ITableColumnAccessor}
*/
private commonColumnsAccessors(): ITableColumnAccessor[] {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'quantity', accessor: 'quantityFormatted' },
{ key: 'valuation', accessor: 'valuationFormatted' },
{ key: 'average', accessor: 'averageFormatted' },
];
}
/**
* Maps the given total node to table row.
* @param {IInventoryValuationTotal} total
* @returns {ITableRow}
*/
private totalRowMapper = (total: IInventoryValuationTotal): ITableRow => {
const accessors = this.commonColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, accessors, meta);
};
/**
* Maps the given item node to table row.
* @param {IInventoryValuationItem} item
* @returns {ITableRow}
*/
private itemRowMapper = (item: IInventoryValuationItem): ITableRow => {
const accessors = this.commonColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, accessors, meta);
};
/**
* Maps the given items nodes to table rowes.
* @param {IInventoryValuationItem[]} items
* @returns {ITableRow[]}
*/
private itemsRowsMapper = (items: IInventoryValuationItem[]): ITableRow[] => {
return R.map(this.itemRowMapper)(items);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
const itemsRows = this.itemsRowsMapper(this.data.items);
const totalRow = this.totalRowMapper(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
)([...itemsRows]) as ITableRow[];
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = [
{ key: 'item_name', label: 'Item Name' },
{ key: 'quantity', label: 'Quantity' },
{ key: 'valuation', label: 'Valuation' },
{ key: 'average', label: 'Average' },
];
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,39 @@
import { Inject, Service } from 'typedi';
import { InventoryValuationSheetService } from './InventoryValuationSheetService';
import {
IInventoryValuationReportQuery,
IInventoryValuationTable,
} from '@/interfaces';
import { InventoryValuationSheetTable } from './InventoryValuationSheetTable';
@Service()
export class InventoryValuationSheetTableInjectable {
@Inject()
private sheet: InventoryValuationSheetService;
/**
* Retrieves the inventory valuation json table format.
* @param {number} tenantId -
* @param {IInventoryValuationReportQuery} filter -
* @returns {Promise<IInventoryValuationTable>}
*/
public async table(
tenantId: number,
filter: IInventoryValuationReportQuery
): Promise<IInventoryValuationTable> {
const { data, query, meta } = await this.sheet.inventoryValuationSheet(
tenantId,
filter
);
const table = new InventoryValuationSheetTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
query,
meta,
};
}
}

View File

@@ -0,0 +1,4 @@
export enum ROW_TYPE {
ITEM = 'ITEM',
TOTAL = 'TOTAL',
}

View File

@@ -6,8 +6,10 @@ import {
IJournalReportQuery,
IJournalReport,
IContact,
IJournalTableData,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import moment from 'moment';
export default class JournalSheet extends FinancialSheet {
readonly tenantId: number;
@@ -96,6 +98,8 @@ export default class JournalSheet extends FinancialSheet {
return {
date: groupEntry.date,
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
referenceType: groupEntry.referenceType,
referenceId: groupEntry.referenceId,
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
@@ -131,7 +135,7 @@ export default class JournalSheet extends FinancialSheet {
* Retrieve journal report.
* @return {IJournalReport}
*/
reportData(): IJournalReport {
reportData(): IJournalTableData {
return this.entriesWalker(this.journal.entries);
}
}

View File

@@ -0,0 +1,59 @@
import { Inject } from 'typedi';
import { JournalSheetService } from './JournalSheetService';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
import { IJournalReportQuery, IJournalTable } from '@/interfaces';
import { JournalSheetExportInjectable } from './JournalSheetExport';
export class JournalSheetApplication {
@Inject()
private journalSheetTable: JournalSheetTableInjectable;
@Inject()
private journalSheet: JournalSheetService;
@Inject()
private journalExport: JournalSheetExportInjectable;
/**
* Retrieves the journal sheet.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {}
*/
public sheet(tenantId: number, query: IJournalReportQuery) {
return this.journalSheet.journalSheet(tenantId, query);
}
/**
* Retrieves the journal sheet in table format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalTable>}
*/
public table(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalTable> {
return this.journalSheetTable.table(tenantId, query);
}
/**
* Retrieves the journal sheet in xlsx format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns
*/
public xlsx(tenantId: number, query: IJournalReportQuery) {
return this.journalExport.xlsx(tenantId, query);
}
/**
* Retrieves the journal sheet in csv format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns
*/
public csv(tenantId: number, query: IJournalReportQuery) {
return this.journalExport.csv(tenantId, query);
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { IJournalReportQuery } from '@/interfaces';
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
@Service()
export class JournalSheetExportInjectable {
@Inject()
private journalSheetTable: JournalSheetTableInjectable;
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: IJournalReportQuery) {
const table = await this.journalSheetTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the trial balance sheet in CSV format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IJournalReportQuery
): Promise<string> {
const table = await this.journalSheetTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -1,24 +1,25 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
import {
IJournalReportQuery,
IJournalSheet,
IJournalSheetMeta,
IJournalTableData,
} from '@/interfaces';
import JournalSheet from './JournalSheet';
import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean, transformToMap } from 'utils';
import { Tenant } from '@/system/models';
import { parseBoolean, transformToMap } from 'utils';
@Service()
export default class JournalSheetService {
export class JournalSheetService {
@Inject()
tenancy: TenancyService;
private tenancy: TenancyService;
@Inject()
inventoryService: InventoryService;
@Inject('logger')
logger: any;
private inventoryService: InventoryService;
/**
* Default journal sheet filter queyr.
@@ -67,9 +68,13 @@ export default class JournalSheetService {
/**
* Journal sheet.
* @param {number} tenantId
* @param {IJournalSheetFilterQuery} query
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalSheet>}
*/
async journalSheet(tenantId: number, query: IJournalReportQuery) {
async journalSheet(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalSheet> {
const i18n = this.tenancy.i18n(tenantId);
const { accountRepository, transactionsRepository, contactRepository } =
this.tenancy.repositories(tenantId);
@@ -80,11 +85,6 @@ export default class JournalSheetService {
...this.defaultQuery,
...query,
};
this.logger.info('[journal] trying to calculate the report.', {
tenantId,
filter,
});
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');

View File

@@ -0,0 +1,232 @@
import * as R from 'ramda';
import { first } from 'lodash';
import {
IColumnMapperMeta,
IJournalEntry,
IJournalReportEntriesGroup,
IJournalReportQuery,
IJournalTableData,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import { FinancialTable } from '../FinancialTable';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import FinancialSheet from '../FinancialSheet';
import { ROW_TYPE } from './types';
export class JournalSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IJournalTableData;
private query: IJournalReportQuery;
private i18n: any;
/**
* Constructor method.
* @param {IJournalTableData} data
* @param {IJournalReportQuery} query
* @param i18n
*/
constructor(data: IJournalTableData, query: IJournalReportQuery, i18n: any) {
super();
this.data = data;
this.query = query;
this.i18n = i18n;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private groupColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'date', accessor: 'dateFormatted' },
{ key: 'transaction_type', accessor: 'referenceTypeFormatted' },
{ key: 'transaction_number', accessor: 'entry.transactionNumber' },
{ key: 'description', accessor: 'entry.note' },
{ key: 'account_code', accessor: 'entry.accountCode' },
{ key: 'account_name', accessor: 'entry.accountName' },
{ key: 'credit', accessor: 'entry.formattedCredit' },
{ key: 'debit', accessor: 'entry.formattedDebit' },
];
};
/**
* Retrieves the group entry accessors.
* @returns {ITableColumnAccessor[]}
*/
private entryColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'date', accessor: '_empty_' },
{ key: 'transaction_type', accessor: '_empty_' },
{ key: 'transaction_number', accessor: 'transactionNumber' },
{ key: 'description', accessor: 'note' },
{ key: 'account_code', accessor: 'accountCode' },
{ key: 'account_name', accessor: 'accountName' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
];
};
/**
* Retrieves the total entry column accessors.
* @returns {ITableColumnAccessor[]}
*/
private totalEntryColumnAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'date', accessor: '_empty_' },
{ key: 'transaction_type', accessor: '_empty_' },
{ key: 'transaction_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'account_code', accessor: '_empty_' },
{ key: 'account_name', accessor: '_empty_' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' },
];
};
/**
* Retrieves the total entry column accessors.
* @returns {IColumnMapperMeta[]}
*/
private blankEnrtyColumnAccessors = (): IColumnMapperMeta[] => {
return [
{ key: 'date', value: '' },
{ key: 'transaction_type', value: '' },
{ key: 'transaction_number', value: '' },
{ key: 'description', value: '' },
{ key: 'account_code', value: '' },
{ key: 'account_name', value: '' },
{ key: 'credit', value: '' },
{ key: 'debit', value: '' },
];
};
/**
* Retrieves the common columns.
* @returns {ITableColumn[]}
*/
private commonColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'transaction_type', label: 'Transaction Type' },
{ key: 'transaction_number', label: 'Num.' },
{ key: 'description', label: 'Description' },
{ key: 'account_code', label: 'Acc. Code' },
{ key: 'account_name', label: 'Account' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
];
}
/**
* Maps the group and first entry to table row.
* @param {IJournalReportEntriesGroup} group
* @returns {ITableRow}
*/
private firstEntryGroupMapper = (
group: IJournalReportEntriesGroup
): ITableRow => {
const meta = {
rowTypes: [ROW_TYPE.ENTRY],
};
const computedGroup = { ...group, entry: first(group.entries) };
const columns = this.groupColumnsAccessors();
return tableRowMapper(computedGroup, columns, meta);
};
/**
* Maps the given group entry to table rows.
* @param {IJournalEntry} entry
* @returns {ITableRow}
*/
private entryMapper = (entry: IJournalEntry): ITableRow => {
const columns = this.entryColumnsAccessors();
const meta = {
rowTypes: [ROW_TYPE.ENTRY],
};
return tableRowMapper(entry, columns, meta);
};
/**
* Maps the given group entries to table rows.
* @param {IJournalReportEntriesGroup} group
* @returns {ITableRow[]}
*/
private entriesMapper = (group: IJournalReportEntriesGroup): ITableRow[] => {
const entries = R.remove(0, 1, group.entries);
return R.map(this.entryMapper, entries);
};
/**
* Maps the given group entry to total table row.
* @param {IJournalReportEntriesGroup} group
* @returns {ITableRow}
*/
public totalEntryMapper = (group: IJournalReportEntriesGroup): ITableRow => {
const total = this.totalEntryColumnAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(group, total, meta);
};
/**
* Retrieves the blank entry row.
* @returns {ITableRow}
*/
private blankEntryMapper = (): ITableRow => {
const columns = this.blankEnrtyColumnAccessors();
const meta = {};
return tableRowMapper({} as IJournalEntry, columns, meta);
};
/**
* Maps the entry group to table rows.
* @param {IJournalReportEntriesGroup} group -
* @returns {ITableRow}
*/
private groupMapper = (group: IJournalReportEntriesGroup): ITableRow[] => {
const firstRow = this.firstEntryGroupMapper(group);
const lastRows = this.entriesMapper(group);
const totalRow = this.totalEntryMapper(group);
const blankRow = this.blankEntryMapper();
return [firstRow, ...lastRows, totalRow, blankRow];
};
/**
* Maps the given group entries to table rows.
* @param {IJournalReportEntriesGroup[]} entries -
* @returns {ITableRow[]}
*/
private groupsMapper = (
entries: IJournalReportEntriesGroup[]
): ITableRow[] => {
return R.compose(R.flatten, R.map(this.groupMapper))(entries);
};
/**
* Retrieves the table data rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
return R.compose(this.groupsMapper)(this.data);
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,39 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject } from 'typedi';
import { JournalSheetService } from './JournalSheetService';
import { IJournalReportQuery, IJournalTable } from '@/interfaces';
import { JournalSheetTable } from './JournalSheetTable';
export class JournalSheetTableInjectable {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private journalSheetService: JournalSheetService;
/**
* Retrieves the journal sheet in table format.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalTable>}
*/
public async table(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalTable> {
const journal = await this.journalSheetService.journalSheet(
tenantId,
query
);
const table = new JournalSheetTable(journal.data, journal.query, {});
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
query: journal.query,
meta: journal.meta,
};
}
}

View File

@@ -0,0 +1,5 @@
export enum ROW_TYPE {
ENTRY = 'ENTRY',
TOTAL = 'TOTAL'
};

View File

@@ -2,36 +2,34 @@ import { get, isEmpty, sumBy } from 'lodash';
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, transformToMap } from 'utils';
import { IAccountTransaction, IItem } from '@/interfaces';
import {
IAccountTransaction,
IInventoryValuationTotal,
IInventoryValuationItem,
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IItem,
} from '@/interfaces';
IPurchasesByItemsItem,
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from '@/interfaces/PurchasesByItemsSheet';
export default class InventoryValuationReport extends FinancialSheet {
export class PurchasesByItems extends FinancialSheet {
readonly baseCurrency: string;
readonly items: IItem[];
readonly itemsTransactions: Map<number, IAccountTransaction>;
readonly query: IInventoryValuationReportQuery;
readonly query: IPurchasesByItemsReportQuery;
/**
* Constructor method.
* @param {IInventoryValuationReportQuery} query
* @param {IPurchasesByItemsReportQuery} query
* @param {IItem[]} items
* @param {IAccountTransaction[]} itemsTransactions
* @param {string} baseCurrency
*/
constructor(
query: IInventoryValuationReportQuery,
query: IPurchasesByItemsReportQuery,
items: IItem[],
itemsTransactions: IAccountTransaction[],
baseCurrency: string
) {
super();
this.baseCurrency = baseCurrency;
this.items = items;
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
@@ -98,7 +96,7 @@ export default class InventoryValuationReport extends FinancialSheet {
* @param {IInventoryValuationItem} item
* @returns
*/
private itemSectionMapper = (item: IItem): IInventoryValuationItem => {
private itemSectionMapper = (item: IItem): IPurchasesByItemsItem => {
const meta = this.getItemTransaction(item.id);
return {
@@ -145,9 +143,9 @@ export default class InventoryValuationReport extends FinancialSheet {
/**
* Retrieve the items sections.
* @returns {IInventoryValuationItem[]}
* @returns {IPurchasesByItemsItem[]}
*/
private itemsSection = (): IInventoryValuationItem[] => {
private itemsSection = (): IPurchasesByItemsItem[] => {
return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter),
this.itemsMapper
@@ -156,10 +154,10 @@ export default class InventoryValuationReport extends FinancialSheet {
/**
* Retrieve the total section of the sheet.
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
* @param {IPurchasesByItemsItem[]} items
* @returns {IPurchasesByItemsTotal}
*/
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
private totalSection(items: IPurchasesByItemsItem[]): IPurchasesByItemsTotal {
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
@@ -176,12 +174,12 @@ export default class InventoryValuationReport extends FinancialSheet {
/**
* Retrieve the sheet data.
* @returns
* @returns {IInventoryValuationStatement}
*/
reportData(): IInventoryValuationStatement {
public reportData(): IPurchasesByItemsSheetData {
const items = this.itemsSection();
const total = this.totalSection(items);
return items.length > 0 ? { items, total } : {};
return { items, total };
}
}

View File

@@ -0,0 +1,73 @@
import { Service, Inject } from 'typedi';
import { PurchasesByItemsExport } from './PurchasesByItemsExport';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsTable,
} from '@/interfaces/PurchasesByItemsSheet';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { PurchasesByItemsService } from './PurchasesByItemsService';
@Service()
export class PurcahsesByItemsApplication {
@Inject()
private purchasesByItemsSheet: PurchasesByItemsService;
@Inject()
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
@Inject()
private purchasesByItemsExport: PurchasesByItemsExport;
/**
* Retrieves the purchases by items in json format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns
*/
public sheet(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsSheet> {
return this.purchasesByItemsSheet.purchasesByItems(tenantId, query);
}
/**
* Retrieves the purchases by items in table format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<IPurchasesByItemsTable>}
*/
public table(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsTable> {
return this.purchasesByItemsTable.table(tenantId, query);
}
/**
* Retrieves the purchases by items in csv format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<string>}
*/
public csv(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<string> {
return this.purchasesByItemsExport.csv(tenantId, query);
}
/**
* Retrieves the purchases by items in xlsx format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public xlsx(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<Buffer> {
return this.purchasesByItemsExport.xlsx(tenantId, query);
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
import { IPurchasesByItemsReportQuery } from '@/interfaces/PurchasesByItemsSheet';
@Service()
export class PurchasesByItemsExport {
@Inject()
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
/**
* Retrieves the purchases by items sheet in XLSX format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<Buffer> {
const table = await this.purchasesByItemsTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the purchases by items sheet in CSV format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<string> {
const table = await this.purchasesByItemsTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -1,24 +1,24 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import {
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import { Service, Inject } from 'typedi';
import TenancyService from '@/services/Tenancy/TenancyService';
import PurchasesByItems from './PurchasesByItems';
import { PurchasesByItems } from './PurchasesByItems';
import { Tenant } from '@/system/models';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsSheetMeta,
} from '@/interfaces/PurchasesByItemsSheet';
@Service()
export default class InventoryValuationReportService {
export class PurchasesByItemsService {
@Inject()
private tenancy: TenancyService;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
* Defaults purchases by items filter query.
* @return {IPurchasesByItemsReportQuery}
*/
get defaultQuery(): IInventoryValuationReportQuery {
get defaultQuery(): IPurchasesByItemsReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
@@ -40,7 +40,7 @@ export default class InventoryValuationReportService {
* @param {number} tenantId -
* @returns {IBalanceSheetMeta}
*/
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
reportMetadata(tenantId: number): IPurchasesByItemsSheetMeta {
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
@@ -62,18 +62,13 @@ export default class InventoryValuationReportService {
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*
* @return {IBalanceSheetStatement}
* @param {IPurchasesByItemsReportQuery} query
* @return {Promise<IPurchasesByItemsSheet>}
*/
public async purchasesByItems(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<{
data: IInventoryValuationStatement;
query: IInventoryValuationReportQuery;
meta: IInventoryValuationSheetMeta;
}> {
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsSheet> {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -106,7 +101,6 @@ export default class InventoryValuationReportService {
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
}
);
const purchasesByItemsInstance = new PurchasesByItems(
filter,
inventoryItems,

View File

@@ -0,0 +1,111 @@
import * as R from 'ramda';
import { ITableColumn, ITableColumnAccessor, ITableRow } from '@/interfaces';
import { ROW_TYPE } from './_types';
import { tableRowMapper } from '@/utils';
import { FinancialTable } from '../FinancialTable';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import FinancialSheet from '../FinancialSheet';
import {
IPurchasesByItemsItem,
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from '@/interfaces/PurchasesByItemsSheet';
export class PurchasesByItemsTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private data: IPurchasesByItemsSheetData;
/**
* Constructor method.
* @param data
*/
constructor(data) {
super();
this.data = data;
}
/**
* Retrieves thge common table accessors.
* @returns {ITableColumnAccessor[]}
*/
private commonTableAccessors(): ITableColumnAccessor[] {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'quantity_purchases', accessor: 'quantityPurchasedFormatted' },
{ key: 'purchase_amount', accessor: 'purchaseCostFormatted' },
{ key: 'average_cost', accessor: 'averageCostPriceFormatted' },
];
}
/**
* Retrieves the common table columns.
* @returns {ITableColumn[]}
*/
private commonTableColumns(): ITableColumn[] {
return [
{ label: 'Item name', key: 'item_name' },
{ label: 'Quantity Purchased', key: 'quantity_purchases' },
{ label: 'Purchase Amount', key: 'purchase_amount' },
{ label: 'Average Price', key: 'average_cost' },
];
}
/**
* Maps the given item node to table row.
* @param {IPurchasesByItemsItem} item
* @returns {ITableRow}
*/
private itemMap = (item: IPurchasesByItemsItem): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, columns, meta);
};
/**
* Maps the given items nodes to table rows.
* @param {IPurchasesByItemsItem[]} items - Items nodes.
* @returns {ITableRow[]}
*/
private itemsMap = (items: IPurchasesByItemsItem[]): ITableRow[] => {
return R.map(this.itemMap)(items);
};
/**
* Maps the given total node to table rows.
* @param {IPurchasesByItemsTotal} total
* @returns {ITableRow}
*/
private totalNodeMap = (total: IPurchasesByItemsTotal): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, columns, meta);
};
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = this.commonTableColumns();
return R.compose(this.tableColumnsCellIndexing)(columns);
}
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
const itemsRows = this.itemsMap(this.data.items);
const totalRow = this.totalNodeMap(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
)(itemsRows) as ITableRow[];
}
}

View File

@@ -0,0 +1,38 @@
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsTable,
} from '@/interfaces/PurchasesByItemsSheet';
import { Inject, Service } from 'typedi';
import { PurchasesByItemsService } from './PurchasesByItemsService';
import { PurchasesByItemsTable } from './PurchasesByItemsTable';
@Service()
export class PurchasesByItemsTableInjectable {
@Inject()
private purchasesByItemsSheet: PurchasesByItemsService;
/**
* Retrieves the purchases by items table format.
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} filter
* @returns {Promise<IPurchasesByItemsTable>}
*/
public async table(
tenantId: number,
filter: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsTable> {
const { data, query, meta } =
await this.purchasesByItemsSheet.purchasesByItems(tenantId, filter);
const table = new PurchasesByItemsTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
meta,
query,
};
}
}

View File

@@ -0,0 +1,5 @@
export enum ROW_TYPE {
TOTAL = 'TOTAL',
ITEM = 'ITEM'
}

View File

@@ -7,7 +7,7 @@ import {
IAccountTransaction,
ISalesByItemsItem,
ISalesByItemsTotal,
ISalesByItemsSheetStatement,
ISalesByItemsSheetData,
IItem,
} from '@/interfaces';
@@ -146,7 +146,7 @@ export default class SalesByItemsReport extends FinancialSheet {
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
*/
totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
private totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
const quantitySold = sumBy(items, (item) => item.quantitySold);
const soldCost = sumBy(items, (item) => item.soldCost);
@@ -163,12 +163,12 @@ export default class SalesByItemsReport extends FinancialSheet {
/**
* Retrieve the sheet data.
* @returns {ISalesByItemsSheetStatement}
* @returns {ISalesByItemsSheetData}
*/
reportData(): ISalesByItemsSheetStatement {
public reportData(): ISalesByItemsSheetData {
const items = this.itemsSection();
const total = this.totalSection(items);
return items.length > 0 ? { items, total } : {};
return { items, total };
}
}

View File

@@ -0,0 +1,74 @@
import { Inject, Service } from 'typedi';
import {
ISalesByItemsReportQuery,
ISalesByItemsSheet,
ISalesByItemsSheetData,
ISalesByItemsTable,
} from '@/interfaces';
import { SalesByItemsReportService } from './SalesByItemsService';
import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
import { SalesByItemsExport } from './SalesByItemsExport';
@Service()
export class SalesByItemsApplication {
@Inject()
private salesByItemsSheet: SalesByItemsReportService;
@Inject()
private salesByItemsTable: SalesByItemsTableInjectable;
@Inject()
private salesByItemsExport: SalesByItemsExport;
/**
* Retrieves the sales by items report in json format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} filter
* @returns {Promise<ISalesByItemsSheetData>}
*/
public sheet(
tenantId: number,
filter: ISalesByItemsReportQuery
): Promise<ISalesByItemsSheet> {
return this.salesByItemsSheet.salesByItems(tenantId, filter);
}
/**
* Retrieves the sales by items report in table format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} filter
* @returns {Promise<ISalesByItemsTable>}
*/
public table(
tenantId: number,
filter: ISalesByItemsReportQuery
): Promise<ISalesByItemsTable> {
return this.salesByItemsTable.table(tenantId, filter);
}
/**
* Retrieves the sales by items report in csv format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} filter
* @returns {Promise<string>}
*/
public csv(
tenantId: number,
filter: ISalesByItemsReportQuery
): Promise<string> {
return this.salesByItemsExport.csv(tenantId, filter);
}
/**
* Retrieves the sales by items report in xlsx format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} filter
* @returns {Promise<Buffer>}
*/
public xlsx(
tenantId: number,
filter: ISalesByItemsReportQuery
): Promise<Buffer> {
return this.salesByItemsExport.xlsx(tenantId, filter);
}
}

View File

@@ -0,0 +1,43 @@
import { Inject, Service } from 'typedi';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { ISalesByItemsReportQuery } from '@/interfaces';
import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
@Service()
export class SalesByItemsExport {
@Inject()
private salesByItemsTable: SalesByItemsTableInjectable;
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public async xlsx(tenantId: number, query: ISalesByItemsReportQuery) {
const table = await this.salesByItemsTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToXLSX();
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
}
/**
* Retrieves the trial balance sheet in CSV format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} query
* @returns {Promise<Buffer>}
*/
public async csv(
tenantId: number,
query: ISalesByItemsReportQuery
): Promise<string> {
const table = await this.salesByItemsTable.table(tenantId, query);
const tableSheet = new TableSheet(table.table);
const tableCsv = tableSheet.convertToCSV();
return tableCsv;
}
}

View File

@@ -2,15 +2,15 @@ import { Service, Inject } from 'typedi';
import moment from 'moment';
import {
ISalesByItemsReportQuery,
ISalesByItemsSheetStatement,
ISalesByItemsSheetMeta
ISalesByItemsSheetMeta,
ISalesByItemsSheet,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import SalesByItems from './SalesByItems';
import { Tenant } from '@/system/models';
@Service()
export default class SalesByItemsReportService {
export class SalesByItemsReportService {
@Inject()
tenancy: TenancyService;
@@ -63,20 +63,14 @@ export default class SalesByItemsReportService {
/**
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*
* @return {IBalanceSheetStatement}
* @return {Promise<ISalesByItemsSheet>}
*/
public async salesByItems(
tenantId: number,
query: ISalesByItemsReportQuery
): Promise<{
data: ISalesByItemsSheetStatement,
query: ISalesByItemsReportQuery,
meta: ISalesByItemsSheetMeta,
}> {
): Promise<ISalesByItemsSheet> {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -107,20 +101,19 @@ export default class SalesByItemsReportService {
builder.whereIn('itemId', inventoryItemsIds);
// Filter the date range of the sheet.
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
}
);
const purchasesByItemsInstance = new SalesByItems(
const sheet = new SalesByItems(
filter,
inventoryItems,
inventoryTransactions,
tenant.metadata.baseCurrency,
tenant.metadata.baseCurrency
);
const purchasesByItemsData = purchasesByItemsInstance.reportData();
const salesByItemsData = sheet.reportData();
return {
data: purchasesByItemsData,
data: salesByItemsData,
query: filter,
meta: this.reportMetadata(tenantId),
};

View File

@@ -0,0 +1,104 @@
import * as R from 'ramda';
import {
ISalesByItemsItem,
ISalesByItemsSheetStatement,
ISalesByItemsTotal,
ITableColumn,
ITableRow,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import FinancialSheet from '../FinancialSheet';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { FinancialTable } from '../FinancialTable';
import { ROW_TYPE } from './constants';
export class SalesByItemsTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
private readonly data: ISalesByItemsSheetStatement;
/**
* Constructor method.
* @param {ISalesByItemsSheetStatement} data
*/
constructor(data: ISalesByItemsSheetStatement) {
super();
this.data = data;
}
/**
* Retrieves the common table accessors.
* @returns {ITableColumn[]}
*/
private commonTableAccessors() {
return [
{ key: 'item_name', accessor: 'name' },
{ key: 'sold_quantity', accessor: 'quantitySoldFormatted' },
{ key: 'sold_amount', accessor: 'soldCostFormatted' },
{ key: 'average_price', accessor: 'averageSellPriceFormatted' },
];
}
/**
* Maps the given item node to table row.
* @param {ISalesByItemsItem} item
* @returns {ITableRow}
*/
private itemMap = (item: ISalesByItemsItem): ITableRow => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.ITEM],
};
return tableRowMapper(item, columns, meta);
};
/**
* Maps the given items nodes to table rows.
* @param {ISalesByItemsItem[]} items
* @returns {ITableRow[]}
*/
private itemsMap = (items: ISalesByItemsItem[]): ITableRow[] => {
return R.map(this.itemMap, items);
};
/**
* Maps the given total node to table row.
* @param {ISalesByItemsTotal} total
* @returns {ITableRow[]}
*/
private totalMap = (total: ISalesByItemsTotal) => {
const columns = this.commonTableAccessors();
const meta = {
rowTypes: [ROW_TYPE.TOTAL],
};
return tableRowMapper(total, columns, meta);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
const itemsRows = this.itemsMap(this.data.items);
const totalRow = this.totalMap(this.data.total);
return R.compose(
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
)([...itemsRows]) as ITableRow[];
}
/**
* Retrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
const columns = [
{ key: 'item_name', label: 'Item name' },
{ key: 'sold_quantity', label: 'Sold quantity' },
{ key: 'sold_amount', label: 'Sold amount' },
{ key: 'average_price', label: 'Average price' },
];
return R.compose(this.tableColumnsCellIndexing)(columns);
}
}

View File

@@ -0,0 +1,33 @@
import { Inject, Service } from 'typedi';
import { ISalesByItemsReportQuery } from '@/interfaces';
import { SalesByItemsReportService } from './SalesByItemsService';
import { SalesByItemsTable } from './SalesByItemsTable';
@Service()
export class SalesByItemsTableInjectable {
@Inject()
private salesByItemSheet: SalesByItemsReportService;
/**
* Retrieves the sales by items report in table format.
* @param {number} tenantId
* @param {ISalesByItemsReportQuery} filter
* @returns {Promise<ISalesByItemsTable>}
*/
public async table(tenantId: number, filter: ISalesByItemsReportQuery) {
const { data, query, meta } = await this.salesByItemSheet.salesByItems(
tenantId,
filter
);
const table = new SalesByItemsTable(data);
return {
table: {
columns: table.tableColumns(),
rows: table.tableData(),
},
meta,
query,
};
}
}

View File

@@ -0,0 +1,6 @@
export enum ROW_TYPE {
ITEM = 'ITEM',
TOTAL = 'TOTAL',
}

View File

@@ -252,10 +252,6 @@ export default class TrialBalanceSheet extends FinancialSheet {
* @return {ITrialBalanceSheetData}
*/
public reportData(): ITrialBalanceSheetData {
// Don't return noting if the journal has no transactions.
if (this.repository.totalAccountsLedger.isEmpty()) {
return null;
}
// Retrieve accounts nodes.
const accounts = this.accountsSection(this.repository.accounts);

View File

@@ -46,7 +46,7 @@ export class TrialBalanceSheetTable extends R.compose(
this.query = query;
this.i18n = i18n;
}
/**
* Retrieve the common columns for all report nodes.
* @param {ITableColumnAccessor[]}
@@ -123,7 +123,7 @@ export class TrialBalanceSheetTable extends R.compose(
*/
public tableRows = (): ITableRow[] => {
return R.compose(
R.append(this.totalTableRow()),
R.unless(R.isEmpty, R.append(this.totalTableRow())),
R.concat(this.accountsTableRows())
)([]);
};

View File

@@ -0,0 +1,106 @@
import { Inject, Service } from 'typedi';
import { CommonMailOptions } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
import { formatSmsMessage } from '@/utils';
import { Tenant } from '@/system/models';
@Service()
export class ContactMailNotification {
@Inject()
private mailTenancy: MailTenancy;
@Inject()
private tenancy: HasTenancyService;
/**
* Parses the default message options.
* @param {number} tenantId -
* @param {number} invoiceId -
* @param {string} subject -
* @param {string} body -
* @returns {Promise<SaleInvoiceMailOptions>}
*/
public async getDefaultMailOptions(
tenantId: number,
contactId: number,
subject: string = '',
body: string = ''
): Promise<CommonMailOptions> {
const { Customer } = this.tenancy.models(tenantId);
const contact = await Customer.query()
.findById(contactId)
.throwIfNotFound();
const toAddresses = contact.contactAddresses;
const fromAddresses = await this.mailTenancy.senders(tenantId);
const toAddress = toAddresses.find((a) => a.primary);
const fromAddress = fromAddresses.find((a) => a.primary);
const to = toAddress?.mail || '';
const from = fromAddress?.mail || '';
return {
subject,
body,
to,
from,
fromAddresses,
toAddresses,
};
}
/**
* Retrieves the mail options of the given contact.
* @param {number} tenantId - Tenant id.
* @param {number} invoiceId - Invoice id.
* @param {string} defaultSubject - Default subject text.
* @param {string} defaultBody - Default body text.
* @returns {Promise<CommonMailOptions>}
*/
public async getMailOptions(
tenantId: number,
contactId: number,
defaultSubject?: string,
defaultBody?: string,
formatterData?: Record<string, any>
): Promise<CommonMailOptions> {
const mailOpts = await this.getDefaultMailOptions(
tenantId,
contactId,
defaultSubject,
defaultBody
);
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
const formatArgs = {
...commonFormatArgs,
...formatterData,
};
const subject = formatSmsMessage(mailOpts.subject, formatArgs);
const body = formatSmsMessage(mailOpts.body, formatArgs);
return {
...mailOpts,
subject,
body,
};
}
/**
* Retrieves the common format args.
* @param {number} tenantId
* @returns {Promise<Record<string, string>>}
*/
public async getCommonFormatArgs(
tenantId: number
): Promise<Record<string, string>> {
const organization = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
return {
CompanyName: organization.metadata.name,
};
}
}

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
MAIL_FROM_NOT_FOUND: 'Mail from address not found',
MAIL_TO_NOT_FOUND: 'Mail to address not found',
MAIL_SUBJECT_NOT_FOUND: 'Mail subject not found',
MAIL_BODY_NOT_FOUND: 'Mail body not found',
};

View File

@@ -0,0 +1,33 @@
import { isEmpty } from 'lodash';
import { ServiceError } from '@/exceptions';
import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces';
import { ERRORS } from './constants';
/**
* Merges the mail options with incoming options.
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
* @throws {ServiceError}
*/
export function parseAndValidateMailOptions(
mailOptions: Partial<CommonMailOptions>,
overridedOptions: Partial<CommonMailOptionsDTO>
) {
const mergedMessageOptions = {
...mailOptions,
...overridedOptions,
};
if (isEmpty(mergedMessageOptions.from)) {
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.to)) {
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.subject)) {
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
}
if (isEmpty(mergedMessageOptions.body)) {
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
}
return mergedMessageOptions;
}

View File

@@ -0,0 +1,25 @@
import config from '@/config';
import { Tenant } from "@/system/models";
import { Service } from 'typedi';
@Service()
export class MailTenancy {
/**
* Retrieves the senders mails of the given tenant.
* @param {number} tenantId
*/
public async senders(tenantId: number) {
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
return [
{
mail: config.mail.from,
label: tenant.metadata.name,
primary: true,
}
].filter((item) => item.mail)
}
}

View File

@@ -10,7 +10,6 @@ import {
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
import { BillPayment } from '@/models';
import { ERRORS } from './constants';
@Service()
@@ -18,19 +17,6 @@ export class BillPaymentValidators {
@Inject()
private tenancy: TenancyService;
/**
* Validates the payment existance.
* @param {BillPayment | undefined | null} payment
* @throws {ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND)}
*/
public async validateBillPaymentExistance(
payment: BillPayment | undefined | null
) {
if (!payment) {
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
}
}
/**
* Validates the bill payment existance.
* @param {Request} req

View File

@@ -1,6 +1,5 @@
import { Knex } from 'knex';
import UnitOfWork from '@/services/UnitOfWork';
import { BillPaymentValidators } from './BillPaymentValidators';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@@ -21,9 +20,6 @@ export class DeleteBillPayment {
@Inject()
private uow: UnitOfWork;
@Inject()
private validators: BillPaymentValidators;
/**
* Deletes the bill payment and associated transactions.
* @param {number} tenantId - Tenant id.
@@ -36,10 +32,8 @@ export class DeleteBillPayment {
// Retrieve the bill payment or throw not found service error.
const oldBillPayment = await BillPayment.query()
.withGraphFetched('entries')
.findById(billPaymentId);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(oldBillPayment);
.findById(billPaymentId)
.throwIfNotFound();
// Deletes the bill transactions with associated transactions under
// unit-of-work envirement.

View File

@@ -57,12 +57,12 @@ export class EditBillPayment {
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
const oldBillPayment = await BillPayment.query().findById(billPaymentId);
const oldBillPayment = await BillPayment.query()
.findById(billPaymentId)
.withGraphFetched('entries')
.throwIfNotFound();
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(oldBillPayment);
//
// Retrieves the bill payment vendor or throw not found error.
const vendor = await Contact.query()
.modify('vendor')
.findById(billPaymentDTO.vendorId)
@@ -126,7 +126,7 @@ export class EditBillPayment {
trx,
} as IBillPaymentEditingPayload);
// Deletes the bill payment transaction graph from the storage.
// Edits the bill payment transaction graph on the storage.
const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({
id: billPaymentId,
...billPaymentObj,

View File

@@ -1,12 +1,8 @@
import { IBillPayment } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
import { ServiceError } from '@/exceptions';
import { BillPaymentTransformer } from './BillPaymentTransformer';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import { BillsValidators } from '../Bills/BillsValidators';
import { BillPaymentValidators } from './BillPaymentValidators';
@Service()
export class GetBillPayment {
@@ -16,9 +12,6 @@ export class GetBillPayment {
@Inject()
private transformer: TransformerInjectable;
@Inject()
private validators: BillPaymentValidators;
/**
* Retrieve bill payment.
* @param {number} tenantId
@@ -37,10 +30,8 @@ export class GetBillPayment {
.withGraphFetched('paymentAccount')
.withGraphFetched('transactions')
.withGraphFetched('branch')
.findById(billPyamentId);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(billPayment);
.findById(billPyamentId)
.throwIfNotFound();
return this.transformer.transform(
tenantId,

View File

@@ -18,10 +18,9 @@ export class GetPaymentBills {
public async getPaymentBills(tenantId: number, billPaymentId: number) {
const { Bill, BillPayment } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query().findById(billPaymentId);
// Validates the bill payment existance.
this.validators.validateBillPaymentExistance(billPayment);
const billPayment = await BillPayment.query()
.findById(billPaymentId)
.throwIfNotFound();
const paymentBillsIds = billPayment.entries.map((entry) => entry.id);

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