diff --git a/.all-contributorsrc b/.all-contributorsrc
index 598b7bca4..b037cabba 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -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,
diff --git a/.env.example b/.env.example
index 7a86ab280..72882dd40 100644
--- a/.env.example
+++ b/.env.example
@@ -56,5 +56,42 @@ 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/
\ No newline at end of file
+# 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=
+
+# The Plaid environment to use ('sandbox' or 'development').
+# https://plaid.com/docs/#api-host
+PLAID_ENV=sandbox
+
+# Your Plaid keys, which can be found in the Plaid Dashboard.
+# https://dashboard.plaid.com/account/keys
+PLAID_CLIENT_ID=
+PLAID_SECRET_DEVELOPMENT=
+PLAID_SECRET_SANDBOX=
+
+PLAID_LINK_WEBHOOK=
+
+# (Optional) Redirect URI settings section
+# Only required for OAuth redirect URI testing (not common on desktop):
+# Sandbox Mode:
+# Set the PLAID_SANDBOX_REDIRECT_URI below to 'http://localhost:3001/oauth-link'.
+# The OAuth redirect flow requires an endpoint on the developer's website
+# that the bank website should redirect to. You will also need to configure
+# this redirect URI for your client ID through the Plaid developer dashboard
+# at https://dashboard.plaid.com/team/api.
+# Development mode:
+# When running in development mode, you must use an https:// url.
+# You will need to configure this https:// redirect URI in the Plaid developer dashboard.
+# Instructions to create a self-signed certificate for localhost can be found at
+# https://github.com/plaid/pattern/blob/master/README.md#testing-oauth.
+# If your system is not set up to run localhost with https://, you will be unable to test
+# the OAuth in development and should leave the PLAID_DEVELOPMENT_REDIRECT_URI blank.
+
+PLAID_SANDBOX_REDIRECT_URI=
+PLAID_DEVELOPMENT_REDIRECT_URI=
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b4f3f877..ae75e5f45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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`
diff --git a/README.md b/README.md
index 5476c3201..8f9f0600a 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,12 @@ To get started locally, we have a [guide to help you](https://github.com/bigcapi
[](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.
+
+[](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
 Kalliopi Pliogka 🐛 |
 Robert Koch 💻 |
+
+  Casper Schuijt 🐛 |
+  ANasouf 💻 |
+  Ragnar Laud 🐛 |
+  Asena 🐛 |
+
diff --git a/packages/server/package.json b/packages/server/package.json
index 39bc8a217..ef260cd19 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -94,12 +94,14 @@
"pluralize": "^8.0.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
+ "plaid": "^10.3.0",
"qim": "0.0.52",
"ramda": "^0.27.1",
"rate-limiter-flexible": "^2.1.14",
"reflect-metadata": "^0.1.13",
"rtl-detect": "^1.0.4",
"source-map-loader": "^4.0.1",
+ "socket.io": "^4.7.4",
"tmp-promise": "^3.0.3",
"ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0",
diff --git a/packages/server/resources/scss/modules/financial-sheet.scss b/packages/server/resources/scss/modules/financial-sheet.scss
new file mode 100644
index 000000000..bceb51dc0
--- /dev/null
+++ b/packages/server/resources/scss/modules/financial-sheet.scss
@@ -0,0 +1,57 @@
+@import "../base.scss";
+
+html,
+body {
+ font-size: 14px;
+}
+body{
+ font-weight: 400;
+ letter-spacing: 0;
+ line-height: 1.28581;
+ text-transform: none;
+ color: #000;
+ font-family: Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Icons16, sans-serif;
+}
+.sheet{
+ padding: 20px;
+}
+.sheet__company-name{
+ margin: 0;
+ font-size: 1.4rem;
+}
+.sheet__sheet-type {
+ margin: 0
+}
+.sheet__sheet-date {
+ margin-top: 0.35rem;
+}
+
+.sheet__header {
+ text-align: center;
+ margin-bottom: 1rem;
+}
+
+.sheet__table {
+ border-top: 1px solid #000;
+ table-layout: fixed;
+ border-spacing: 0;
+ text-align: left;
+ font-size: inherit;
+ width: 100%;
+}
+
+.sheet__table thead th {
+ color: #000;
+ border-bottom: 1px solid #000000;
+ padding: 0.5rem;
+}
+
+.sheet__table tbody td {
+ border-bottom: 0;
+ padding-top: 0.28rem;
+ padding-bottom: 0.28rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ color: #252A31;
+ border-bottom: 1px solid transparent;
+}
\ No newline at end of file
diff --git a/packages/server/resources/views/modules/financial-sheet.pug b/packages/server/resources/views/modules/financial-sheet.pug
new file mode 100644
index 000000000..9292c61de
--- /dev/null
+++ b/packages/server/resources/views/modules/financial-sheet.pug
@@ -0,0 +1,25 @@
+block head
+ style
+ include ../../css/modules/financial-sheet.css
+
+style.
+ !{customCSS}
+
+block content
+ .sheet
+ .sheet__header
+ .sheet__company-name=organizationName
+ .sheet__sheet-type=sheetName
+ .sheet__sheet-date=sheetDate
+
+ table.sheet__table
+ thead
+ tr
+ each column in table.columns
+ th(style=column.style class='column--' + column.key)= column.label
+ tbody
+ each row in table.rows
+ tr(class=row.classNames)
+ each cell in row.cells
+ td(class='cell--' + cell.key)
+ span!= cell.value
\ No newline at end of file
diff --git a/packages/server/scripts/gulpConfig.js b/packages/server/scripts/gulpConfig.js
index 51bbc3aff..1caa9af23 100644
--- a/packages/server/scripts/gulpConfig.js
+++ b/packages/server/scripts/gulpConfig.js
@@ -66,12 +66,10 @@ module.exports = {
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
- // {
- // src: './assets/sass/editor-style.scss',
- // dest: './assets/css',
- // sourcemaps: true,
- // minify: true,
- // },
+ {
+ src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`,
+ dest: `${RESOURCES_PATH}/css/modules`,
+ },
],
// RTL builds.
rtl: [
@@ -114,7 +112,7 @@ module.exports = {
// SASS Configuration for all builds.
sass: {
errLogToConsole: true,
- // outputStyle: 'compact',
+ // outputStyle: 'compact',
},
// CSS MQ Packer configuration for all builds and style tasks.
diff --git a/packages/server/src/api/controllers/Banking/BankingController.ts b/packages/server/src/api/controllers/Banking/BankingController.ts
new file mode 100644
index 000000000..27838a285
--- /dev/null
+++ b/packages/server/src/api/controllers/Banking/BankingController.ts
@@ -0,0 +1,18 @@
+import Container, { Inject, Service } from 'typedi';
+import { Router } from 'express';
+import BaseController from '@/api/controllers/BaseController';
+import { PlaidBankingController } from './PlaidBankingController';
+
+@Service()
+export class BankingController extends BaseController {
+ /**
+ * Router constructor.
+ */
+ router() {
+ const router = Router();
+
+ router.use('/plaid', Container.get(PlaidBankingController).router());
+
+ return router;
+ }
+}
diff --git a/packages/server/src/api/controllers/Banking/PlaidBankingController.ts b/packages/server/src/api/controllers/Banking/PlaidBankingController.ts
new file mode 100644
index 000000000..079fdf8bb
--- /dev/null
+++ b/packages/server/src/api/controllers/Banking/PlaidBankingController.ts
@@ -0,0 +1,53 @@
+import { Inject, Service } from 'typedi';
+import { Router, Request, Response } from 'express';
+import BaseController from '@/api/controllers/BaseController';
+import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
+
+@Service()
+export class PlaidBankingController extends BaseController {
+ @Inject()
+ private plaidApp: PlaidApplication;
+
+ /**
+ * Router constructor.
+ */
+ router() {
+ const router = Router();
+
+ router.post('/link-token', this.linkToken.bind(this));
+ router.post('/exchange-token', this.exchangeToken.bind(this));
+
+ return router;
+ }
+
+ /**
+ * Retrieves the Plaid link token.
+ * @param {Request} req
+ * @param {response} res
+ * @returns {Response}
+ */
+ private async linkToken(req: Request, res: Response) {
+ const { tenantId } = req;
+
+ const linkToken = await this.plaidApp.getLinkToken(tenantId);
+
+ return res.status(200).send(linkToken);
+ }
+
+ /**
+ * Exchanges the given public token.
+ * @param {Request} req
+ * @param {response} res
+ * @returns {Response}
+ */
+ public async exchangeToken(req: Request, res: Response) {
+ const { tenantId } = req;
+ const { public_token, institution_id } = req.body;
+
+ await this.plaidApp.exchangeToken(tenantId, {
+ institutionId: institution_id,
+ publicToken: public_token,
+ });
+ return res.status(200).send({});
+ }
+}
diff --git a/packages/server/src/api/controllers/Cashflow/CashflowController.ts b/packages/server/src/api/controllers/Cashflow/CashflowController.ts
index c6dfe5c29..42efa4c48 100644
--- a/packages/server/src/api/controllers/Cashflow/CashflowController.ts
+++ b/packages/server/src/api/controllers/Cashflow/CashflowController.ts
@@ -13,9 +13,9 @@ export default class CashflowController {
router() {
const router = Router();
+ router.use(Container.get(CommandCashflowTransaction).router());
router.use(Container.get(GetCashflowTransaction).router());
router.use(Container.get(GetCashflowAccounts).router());
- router.use(Container.get(CommandCashflowTransaction).router());
router.use(Container.get(DeleteCashflowTransaction).router());
return router;
diff --git a/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts
index 0ddb6d74d..5e0a763d9 100644
--- a/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts
+++ b/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts
@@ -3,14 +3,15 @@ import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator';
import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions';
-import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
+
import { AbilitySubject, CashflowAction } from '@/interfaces';
+import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service()
-export default class DeleteCashflowTransaction extends BaseController {
+export default class DeleteCashflowTransactionController extends BaseController {
@Inject()
- deleteCashflowService: DeleteCashflowTransactionService;
+ private cashflowApplication: CashflowApplication;
/**
* Controller router.
@@ -44,7 +45,7 @@ export default class DeleteCashflowTransaction extends BaseController {
try {
const { oldCashflowTransaction } =
- await this.deleteCashflowService.deleteCashflowTransaction(
+ await this.cashflowApplication.deleteTransaction(
tenantId,
transactionId
);
@@ -92,6 +93,19 @@ export default class DeleteCashflowTransaction extends BaseController {
],
});
}
+ if (
+ error.errorType ===
+ 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED'
+ ) {
+ return res.boom.badRequest(null, {
+ errors: [
+ {
+ type: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
+ code: 4100,
+ },
+ ],
+ });
+ }
}
next(error);
}
diff --git a/packages/server/src/api/controllers/Cashflow/GetCashflowAccounts.ts b/packages/server/src/api/controllers/Cashflow/GetCashflowAccounts.ts
index 59fdb91ba..559a5f4f2 100644
--- a/packages/server/src/api/controllers/Cashflow/GetCashflowAccounts.ts
+++ b/packages/server/src/api/controllers/Cashflow/GetCashflowAccounts.ts
@@ -1,20 +1,16 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
-import { param, query } from 'express-validator';
-import GetCashflowAccountsService from '@/services/Cashflow/GetCashflowAccountsService';
+import { query } from 'express-validator';
import BaseController from '../BaseController';
-import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
+import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service()
export default class GetCashflowAccounts extends BaseController {
@Inject()
- getCashflowAccountsService: GetCashflowAccountsService;
-
- @Inject()
- getCashflowTransactionsService: GetCashflowTransactionsService;
+ private cashflowApplication: CashflowApplication;
/**
* Controller router.
@@ -62,10 +58,7 @@ export default class GetCashflowAccounts extends BaseController {
try {
const cashflowAccounts =
- await this.getCashflowAccountsService.getCashflowAccounts(
- tenantId,
- filter
- );
+ await this.cashflowApplication.getCashflowAccounts(tenantId, filter);
return res.status(200).send({
cashflow_accounts: this.transfromToResponse(cashflowAccounts),
diff --git a/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts
index 7cf8d2d8e..2625a1cb9 100644
--- a/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts
+++ b/packages/server/src/api/controllers/Cashflow/GetCashflowTransaction.ts
@@ -2,15 +2,15 @@ import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator';
import BaseController from '../BaseController';
-import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
+import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service()
export default class GetCashflowAccounts extends BaseController {
@Inject()
- getCashflowTransactionsService: GetCashflowTransactionsService;
+ private cashflowApplication: CashflowApplication;
/**
* Controller router.
@@ -43,11 +43,10 @@ export default class GetCashflowAccounts extends BaseController {
const { transactionId } = req.params;
try {
- const cashflowTransaction =
- await this.getCashflowTransactionsService.getCashflowTransaction(
- tenantId,
- transactionId
- );
+ const cashflowTransaction = await this.cashflowApplication.getTransaction(
+ tenantId,
+ transactionId
+ );
return res.status(200).send({
cashflow_transaction: this.transfromToResponse(cashflowTransaction),
diff --git a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts
index 91abfcf92..a1af70c15 100644
--- a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts
+++ b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts
@@ -1,16 +1,16 @@
import { Service, Inject } from 'typedi';
-import { check } from 'express-validator';
+import { ValidationChain, check, param, query } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions';
-import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
+import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service()
export default class NewCashflowTransactionController extends BaseController {
@Inject()
- private newCashflowTranscationService: NewCashflowTransactionService;
+ private cashflowApplication: CashflowApplication;
/**
* Router constructor.
@@ -18,6 +18,18 @@ export default class NewCashflowTransactionController extends BaseController {
public router() {
const router = Router();
+ router.get(
+ '/transactions/uncategorized/:id',
+ this.asyncMiddleware(this.getUncategorizedCashflowTransaction),
+ this.catchServiceErrors
+ );
+ router.get(
+ '/transactions/:id/uncategorized',
+ this.getUncategorizedTransactionsValidationSchema,
+ this.validationResult,
+ this.asyncMiddleware(this.getUncategorizedCashflowTransactions),
+ this.catchServiceErrors
+ );
router.post(
'/transactions',
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
@@ -26,13 +38,72 @@ export default class NewCashflowTransactionController extends BaseController {
this.asyncMiddleware(this.newCashflowTransaction),
this.catchServiceErrors
);
+ router.post(
+ '/transactions/:id/uncategorize',
+ this.revertCategorizedCashflowTransaction,
+ this.catchServiceErrors
+ );
+ router.post(
+ '/transactions/:id/categorize',
+ this.categorizeCashflowTransactionValidationSchema,
+ this.validationResult,
+ this.categorizeCashflowTransaction,
+ this.catchServiceErrors
+ );
+ router.post(
+ '/transaction/:id/categorize/expense',
+ this.categorizeAsExpenseValidationSchema,
+ this.validationResult,
+ this.categorizesCashflowTransactionAsExpense,
+ this.catchServiceErrors
+ );
return router;
}
+ /**
+ * Getting uncategorized transactions validation schema.
+ * @returns {ValidationChain}
+ */
+ public get getUncategorizedTransactionsValidationSchema() {
+ return [
+ param('id').exists().isNumeric().toInt(),
+ query('page').optional().isNumeric().toInt(),
+ query('page_size').optional().isNumeric().toInt(),
+ ];
+ }
+
+ /**
+ * Categorize as expense validation schema.
+ */
+ public get categorizeAsExpenseValidationSchema() {
+ return [
+ check('expense_account_id').exists(),
+ check('date').isISO8601().exists(),
+ check('reference_no').optional(),
+ check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
+ ];
+ }
+
+ /**
+ * Categorize cashflow tranasction validation schema.
+ */
+ public get categorizeCashflowTransactionValidationSchema() {
+ return [
+ check('date').exists().isISO8601().toDate(),
+ check('credit_account_id').exists().isInt().toInt(),
+ check('transaction_number').optional(),
+ check('transaction_type').exists(),
+ check('reference_no').optional(),
+ check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
+ check('description').optional(),
+ check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
+ ];
+ }
+
/**
* New cashflow transaction validation schema.
*/
- get newTransactionValidationSchema() {
+ public get newTransactionValidationSchema() {
return [
check('date').exists().isISO8601().toDate(),
check('reference_no').optional({ nullable: true }).trim().escape(),
@@ -48,9 +119,7 @@ export default class NewCashflowTransactionController extends BaseController {
check('credit_account_id').exists().isInt().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
-
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
-
check('publish').default(false).isBoolean().toBoolean(),
];
}
@@ -70,13 +139,12 @@ export default class NewCashflowTransactionController extends BaseController {
const ownerContributionDTO = this.matchedBodyData(req);
try {
- const { cashflowTransaction } =
- await this.newCashflowTranscationService.newCashflowTransaction(
+ const cashflowTransaction =
+ await this.cashflowApplication.createTransaction(
tenantId,
ownerContributionDTO,
userId
);
-
return res.status(200).send({
id: cashflowTransaction.id,
message: 'New cashflow transaction has been created successfully.',
@@ -86,11 +154,147 @@ export default class NewCashflowTransactionController extends BaseController {
}
};
+ /**
+ * Revert the categorized cashflow transaction.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ private revertCategorizedCashflowTransaction = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+ ) => {
+ const { tenantId } = req;
+ const { id: cashflowTransactionId } = req.params;
+
+ try {
+ const data = await this.cashflowApplication.uncategorizeTransaction(
+ tenantId,
+ cashflowTransactionId
+ );
+ return res.status(200).send({ data });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ /**
+ * Categorize the cashflow transaction.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ private categorizeCashflowTransaction = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+ ) => {
+ const { tenantId } = req;
+ const { id: cashflowTransactionId } = req.params;
+ const cashflowTransaction = this.matchedBodyData(req);
+
+ try {
+ await this.cashflowApplication.categorizeTransaction(
+ tenantId,
+ cashflowTransactionId,
+ cashflowTransaction
+ );
+ return res.status(200).send({
+ message: 'The cashflow transaction has been created successfully.',
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ /**
+ * Categorize the transaction as expense transaction.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ private categorizesCashflowTransactionAsExpense = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+ ) => {
+ const { tenantId } = req;
+ const { id: cashflowTransactionId } = req.params;
+ const cashflowTransaction = this.matchedBodyData(req);
+
+ try {
+ await this.cashflowApplication.categorizeAsExpense(
+ tenantId,
+ cashflowTransactionId,
+ cashflowTransaction
+ );
+ return res.status(200).send({
+ message: 'The cashflow transaction has been created successfully.',
+ });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ /**
+ * Retrieves the uncategorized cashflow transactions.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ public getUncategorizedCashflowTransaction = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+ ) => {
+ const { tenantId } = req;
+ const { id: transactionId } = req.params;
+
+ try {
+ const data = await this.cashflowApplication.getUncategorizedTransaction(
+ tenantId,
+ transactionId
+ );
+ return res.status(200).send({ data });
+ } catch (error) {
+ next(error);
+ }
+ };
+
+ /**
+ * Retrieves the uncategorized cashflow transactions.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ public getUncategorizedCashflowTransactions = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+ ) => {
+ const { tenantId } = req;
+ const { id: accountId } = req.params;
+ const query = this.matchedQueryData(req);
+
+ try {
+ const data = await this.cashflowApplication.getUncategorizedTransactions(
+ tenantId,
+ accountId,
+ query
+ );
+
+ return res.status(200).send(data);
+ } catch (error) {
+ next(error);
+ }
+ };
+
/**
* Handle the service errors.
* @param error
- * @param req
- * @param res
+ * @param {Request} req
+ * @param {res
* @param next
* @returns
*/
@@ -140,6 +344,16 @@ export default class NewCashflowTransactionController extends BaseController {
],
});
}
+ if (error.errorType === 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID') {
+ return res.boom.badRequest(null, {
+ errors: [
+ {
+ type: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
+ code: 4100,
+ },
+ ],
+ });
+ }
}
next(error);
}
diff --git a/packages/server/src/api/controllers/Contacts/Contacts.ts b/packages/server/src/api/controllers/Contacts/Contacts.ts
index a05ee93fd..24b99e09f 100644
--- a/packages/server/src/api/controllers/Contacts/Contacts.ts
+++ b/packages/server/src/api/controllers/Contacts/Contacts.ts
@@ -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') {
diff --git a/packages/server/src/api/controllers/ExchangeRates.ts b/packages/server/src/api/controllers/ExchangeRates.ts
index 4b808e921..63c476bf9 100644
--- a/packages/server/src/api/controllers/ExchangeRates.ts
+++ b/packages/server/src/api/controllers/ExchangeRates.ts
@@ -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',
+ },
+ ],
});
}
}
diff --git a/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts b/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts
index 5d626896c..d87873a2b 100644
--- a/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/APAgingSummary.ts
@@ -71,6 +71,7 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
@@ -98,6 +99,15 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
+ // Retrieves the pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.APAgingSummaryApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.APAgingSummaryApp.sheet(tenantId, filter);
diff --git a/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts b/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts
index 10e42e900..86b4b920a 100644
--- a/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/ARAgingSummary.ts
@@ -11,7 +11,7 @@ import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class ARAgingSummaryReportController extends BaseFinancialReportController {
@Inject()
- ARAgingSummaryApp: ARAgingSummaryApplication;
+ private ARAgingSummaryApp: ARAgingSummaryApplication;
/**
* Router constructor.
@@ -69,6 +69,7 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF
]);
// Retrieves the xlsx format.
if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
@@ -96,6 +97,15 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
+ // Retrieves the pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.ARAgingSummaryApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.ARAgingSummaryApp.sheet(tenantId, filter);
diff --git a/packages/server/src/api/controllers/FinancialStatements/BalanceSheet.ts b/packages/server/src/api/controllers/FinancialStatements/BalanceSheet.ts
index 0af53d723..fda717a38 100644
--- a/packages/server/src/api/controllers/FinancialStatements/BalanceSheet.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/BalanceSheet.ts
@@ -101,6 +101,7 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE == acceptType) {
@@ -128,6 +129,15 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
+ // Retrieves the pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.balanceSheetApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ res.send(pdfContent);
} else {
const sheet = await this.balanceSheetApp.sheet(tenantId, filter);
diff --git a/packages/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts b/packages/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts
index bab04246d..bc24b5379 100644
--- a/packages/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts
@@ -79,6 +79,7 @@ export default class CashFlowController extends BaseFinancialReportController {
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
@@ -106,6 +107,15 @@ export default class CashFlowController extends BaseFinancialReportController {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
+ // Retrieves the pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.cashflowSheetApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
// Retrieves the json format.
} else {
const cashflow = await this.cashflowSheetApp.sheet(tenantId, filter);
diff --git a/packages/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts b/packages/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts
index 6c10543f5..e69fcdc1c 100644
--- a/packages/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/CustomerBalanceSummary/index.ts
@@ -75,6 +75,7 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the xlsx format.
@@ -109,6 +110,19 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
filter
);
return res.status(200).send(table);
+ // Retrieves the pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const buffer = await this.customerBalanceSummaryApp.pdf(
+ tenantId,
+ filter
+ );
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': buffer.length,
+ });
+ return res.send(buffer);
+ // Retrieves the json format.
} else {
const sheet = await this.customerBalanceSummaryApp.sheet(
tenantId,
diff --git a/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts b/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts
index ac3f002a7..188178a01 100644
--- a/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/GeneralLedger.ts
@@ -2,20 +2,21 @@ 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.
*/
- router() {
+ public router() {
const router = Router();
router.get(
@@ -31,7 +32,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
/**
* Validation schema.
*/
- get validationSchema(): ValidationChain[] {
+ private get validationSchema(): ValidationChain[] {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
@@ -60,21 +61,56 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
* @param {Request} req -
* @param {Response} res -
*/
- async generalLedger(req: Request, res: Response, next: NextFunction) {
- const { tenantId, settings } = req;
+ private async generalLedger(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.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,
+ ACCEPT_TYPE.APPLICATION_PDF,
+ ]);
+ // 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),
+ 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 pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.generalLedgerApplication.pdf(
+ tenantId,
+ filter
+ );
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
});
- } catch (error) {
- next(error);
+ return res.send(pdfContent);
+ // Retrieves the json format.
+ } else {
+ const sheet = await this.generalLedgerApplication.sheet(tenantId, filter);
+ return res.status(200).send(sheet);
}
}
}
diff --git a/packages/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts b/packages/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts
index 07f91af4a..3288ee847 100644
--- a/packages/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts
@@ -96,6 +96,7 @@ export default class InventoryDetailsController extends BaseController {
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the csv format.
if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
@@ -127,6 +128,15 @@ export default class InventoryDetailsController extends BaseController {
filter
);
return res.status(200).send(table);
+ // Retrieves the pdf format.
+ } else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
+ const buffer = await this.inventoryItemDetailsApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': buffer.length,
+ });
+ return res.send(buffer);
} else {
const sheet = await this.inventoryItemDetailsApp.sheet(
tenantId,
diff --git a/packages/server/src/api/controllers/FinancialStatements/InventoryValuationSheet.ts b/packages/server/src/api/controllers/FinancialStatements/InventoryValuationSheet.ts
index a98c8c997..b31a911a2 100644
--- a/packages/server/src/api/controllers/FinancialStatements/InventoryValuationSheet.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/InventoryValuationSheet.ts
@@ -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,55 @@ 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),
+ 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,
+ ACCEPT_TYPE.APPLICATION_PDF,
+ ]);
+
+ // 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 pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.inventoryValuationApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
});
- } catch (error) {
- next(error);
+ return res.status(200).send(pdfContent);
+ // Retrieves the json format.
+ } else {
+ const { data, query, meta } = await this.inventoryValuationApp.sheet(
+ tenantId,
+ filter
+ );
+ return res.status(200).send({ meta, data, query });
}
}
}
diff --git a/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts b/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts
index ebd6074f4..561f69329 100644
--- a/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/JournalSheet.ts
@@ -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,58 @@ 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,
+ ACCEPT_TYPE.APPLICATION_PDF,
+ ]);
- 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 if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.journalSheetApp.pdf(tenantId, filter);
- return res.status(200).send({
- data: this.transfromToResponse(data),
- query: this.transfromToResponse(query),
- meta: this.transfromToResponse(meta),
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
});
- } catch (error) {
- next(error);
+ res.send(pdfContent);
+ } else {
+ const sheet = await this.journalSheetApp.sheet(tenantId, filter);
+
+ return res.status(200).send(sheet);
}
}
}
diff --git a/packages/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts b/packages/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts
index 8c2404335..995df07b4 100644
--- a/packages/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/ProfitLossSheet.ts
@@ -96,6 +96,7 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
try {
// Retrieves the csv format.
@@ -125,6 +126,14 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
);
return res.send(sheet);
// Retrieves the json format.
+ } else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
+ const pdfContent = await this.profitLossSheetApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
} else {
const sheet = await this.profitLossSheetApp.sheet(tenantId, filter);
diff --git a/packages/server/src/api/controllers/FinancialStatements/PurchasesByItem.ts b/packages/server/src/api/controllers/FinancialStatements/PurchasesByItem.ts
index 5e7aacb09..2c92bdc97 100644
--- a/packages/server/src/api/controllers/FinancialStatements/PurchasesByItem.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/PurchasesByItem.ts
@@ -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,56 @@ 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),
+ 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,
+ ACCEPT_TYPE.APPLICATION_PDF,
+ ]);
+ // 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);
+ // PDF response format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.purchasesByItemsApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
});
- } catch (error) {
- next(error);
+ return res.send(pdfContent);
+ // Json response format.
+ } else {
+ const sheet = await this.purchasesByItemsApp.sheet(tenantId, filter);
+
+ return res.status(200).send(sheet);
}
}
}
diff --git a/packages/server/src/api/controllers/FinancialStatements/SalesByItems.ts b/packages/server/src/api/controllers/FinancialStatements/SalesByItems.ts
index d31954398..28ab611c0 100644
--- a/packages/server/src/api/controllers/FinancialStatements/SalesByItems.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/SalesByItems.ts
@@ -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;
+ private 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,53 @@ 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,
+ ACCEPT_TYPE.APPLICATION_PDF,
+ ]);
+ // 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),
+ return res.send(buffer);
+ // Retrieves the json format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.salesByItemsApp.pdf(tenantId, filter);
+
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
});
- } catch (error) {
- next(error);
+ return res.send(pdfContent);
+ } else {
+ const sheet = await this.salesByItemsApp.sheet(tenantId, filter);
+ return res.status(200).send(sheet);
}
}
}
diff --git a/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts b/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts
index 933b5c9c4..42a96aab2 100644
--- a/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/SalesTaxLiabilitySummary/index.ts
@@ -62,6 +62,7 @@ export default class SalesTaxLiabilitySummary extends BaseFinancialReportControl
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the json table format.
@@ -97,6 +98,16 @@ export default class SalesTaxLiabilitySummary extends BaseFinancialReportControl
return res.send(buffer);
// Retrieves the json format.
+ } else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
+ const pdfContent = await this.salesTaxLiabilitySummaryApp.pdf(
+ tenantId,
+ filter
+ );
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.status(200).send(pdfContent);
} else {
const sheet = await this.salesTaxLiabilitySummaryApp.sheet(
tenantId,
diff --git a/packages/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts b/packages/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts
index 4bc3b1f44..c10eeea16 100644
--- a/packages/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/TransactionsByCustomers/index.ts
@@ -70,6 +70,7 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
try {
// Retrieves the json table format.
@@ -103,6 +104,16 @@ export default class TransactionsByCustomersReportController extends BaseFinanci
);
return res.send(buffer);
// Retrieve the json format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.transactionsByCustomersApp.pdf(
+ tenantId,
+ filter
+ );
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
} else {
const sheet = await this.transactionsByCustomersApp.sheet(
tenantId,
diff --git a/packages/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts b/packages/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts
index a0c1bf037..c437892f4 100644
--- a/packages/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/TransactionsByVendors/index.ts
@@ -71,6 +71,7 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the xlsx format.
@@ -101,6 +102,17 @@ export default class TransactionsByVendorsReportController extends BaseFinancial
filter
);
return res.status(200).send(table);
+ // Retrieves the pdf format.
+ } else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
+ const pdfContent = await this.transactionsByVendorsApp.pdf(
+ tenantId,
+ filter
+ );
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.transactionsByVendorsApp.sheet(
diff --git a/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts b/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts
index ce23c1071..b92e355a2 100644
--- a/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/TrialBalanceSheet.ts
@@ -3,7 +3,6 @@ import { Request, Response, Router, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import { castArray } from 'lodash';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
-import TrialBalanceSheetService from '@/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetInjectable';
import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@@ -81,6 +80,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves in json table format.
if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
@@ -109,6 +109,17 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
+ // Retrieves in pdf format.
+ } else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
+ const pdfContent = await this.trialBalanceSheetApp.pdf(
+ tenantId,
+ filter
+ );
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ res.send(pdfContent);
// Retrieves in json format.
} else {
const { data, query, meta } = await this.trialBalanceSheetApp.sheet(
diff --git a/packages/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts b/packages/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts
index ade69cb62..f1e26a0ed 100644
--- a/packages/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts
+++ b/packages/server/src/api/controllers/FinancialStatements/VendorBalanceSummary/index.ts
@@ -72,6 +72,7 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
+ ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the csv format.
@@ -100,6 +101,17 @@ export default class VendorBalanceSummaryReportController extends BaseFinancialR
filter
);
return res.status(200).send(table);
+ // Retrieves the pdf format.
+ } else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
+ const pdfContent = await this.vendorBalanceSummaryApp.pdf(
+ tenantId,
+ filter
+ );
+ res.set({
+ 'Content-Type': 'application/pdf',
+ 'Content-Length': pdfContent.length,
+ });
+ return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.vendorBalanceSummaryApp.sheet(
diff --git a/packages/server/src/api/controllers/Purchases/Bills.ts b/packages/server/src/api/controllers/Purchases/Bills.ts
index 2cd9e62de..4f65eab26 100644
--- a/packages/server/src/api/controllers/Purchases/Bills.ts
+++ b/packages/server/src/api/controllers/Purchases/Bills.ts
@@ -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);
}
diff --git a/packages/server/src/api/controllers/Purchases/BillsPayments.ts b/packages/server/src/api/controllers/Purchases/BillsPayments.ts
index a663a3032..984ab6fba 100644
--- a/packages/server/src/api/controllers/Purchases/BillsPayments.ts
+++ b/packages/server/src/api/controllers/Purchases/BillsPayments.ts
@@ -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);
}
diff --git a/packages/server/src/api/controllers/Purchases/VendorCredit.ts b/packages/server/src/api/controllers/Purchases/VendorCredit.ts
index 3d2832308..79ae6741b 100644
--- a/packages/server/src/api/controllers/Purchases/VendorCredit.ts
+++ b/packages/server/src/api/controllers/Purchases/VendorCredit.ts
@@ -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) {
diff --git a/packages/server/src/api/controllers/Sales/CreditNotes.ts b/packages/server/src/api/controllers/Sales/CreditNotes.ts
index 77b506c7d..4644b55c5 100644
--- a/packages/server/src/api/controllers/Sales/CreditNotes.ts
+++ b/packages/server/src/api/controllers/Sales/CreditNotes.ts
@@ -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 });
}
};
diff --git a/packages/server/src/api/controllers/Sales/PaymentReceives.ts b/packages/server/src/api/controllers/Sales/PaymentReceives.ts
index 7cfa93a00..818fcb713 100644
--- a/packages/server/src/api/controllers/Sales/PaymentReceives.ts
+++ b/packages/server/src/api/controllers/Sales/PaymentReceives.ts
@@ -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,
diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts
index 9f3cf3719..e26b40c30 100644
--- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts
+++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts
@@ -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
diff --git a/packages/server/src/api/controllers/Sales/SalesInvoices.ts b/packages/server/src/api/controllers/Sales/SalesInvoices.ts
index d90b94d8d..b45604e43 100644
--- a/packages/server/src/api/controllers/Sales/SalesInvoices.ts
+++ b/packages/server/src/api/controllers/Sales/SalesInvoices.ts
@@ -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
diff --git a/packages/server/src/api/controllers/Sales/SalesReceipts.ts b/packages/server/src/api/controllers/Sales/SalesReceipts.ts
index 3eabcf84e..04edfd9f8 100644
--- a/packages/server/src/api/controllers/Sales/SalesReceipts.ts
+++ b/packages/server/src/api/controllers/Sales/SalesReceipts.ts
@@ -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
diff --git a/packages/server/src/api/controllers/Webhooks/Webhooks.ts b/packages/server/src/api/controllers/Webhooks/Webhooks.ts
new file mode 100644
index 000000000..955e382fb
--- /dev/null
+++ b/packages/server/src/api/controllers/Webhooks/Webhooks.ts
@@ -0,0 +1,47 @@
+import { Router } from 'express';
+import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
+import { Request, Response } from 'express';
+import { Inject, Service } from 'typedi';
+import BaseController from '../BaseController';
+import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
+
+@Service()
+export class Webhooks extends BaseController {
+ @Inject()
+ private plaidApp: PlaidApplication;
+
+ /**
+ * Router constructor.
+ */
+ router() {
+ const router = Router();
+
+ router.use(PlaidWebhookTenantBootMiddleware);
+ router.post('/plaid', this.plaidWebhooks.bind(this));
+
+ return router;
+ }
+
+ /**
+ * Listens to Plaid webhooks.
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {Response}
+ */
+ public async plaidWebhooks(req: Request, res: Response) {
+ const { tenantId } = req;
+ const {
+ webhook_type: webhookType,
+ webhook_code: webhookCode,
+ item_id: plaidItemId,
+ } = req.body;
+
+ await this.plaidApp.webhooks(
+ tenantId,
+ plaidItemId,
+ webhookType,
+ webhookCode
+ );
+ return res.status(200).send({ code: 200, message: 'ok' });
+ }
+}
diff --git a/packages/server/src/api/index.ts b/packages/server/src/api/index.ts
index 1a3ff17c6..cc1206aee 100644
--- a/packages/server/src/api/index.ts
+++ b/packages/server/src/api/index.ts
@@ -57,6 +57,8 @@ import { ProjectTasksController } from './controllers/Projects/Tasks';
import { ProjectTimesController } from './controllers/Projects/Times';
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
import { ImportController } from './controllers/Import/ImportController';
+import { BankingController } from './controllers/Banking/BankingController';
+import { Webhooks } from './controllers/Webhooks/Webhooks';
export default () => {
const app = Router();
@@ -72,6 +74,7 @@ export default () => {
app.use('/ping', Container.get(Ping).router());
app.use('/jobs', Container.get(Jobs).router());
app.use('/account', Container.get(Account).router());
+ app.use('/webhooks', Container.get(Webhooks).router());
// - Dashboard routes.
// ---------------------------
@@ -119,6 +122,7 @@ export default () => {
Container.get(InventoryItemsCostController).router()
);
dashboard.use('/cashflow', Container.get(CashflowController).router());
+ dashboard.use('/banking', Container.get(BankingController).router());
dashboard.use('/roles', Container.get(RolesController).router());
dashboard.use(
'/transactions-locking',
diff --git a/packages/server/src/config/index.ts b/packages/server/src/config/index.ts
index bc6833130..12938038f 100644
--- a/packages/server/src/config/index.ts
+++ b/packages/server/src/config/index.ts
@@ -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,27 @@ 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,
+ },
+ },
+
+ /**
+ * Plaid.
+ */
+ plaid: {
+ env: process.env.PLAID_ENV || 'sandbox',
+ clientId: process.env.PLAID_CLIENT_ID,
+ secretDevelopment: process.env.PLAID_SECRET_DEVELOPMENT,
+ secretSandbox: process.env.PLAID_SECRET_SANDBOX,
+ redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
+ redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
+ linkWebhook: process.env.PLAID_LINK_WEBHOOK
+ },
};
diff --git a/packages/server/src/data/options.ts b/packages/server/src/data/options.ts
index 023628ef8..2c0377fda 100644
--- a/packages/server/src/data/options.ts
+++ b/packages/server/src/data/options.ts
@@ -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: {
@@ -118,6 +136,11 @@ export default {
type: 'number',
},
},
+ inventory: {
+ cost_compute_running: {
+ type: 'boolean',
+ },
+ },
accounts: {
account_code_required: {
type: 'boolean',
@@ -147,6 +170,12 @@ export default {
auto_increment: {
type: 'boolean',
},
+ customer_notes: {
+ type: 'string',
+ },
+ terms_conditions: {
+ type: 'string',
+ },
},
vendor_credit: {
next_number: {
diff --git a/packages/server/src/database/migrations/20240201160214_create_plaid_items_Table.js b/packages/server/src/database/migrations/20240201160214_create_plaid_items_Table.js
new file mode 100644
index 000000000..089283290
--- /dev/null
+++ b/packages/server/src/database/migrations/20240201160214_create_plaid_items_Table.js
@@ -0,0 +1,14 @@
+exports.up = function (knex) {
+ return knex.schema.createTable('plaid_items', (table) => {
+ table.increments('id');
+ table.integer('tenant_id').unsigned();
+ table.string('plaid_item_id');
+ table.string('plaid_institution_id');
+ table.string('plaid_access_token');
+ table.string('last_cursor');
+ table.string('status');
+ table.timestamps();
+ });
+};
+
+exports.down = function (knex) {};
diff --git a/packages/server/src/database/migrations/20240201235818_add_plaid_account_id_to_accounts_table.js b/packages/server/src/database/migrations/20240201235818_add_plaid_account_id_to_accounts_table.js
new file mode 100644
index 000000000..901f08987
--- /dev/null
+++ b/packages/server/src/database/migrations/20240201235818_add_plaid_account_id_to_accounts_table.js
@@ -0,0 +1,9 @@
+exports.up = function (knex) {
+ return knex.schema.table('accounts', (table) => {
+ table.string('plaid_account_id');
+ table.string('account_mask').nullable();
+ table.decimal('bank_balance', 15, 5);
+ });
+};
+
+exports.down = function (knex) {};
diff --git a/packages/server/src/database/migrations/20240204180554_add_plaid_transaction_id_to_cashflow_transaction.js b/packages/server/src/database/migrations/20240204180554_add_plaid_transaction_id_to_cashflow_transaction.js
new file mode 100644
index 000000000..3c88cd589
--- /dev/null
+++ b/packages/server/src/database/migrations/20240204180554_add_plaid_transaction_id_to_cashflow_transaction.js
@@ -0,0 +1,7 @@
+exports.up = function (knex) {
+ return knex.schema.table('cashflow_transactions', (table) => {
+ table.string('plaid_transaction_id');
+ });
+};
+
+exports.down = function (knex) {};
diff --git a/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js b/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js
new file mode 100644
index 000000000..29a596772
--- /dev/null
+++ b/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js
@@ -0,0 +1,28 @@
+exports.up = function (knex) {
+ return knex.schema.createTable(
+ 'uncategorized_cashflow_transactions',
+ (table) => {
+ table.increments('id');
+ table.date('date').index();
+ table.decimal('amount');
+ table.string('currency_code');
+ table.string('reference_no').index();
+ table.string('payee');
+ table
+ .integer('account_id')
+ .unsigned()
+ .references('id')
+ .inTable('accounts');
+ table.string('description');
+ table.string('categorize_ref_type');
+ table.integer('categorize_ref_id').unsigned();
+ table.boolean('categorized').defaultTo(false);
+ table.string('plaid_transaction_id');
+ table.timestamps();
+ }
+ );
+};
+
+exports.down = function (knex) {
+ return knex.schema.dropTableIfExists('uncategorized_cashflow_transactions');
+};
diff --git a/packages/server/src/database/migrations/20240304153926_add_uncategorized_transactions_column_to_accounts_table.js b/packages/server/src/database/migrations/20240304153926_add_uncategorized_transactions_column_to_accounts_table.js
new file mode 100644
index 000000000..06d05f521
--- /dev/null
+++ b/packages/server/src/database/migrations/20240304153926_add_uncategorized_transactions_column_to_accounts_table.js
@@ -0,0 +1,10 @@
+exports.up = function (knex) {
+ return knex.schema.table('accounts', (table) => {
+ table.integer('uncategorized_transactions').defaultTo(0);
+ table.boolean('is_system_account').defaultTo(true);
+ table.boolean('is_feeds_active').defaultTo(false);
+ table.datetime('last_feeds_updated_at').nullable();
+ });
+};
+
+exports.down = function (knex) {};
diff --git a/packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js b/packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js
new file mode 100644
index 000000000..01b93bea5
--- /dev/null
+++ b/packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js
@@ -0,0 +1,15 @@
+exports.up = function (knex) {
+ return knex.schema.table('cashflow_transactions', (table) => {
+ table
+ .integer('uncategorized_transaction_id')
+ .unsigned()
+ .references('id')
+ .inTable('uncategorized_cashflow_transactions');
+ });
+};
+
+exports.down = function (knex) {
+ return knex.schema.table('cashflow_transactions', (table) => {
+ table.dropColumn('uncategorized_transaction_id');
+ });
+};
diff --git a/packages/server/src/interfaces/APAgingSummaryReport.ts b/packages/server/src/interfaces/APAgingSummaryReport.ts
index 6bf7c83bb..21ed036c7 100644
--- a/packages/server/src/interfaces/APAgingSummaryReport.ts
+++ b/packages/server/src/interfaces/APAgingSummaryReport.ts
@@ -1,13 +1,11 @@
import {
IAgingPeriod,
- IAgingPeriodTotal,
- IAgingAmount,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
} from './AgingReport';
-import { INumberFormatQuery } from './FinancialStatements';
+import { IFinancialSheetCommonMeta } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
@@ -26,17 +24,22 @@ export interface IAPAgingSummaryData extends IAgingSummaryData {
export type IAPAgingSummaryColumns = IAgingPeriod[];
-export interface IARAgingSummaryMeta {
- baseCurrency: string;
- organizationName: string;
+export interface IARAgingSummaryMeta extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
}
-export interface IAPAgingSummaryMeta {
- baseCurrency: string;
- organizationName: string;
+export interface IAPAgingSummaryMeta extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
}
export interface IAPAgingSummaryTable extends IFinancialTable {
query: IAPAgingSummaryQuery;
meta: IAPAgingSummaryMeta;
}
+
+export interface IAPAgingSummarySheet {
+ data: IAPAgingSummaryData;
+ meta: IAPAgingSummaryMeta;
+ query: IAPAgingSummaryQuery;
+ columns: any;
+}
diff --git a/packages/server/src/interfaces/ARAgingSummaryReport.ts b/packages/server/src/interfaces/ARAgingSummaryReport.ts
index d42fc8f7f..b20753927 100644
--- a/packages/server/src/interfaces/ARAgingSummaryReport.ts
+++ b/packages/server/src/interfaces/ARAgingSummaryReport.ts
@@ -32,3 +32,11 @@ export interface IARAgingSummaryTable extends IFinancialTable {
meta: IARAgingSummaryMeta;
query: IARAgingSummaryQuery;
}
+
+export interface IARAgingSummarySheet {
+ data: IARAgingSummaryData;
+ meta: IARAgingSummaryMeta;
+ query: IARAgingSummaryQuery;
+ columns: IARAgingSummaryColumns;
+}
+
diff --git a/packages/server/src/interfaces/Account.ts b/packages/server/src/interfaces/Account.ts
index 2239448c1..7c045def3 100644
--- a/packages/server/src/interfaces/Account.ts
+++ b/packages/server/src/interfaces/Account.ts
@@ -6,12 +6,15 @@ export interface IAccountDTO {
code: string;
description: string;
accountType: string;
- parentAccountId: number;
+ parentAccountId?: number;
active: boolean;
+ bankBalance?: number;
+ accountMask?: string;
}
export interface IAccountCreateDTO extends IAccountDTO {
currencyCode?: string;
+ plaidAccountId?: string;
}
export interface IAccountEditDTO extends IAccountDTO {}
@@ -33,6 +36,7 @@ export interface IAccount {
type?: any[];
accountNormal: string;
accountParentType: string;
+ bankBalance: string;
}
export enum AccountNormal {
@@ -154,10 +158,9 @@ export enum AccountAction {
TransactionsLocking = 'TransactionsLocking',
}
-
export enum TaxRateAction {
CREATE = 'Create',
EDIT = 'Edit',
DELETE = 'Delete',
VIEW = 'View',
-}
\ No newline at end of file
+}
diff --git a/packages/server/src/interfaces/AgingReport.ts b/packages/server/src/interfaces/AgingReport.ts
index c68b6b389..dc2a0dd1b 100644
--- a/packages/server/src/interfaces/AgingReport.ts
+++ b/packages/server/src/interfaces/AgingReport.ts
@@ -1,5 +1,7 @@
-
-import { INumberFormatQuery } from './FinancialStatements';
+import {
+ IFinancialSheetCommonMeta,
+ INumberFormatQuery,
+} from './FinancialStatements';
export interface IAgingPeriodTotal extends IAgingPeriod {
total: IAgingAmount;
@@ -42,3 +44,8 @@ export interface IAgingSummaryTotal {
export interface IAgingSummaryData {
total: IAgingSummaryTotal;
}
+
+export interface IAgingSummaryMeta extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
+ formattedDateRange: string;
+}
diff --git a/packages/server/src/interfaces/BalanceSheet.ts b/packages/server/src/interfaces/BalanceSheet.ts
index a74e48ee8..567cb8764 100644
--- a/packages/server/src/interfaces/BalanceSheet.ts
+++ b/packages/server/src/interfaces/BalanceSheet.ts
@@ -2,6 +2,7 @@ import {
INumberFormatQuery,
IFormatNumberSettings,
IFinancialSheetBranchesQuery,
+ IFinancialSheetCommonMeta,
} from './FinancialStatements';
import { IFinancialTable } from './Table';
@@ -63,10 +64,9 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
}
// Balance sheet meta.
-export interface IBalanceSheetMeta {
- isCostComputeRunning: boolean;
- organizationName: string;
- baseCurrency: string;
+export interface IBalanceSheetMeta extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
+ formattedDateRange: string;
}
export interface IBalanceSheetFormatNumberSettings
diff --git a/packages/server/src/interfaces/CashFlow.ts b/packages/server/src/interfaces/CashFlow.ts
index 1a4a1a6a1..499c526b0 100644
--- a/packages/server/src/interfaces/CashFlow.ts
+++ b/packages/server/src/interfaces/CashFlow.ts
@@ -1,4 +1,7 @@
-import { INumberFormatQuery } from './FinancialStatements';
+import {
+ IFinancialSheetCommonMeta,
+ INumberFormatQuery,
+} from './FinancialStatements';
import { IAccount } from './Account';
import { ILedger } from './Ledger';
import { IFinancialTable, ITableRow } from './Table';
@@ -79,8 +82,8 @@ export interface ICashFlowStatementAggregateSection
export interface ICashFlowCashBeginningNode
extends ICashFlowStatementCommonSection {
- sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING;
- }
+ sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING;
+}
export type ICashFlowStatementSection =
| ICashFlowStatementAccountSection
@@ -89,10 +92,10 @@ export type ICashFlowStatementSection =
| ICashFlowStatementCommonSection;
export interface ICashFlowStatementColumn {}
-export interface ICashFlowStatementMeta {
- isCostComputeRunning: boolean;
- organizationName: string;
- baseCurrency: string;
+export interface ICashFlowStatementMeta extends IFinancialSheetCommonMeta {
+ formattedToDate: string;
+ formattedFromDate: string;
+ formattedDateRange: string;
}
export interface ICashFlowStatementDOO {
@@ -230,3 +233,38 @@ export interface ICashflowTransactionSchema {
}
export interface ICashflowTransactionInput extends ICashflowTransactionSchema {}
+
+export interface ICategorizeCashflowTransactioDTO {
+ creditAccountId: number;
+ referenceNo: string;
+ transactionNumber: string;
+ transactionType: string;
+ exchangeRate: number;
+ description: string;
+ branchId: number;
+}
+
+export interface IUncategorizedCashflowTransaction {
+ id?: number;
+ amount: number;
+ date: Date;
+ currencyCode: string;
+ accountId: number;
+ description: string;
+ referenceNo: string;
+ categorizeRefType: string;
+ categorizeRefId: number;
+ categorized: boolean;
+}
+
+
+export interface CreateUncategorizedTransactionDTO {
+ date: Date | string;
+ accountId: number;
+ amount: number;
+ currencyCode: string;
+ payee?: string;
+ description?: string;
+ referenceNo?: string | null;
+ plaidTransactionId?: string | null;
+}
diff --git a/packages/server/src/interfaces/CashflowService.ts b/packages/server/src/interfaces/CashflowService.ts
index 8b0576d8e..7d427b998 100644
--- a/packages/server/src/interfaces/CashflowService.ts
+++ b/packages/server/src/interfaces/CashflowService.ts
@@ -1,5 +1,6 @@
import { Knex } from 'knex';
import { IAccount } from './Account';
+import { IUncategorizedCashflowTransaction } from './CashFlow';
export interface ICashflowAccountTransactionsFilter {
page: number;
@@ -45,9 +46,13 @@ export interface ICashflowCommandDTO {
publish: boolean;
branchId?: number;
+ plaidTransactionId?: string;
}
-export interface ICashflowNewCommandDTO extends ICashflowCommandDTO {}
+export interface ICashflowNewCommandDTO extends ICashflowCommandDTO {
+ plaidAccountId?: string;
+ uncategorizedTransactionId?: number;
+}
export interface ICashflowTransaction {
id?: number;
@@ -79,6 +84,8 @@ export interface ICashflowTransaction {
isCashDebit?: boolean;
isCashCredit?: boolean;
+
+ uncategorizedTransactionId?: number;
}
export interface ICashflowTransactionLine {
@@ -121,8 +128,39 @@ export interface ICommandCashflowDeletedPayload {
trx: Knex.Transaction;
}
+export interface ICashflowTransactionCategorizedPayload {
+ tenantId: number;
+ cashflowTransactionId: number;
+ cashflowTransaction: ICashflowTransaction;
+ trx: Knex.Transaction;
+}
+export interface ICashflowTransactionUncategorizingPayload {
+ tenantId: number;
+ uncategorizedTransaction: IUncategorizedCashflowTransaction;
+ trx: Knex.Transaction;
+}
+export interface ICashflowTransactionUncategorizedPayload {
+ tenantId: number;
+ uncategorizedTransaction: IUncategorizedCashflowTransaction;
+ oldUncategorizedTransaction: IUncategorizedCashflowTransaction;
+ trx: Knex.Transaction;
+}
+
export enum CashflowAction {
Create = 'Create',
Delete = 'Delete',
View = 'View',
}
+
+export interface CategorizeTransactionAsExpenseDTO {
+ expenseAccountId: number;
+ exchangeRate: number;
+ referenceNo: string;
+ description: string;
+ branchId?: number;
+}
+
+export interface IGetUncategorizedTransactionsQuery {
+ page?: number;
+ pageSize?: number;
+}
diff --git a/packages/server/src/interfaces/CustomerBalanceSummary.ts b/packages/server/src/interfaces/CustomerBalanceSummary.ts
index cf5b5900d..c55452d98 100644
--- a/packages/server/src/interfaces/CustomerBalanceSummary.ts
+++ b/packages/server/src/interfaces/CustomerBalanceSummary.ts
@@ -4,6 +4,7 @@ import {
IContactBalanceSummaryPercentage,
IContactBalanceSummaryTotal,
} from './ContactBalanceSummary';
+import { IFinancialSheetCommonMeta } from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface ICustomerBalanceSummaryQuery
@@ -35,9 +36,15 @@ export interface ICustomerBalanceSummaryData {
total: ICustomerBalanceSummaryTotal;
}
+export interface ICustomerBalanceSummaryMeta extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
+ formattedDateRange: string;
+}
+
export interface ICustomerBalanceSummaryStatement {
data: ICustomerBalanceSummaryData;
query: ICustomerBalanceSummaryQuery;
+ meta: ICustomerBalanceSummaryMeta;
}
export interface ICustomerBalanceSummaryService {
@@ -49,4 +56,5 @@ export interface ICustomerBalanceSummaryService {
export interface ICustomerBalanceSummaryTable extends IFinancialTable {
query: ICustomerBalanceSummaryQuery;
+ meta: ICustomerBalanceSummaryMeta;
}
diff --git a/packages/server/src/interfaces/ExchangeRate.ts b/packages/server/src/interfaces/ExchangeRate.ts
index fc3bd33e4..45080fc0f 100644
--- a/packages/server/src/interfaces/ExchangeRate.ts
+++ b/packages/server/src/interfaces/ExchangeRate.ts
@@ -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;
- editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise;
-
- deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise;
- listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise;
-};
\ No newline at end of file
+export interface EchangeRateLatestPOJO {
+ baseCurrency: string;
+ toCurrency: string;
+ exchangeRate: number;
+}
diff --git a/packages/server/src/interfaces/FinancialStatements.ts b/packages/server/src/interfaces/FinancialStatements.ts
index fb1d77452..abf2468c6 100644
--- a/packages/server/src/interfaces/FinancialStatements.ts
+++ b/packages/server/src/interfaces/FinancialStatements.ts
@@ -42,4 +42,13 @@ export enum ReportsAction {
export interface IFinancialSheetBranchesQuery {
branchesIds?: number[];
-}
\ No newline at end of file
+}
+
+export interface IFinancialSheetCommonMeta {
+ organizationName: string;
+ baseCurrency: string;
+ dateFormat: string;
+ isCostComputeRunning: boolean;
+ sheetName: string;
+
+}
diff --git a/packages/server/src/interfaces/GeneralLedgerSheet.ts b/packages/server/src/interfaces/GeneralLedgerSheet.ts
index bf1662086..6de1bda3b 100644
--- a/packages/server/src/interfaces/GeneralLedgerSheet.ts
+++ b/packages/server/src/interfaces/GeneralLedgerSheet.ts
@@ -1,81 +1,90 @@
-
+import { IFinancialSheetCommonMeta } from './FinancialStatements';
+import { IFinancialTable } from './Table';
export interface IGeneralLedgerSheetQuery {
- fromDate: Date | string,
- toDate: Date | string,
- basis: string,
+ fromDate: Date | string;
+ toDate: Date | string;
+ basis: string;
numberFormat: {
- noCents: boolean,
- divideOn1000: boolean,
- },
- noneTransactions: boolean,
- accountsIds: number[],
+ noCents: boolean;
+ divideOn1000: boolean;
+ };
+ noneTransactions: boolean;
+ accountsIds: number[];
branchesIds?: number[];
-};
+}
export interface IGeneralLedgerSheetAccountTransaction {
- id: number,
+ id: number;
- amount: number,
- runningBalance: number,
- credit: number,
- debit: number,
+ amount: number;
+ runningBalance: number;
+ credit: number;
+ debit: number;
- formattedAmount: string,
- formattedCredit: string,
- formattedDebit: string,
- formattedRunningBalance: string,
+ formattedAmount: string;
+ formattedCredit: string;
+ formattedDebit: string;
+ formattedRunningBalance: string;
- currencyCode: string,
- note?: string,
+ currencyCode: string;
+ note?: string;
- transactionType?: string,
- transactionNumber: string,
+ transactionType?: string;
+ transactionNumber: string;
- referenceId?: number,
- referenceType?: string,
+ referenceId?: number;
+ referenceType?: string;
- date: Date|string,
-};
+ date: Date | string;
+ dateFormatted: string;
+}
export interface IGeneralLedgerSheetAccountBalance {
- date: Date|string,
- amount: number,
- formattedAmount: string,
- currencyCode: string,
+ date: Date | string;
+ amount: number;
+ formattedAmount: string;
+ currencyCode: string;
}
export interface IGeneralLedgerSheetAccount {
- id: number,
- name: string,
- code: string,
- index: number,
- parentAccountId: number,
- transactions: IGeneralLedgerSheetAccountTransaction[],
- openingBalance: IGeneralLedgerSheetAccountBalance,
- closingBalance: IGeneralLedgerSheetAccountBalance,
+ id: number;
+ name: string;
+ code: string;
+ index: number;
+ parentAccountId: number;
+ transactions: IGeneralLedgerSheetAccountTransaction[];
+ openingBalance: IGeneralLedgerSheetAccountBalance;
+ closingBalance: IGeneralLedgerSheetAccountBalance;
}
+export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];
+
export interface IAccountTransaction {
- id: number,
- index: number,
- draft: boolean,
- note: string,
- accountId: number,
- transactionType: string,
- referenceType: string,
- referenceId: number,
- contactId: number,
- contactType: string,
- credit: number,
- debit: number,
- date: string|Date,
- createdAt: string|Date,
- updatedAt: string|Date,
+ id: number;
+ index: number;
+ draft: boolean;
+ note: string;
+ accountId: number;
+ transactionType: string;
+ referenceType: string;
+ referenceId: number;
+ contactId: number;
+ contactType: string;
+ credit: number;
+ debit: number;
+ date: string | Date;
+ createdAt: string | Date;
+ updatedAt: string | Date;
}
-export interface IGeneralLedgerMeta {
- isCostComputeRunning: boolean,
- organizationName: string,
- baseCurrency: string,
-};
\ No newline at end of file
+export interface IGeneralLedgerMeta extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: string;
+}
+
+export interface IGeneralLedgerTableData extends IFinancialTable {
+ meta: IGeneralLedgerMeta;
+ query: IGeneralLedgerSheetQuery;
+}
diff --git a/packages/server/src/interfaces/IInventoryValuationSheet.ts b/packages/server/src/interfaces/IInventoryValuationSheet.ts
index dedb6c483..a5bd5d051 100644
--- a/packages/server/src/interfaces/IInventoryValuationSheet.ts
+++ b/packages/server/src/interfaces/IInventoryValuationSheet.ts
@@ -1,4 +1,8 @@
-import { INumberFormatQuery } from './FinancialStatements';
+import {
+ IFinancialSheetCommonMeta,
+ INumberFormatQuery,
+} from './FinancialStatements';
+import { IFinancialTable } from './Table';
export interface IInventoryValuationReportQuery {
asDate: Date | string;
@@ -12,10 +16,10 @@ export interface IInventoryValuationReportQuery {
branchesIds?: number[];
}
-export interface IInventoryValuationSheetMeta {
- organizationName: string;
- baseCurrency: string;
- isCostComputeRunning: boolean;
+export interface IInventoryValuationSheetMeta
+ extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
+ formattedDateRange: string;
}
export interface IInventoryValuationItem {
@@ -39,9 +43,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;
+}
diff --git a/packages/server/src/interfaces/InventoryDetails.ts b/packages/server/src/interfaces/InventoryDetails.ts
index 033ec269d..8da644540 100644
--- a/packages/server/src/interfaces/InventoryDetails.ts
+++ b/packages/server/src/interfaces/InventoryDetails.ts
@@ -1,4 +1,7 @@
-import { INumberFormatQuery } from './FinancialStatements';
+import {
+ IFinancialSheetCommonMeta,
+ INumberFormatQuery,
+} from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IInventoryDetailsQuery {
@@ -79,10 +82,10 @@ export type IInventoryDetailsNode =
| IInventoryDetailsItemTransaction;
export type IInventoryDetailsData = IInventoryDetailsItem[];
-export interface IInventoryItemDetailMeta {
- isCostComputeRunning: boolean;
- organizationName: string;
- baseCurrency: string;
+export interface IInventoryItemDetailMeta extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDay: string;
+ formattedDateRange: string;
}
export interface IInvetoryItemDetailDOO {
diff --git a/packages/server/src/interfaces/JournalReport.ts b/packages/server/src/interfaces/JournalReport.ts
index 9786e1634..319bf526c 100644
--- a/packages/server/src/interfaces/JournalReport.ts
+++ b/packages/server/src/interfaces/JournalReport.ts
@@ -1,36 +1,53 @@
+import { IFinancialSheetCommonMeta } from './FinancialStatements';
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,
-}
\ No newline at end of file
+export interface IJournalSheetMeta extends IFinancialSheetCommonMeta {
+ formattedDateRange: string;
+ formattedFromDate: string;
+ formattedToDate: string;
+}
+
+export interface IJournalTable extends IFinancialTable {
+ query: IJournalReportQuery;
+ meta: IJournalSheetMeta;
+}
+
+export type IJournalTableData = IJournalReportEntriesGroup[];
+
+export interface IJournalSheet {
+ data: IJournalTableData;
+ query: IJournalReportQuery;
+ meta: IJournalSheetMeta;
+}
diff --git a/packages/server/src/interfaces/Mailable.ts b/packages/server/src/interfaces/Mailable.ts
index 36cc3c81f..5682f2529 100644
--- a/packages/server/src/interfaces/Mailable.ts
+++ b/packages/server/src/interfaces/Mailable.ts
@@ -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;
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;
-}
\ No newline at end of file
+}
+
+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;
+}
+
+export interface CommonMailOptionsDTO {
+ to?: string | string[];
+ from?: string;
+ subject?: string;
+ body?: string;
+}
diff --git a/packages/server/src/interfaces/PaymentReceive.ts b/packages/server/src/interfaces/PaymentReceive.ts
index 6f8d8552a..329d0d944 100644
--- a/packages/server/src/interfaces/PaymentReceive.ts
+++ b/packages/server/src/interfaces/PaymentReceive.ts
@@ -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,13 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'createdAt'
| 'branchId'
>;
+
+export interface PaymentReceiveMailOpts extends CommonMailOptions {}
+
+export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
+
+export interface PaymentReceiveMailPresendEvent {
+ tenantId: number;
+ paymentReceiveId: number;
+ messageOptions: PaymentReceiveMailOptsDTO;
+}
diff --git a/packages/server/src/interfaces/Plaid.ts b/packages/server/src/interfaces/Plaid.ts
new file mode 100644
index 000000000..a8ad469df
--- /dev/null
+++ b/packages/server/src/interfaces/Plaid.ts
@@ -0,0 +1,56 @@
+export interface IPlaidItemCreatedEventPayload {
+ tenantId: number;
+ plaidAccessToken: string;
+ plaidItemId: string;
+ plaidInstitutionId: string;
+}
+
+export interface PlaidItemDTO {
+ publicToken: string;
+ institutionId: string;
+}
+
+export interface PlaidAccount {
+ account_id: string;
+ balances: {
+ available: number;
+ current: number;
+ iso_currency_code: string;
+ limit: null;
+ unofficial_currency_code: null;
+ };
+ mask: string;
+ name: string;
+ official_name: string;
+ persistent_account_id: string;
+ subtype: string;
+ type: string;
+}
+
+export interface PlaidTransaction {
+ date: string;
+ account_id: string;
+ amount: number;
+ authorized_date: string;
+ name: string;
+ category: string[];
+ check_number: number | null;
+ iso_currency_code: string;
+ transaction_id: string;
+ transaction_type: string;
+ payment_meta: { reference_number: string | null; payee: string | null };
+}
+
+export interface PlaidFetchedTransactionsUpdates {
+ added: any[];
+ modified: any[];
+ removed: any[];
+ accessToken: string;
+ cursor: string;
+}
+
+export interface SyncAccountsTransactionsTask {
+ tenantId: number;
+ plaidAccountId: number;
+ plaidTransactions: PlaidTransaction[];
+}
diff --git a/packages/server/src/interfaces/ProfitLossSheet.ts b/packages/server/src/interfaces/ProfitLossSheet.ts
index 944a0a950..64b787667 100644
--- a/packages/server/src/interfaces/ProfitLossSheet.ts
+++ b/packages/server/src/interfaces/ProfitLossSheet.ts
@@ -1,5 +1,6 @@
import {
IFinancialSheetBranchesQuery,
+ IFinancialSheetCommonMeta,
INumberFormatQuery,
} from './FinancialStatements';
import { IFinancialTable } from './Table';
@@ -134,10 +135,10 @@ export type IProfitLossSheetNode =
| IProfitLossSheetEquationNode
| IProfitLossSheetAccountNode;
-export interface IProfitLossSheetMeta {
- isCostComputeRunning: boolean;
- organizationName: string;
- baseCurrency: string;
+export interface IProfitLossSheetMeta extends IFinancialSheetCommonMeta{
+ formattedDateRange: string;
+ formattedFromDate: string;
+ formattedToDate: string;
}
// ------------------------------------------------
diff --git a/packages/server/src/interfaces/PurchasesByItemsSheet.ts b/packages/server/src/interfaces/PurchasesByItemsSheet.ts
new file mode 100644
index 000000000..fcd1ee8d9
--- /dev/null
+++ b/packages/server/src/interfaces/PurchasesByItemsSheet.ts
@@ -0,0 +1,58 @@
+import {
+ IFinancialSheetCommonMeta,
+ 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 extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: 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;
+}
diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts
index f2a820e98..9ac17295c 100644
--- a/packages/server/src/interfaces/SaleEstimate.ts
+++ b/packages/server/src/interfaces/SaleEstimate.ts
@@ -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,17 @@ export interface ISaleEstimateApprovedEvent {
saleEstimate: ISaleEstimate;
trx: Knex.Transaction;
}
+
+export interface SaleEstimateMailOptions extends CommonMailOptions {
+ attachEstimate?: boolean;
+}
+
+export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
+ attachEstimate?: boolean;
+}
+
+export interface ISaleEstimateMailPresendEvent {
+ tenantId: number;
+ saleEstimateId: number;
+ messageOptions: SaleEstimateMailOptionsDTO;
+}
diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts
index 7ef8fdea2..61594306a 100644
--- a/packages/server/src/interfaces/SaleInvoice.ts
+++ b/packages/server/src/interfaces/SaleInvoice.ts
@@ -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,29 @@ 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;
+}
+
+export interface ISaleInvoiceMailSend {
+ tenantId: number;
+ saleInvoiceId: number;
+ messageOptions: SendInvoiceMailDTO;
+}
+
+export interface ISaleInvoiceMailSent {
+ tenantId: number;
+ saleInvoiceId: number;
+ messageOptions: SendInvoiceMailDTO;
+}
diff --git a/packages/server/src/interfaces/SaleReceipt.ts b/packages/server/src/interfaces/SaleReceipt.ts
index 4d319ec4f..8904767c6 100644
--- a/packages/server/src/interfaces/SaleReceipt.ts
+++ b/packages/server/src/interfaces/SaleReceipt.ts
@@ -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,17 @@ export interface ISaleReceiptDeletingPayload {
oldSaleReceipt: ISaleReceipt;
trx: Knex.Transaction;
}
+
+export interface SaleReceiptMailOpts extends CommonMailOptions {
+ attachReceipt: boolean;
+}
+
+export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO {
+ attachReceipt?: boolean;
+}
+
+export interface ISaleReceiptMailPresend {
+ tenantId: number;
+ saleReceiptId: number;
+ messageOptions: SaleReceiptMailOptsDTO;
+}
diff --git a/packages/server/src/interfaces/SalesByItemsSheet.ts b/packages/server/src/interfaces/SalesByItemsSheet.ts
index 0e0a41f9e..789ebae77 100644
--- a/packages/server/src/interfaces/SalesByItemsSheet.ts
+++ b/packages/server/src/interfaces/SalesByItemsSheet.ts
@@ -1,45 +1,58 @@
import {
+ IFinancialSheetCommonMeta,
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,
-};
+export interface ISalesByItemsSheetMeta extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: 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;
+}
diff --git a/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts b/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
index 2fbc9f13d..b37d8a384 100644
--- a/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
+++ b/packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
@@ -1,3 +1,4 @@
+import { IFinancialSheetCommonMeta } from "./FinancialStatements";
import { IFinancialTable } from "./Table";
export interface SalesTaxLiabilitySummaryQuery {
@@ -47,9 +48,10 @@ export type SalesTaxLiabilitySummarySalesById = Record<
{ taxRateId: number; credit: number; debit: number }
>;
-export interface SalesTaxLiabilitySummaryMeta {
- organizationName: string;
- baseCurrency: string;
+export interface SalesTaxLiabilitySummaryMeta extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: string;
}
export interface ISalesTaxLiabilitySummaryTable extends IFinancialTable {
diff --git a/packages/server/src/interfaces/TransactionsByCustomers.ts b/packages/server/src/interfaces/TransactionsByCustomers.ts
index fcfb01ea6..e47098b36 100644
--- a/packages/server/src/interfaces/TransactionsByCustomers.ts
+++ b/packages/server/src/interfaces/TransactionsByCustomers.ts
@@ -1,3 +1,4 @@
+import { IFinancialSheetCommonMeta } from './FinancialStatements';
import { IFinancialTable, ITableData } from './Table';
import {
ITransactionsByContactsAmount,
@@ -28,10 +29,12 @@ export type ITransactionsByCustomersData = ITransactionsByCustomersCustomer[];
export interface ITransactionsByCustomersStatement {
data: ITransactionsByCustomersData;
query: ITransactionsByCustomersFilter;
+ meta: ITransactionsByCustomersMeta;
}
export interface ITransactionsByCustomersTable extends IFinancialTable {
query: ITransactionsByCustomersFilter;
+ meta: ITransactionsByCustomersMeta;
}
export interface ITransactionsByCustomersService {
@@ -40,3 +43,9 @@ export interface ITransactionsByCustomersService {
filter: ITransactionsByCustomersFilter
): Promise;
}
+export interface ITransactionsByCustomersMeta
+ extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: string;
+}
diff --git a/packages/server/src/interfaces/TransactionsByVendors.ts b/packages/server/src/interfaces/TransactionsByVendors.ts
index ae129bc79..7a5b16013 100644
--- a/packages/server/src/interfaces/TransactionsByVendors.ts
+++ b/packages/server/src/interfaces/TransactionsByVendors.ts
@@ -1,3 +1,4 @@
+import { IFinancialSheetCommonMeta } from './FinancialStatements';
import { IFinancialTable } from './Table';
import {
ITransactionsByContactsAmount,
@@ -27,6 +28,8 @@ export type ITransactionsByVendorsData = ITransactionsByVendorsVendor[];
export interface ITransactionsByVendorsStatement {
data: ITransactionsByVendorsData;
+ query: ITransactionsByVendorsFilter;
+ meta: ITransactionsByVendorMeta;
}
export interface ITransactionsByVendorsService {
@@ -38,4 +41,10 @@ export interface ITransactionsByVendorsService {
export interface ITransactionsByVendorTable extends IFinancialTable {
query: ITransactionsByVendorsFilter;
-}
\ No newline at end of file
+ meta: ITransactionsByVendorMeta;
+}
+export interface ITransactionsByVendorMeta extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: string;
+}
diff --git a/packages/server/src/interfaces/TrialBalanceSheet.ts b/packages/server/src/interfaces/TrialBalanceSheet.ts
index f423503ae..b6cd79736 100644
--- a/packages/server/src/interfaces/TrialBalanceSheet.ts
+++ b/packages/server/src/interfaces/TrialBalanceSheet.ts
@@ -1,4 +1,7 @@
-import { INumberFormatQuery } from './FinancialStatements';
+import {
+ IFinancialSheetCommonMeta,
+ INumberFormatQuery,
+} from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface ITrialBalanceSheetQuery {
@@ -24,10 +27,10 @@ export interface ITrialBalanceTotal {
formattedBalance: string;
}
-export interface ITrialBalanceSheetMeta {
- isCostComputeRunning: boolean;
- organizationName: string;
- baseCurrency: string;
+export interface ITrialBalanceSheetMeta extends IFinancialSheetCommonMeta {
+ formattedFromDate: string;
+ formattedToDate: string;
+ formattedDateRange: string;
}
export interface ITrialBalanceAccount extends ITrialBalanceTotal {
diff --git a/packages/server/src/interfaces/VendorBalanceSummary.ts b/packages/server/src/interfaces/VendorBalanceSummary.ts
index d214202df..5c603c51d 100644
--- a/packages/server/src/interfaces/VendorBalanceSummary.ts
+++ b/packages/server/src/interfaces/VendorBalanceSummary.ts
@@ -1,4 +1,7 @@
-import { INumberFormatQuery } from './FinancialStatements';
+import {
+ IFinancialSheetCommonMeta,
+ INumberFormatQuery,
+} from './FinancialStatements';
import { IFinancialTable } from './Table';
export interface IVendorBalanceSummaryQuery {
@@ -39,8 +42,9 @@ export interface IVendorBalanceSummaryData {
export interface IVendorBalanceSummaryStatement {
data: IVendorBalanceSummaryData;
- columns: {};
query: IVendorBalanceSummaryQuery;
+ meta: IVendorBalanceSummaryMeta;
+
}
export interface IVendorBalanceSummaryService {
@@ -52,4 +56,9 @@ export interface IVendorBalanceSummaryService {
export interface IVendorBalanceSummaryTable extends IFinancialTable {
query: IVendorBalanceSummaryQuery;
+ meta: IVendorBalanceSummaryMeta;
+}
+
+export interface IVendorBalanceSummaryMeta extends IFinancialSheetCommonMeta {
+ formattedAsDate: string;
}
diff --git a/packages/server/src/interfaces/index.ts b/packages/server/src/interfaces/index.ts
index 1b23eedd3..858acba51 100644
--- a/packages/server/src/interfaces/index.ts
+++ b/packages/server/src/interfaces/index.ts
@@ -74,6 +74,7 @@ export * from './Tasks';
export * from './Times';
export * from './ProjectProfitabilitySummary';
export * from './TaxRate';
+export * from './Plaid';
export interface I18nService {
__: (input: string) => string;
diff --git a/packages/server/src/lib/ExchangeRate/ExchangeRate.ts b/packages/server/src/lib/ExchangeRate/ExchangeRate.ts
new file mode 100644
index 000000000..5bb84cff3
--- /dev/null
+++ b/packages/server/src/lib/ExchangeRate/ExchangeRate.ts
@@ -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 {
+ return this.exchangeRateService.latest(baseCurrency, toCurrency);
+ }
+}
diff --git a/packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts b/packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts
new file mode 100644
index 000000000..221c5c5c5
--- /dev/null
+++ b/packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts
@@ -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 {
+ // 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.'
+ );
+ }
+ }
+}
diff --git a/packages/server/src/lib/ExchangeRate/types.ts b/packages/server/src/lib/ExchangeRate/types.ts
new file mode 100644
index 000000000..8b40125cd
--- /dev/null
+++ b/packages/server/src/lib/ExchangeRate/types.ts
@@ -0,0 +1,17 @@
+export interface IExchangeRateService {
+ latest(baseCurrency: string, toCurrency: string): Promise;
+}
+
+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';
diff --git a/packages/server/src/lib/Mail/index.ts b/packages/server/src/lib/Mail/index.ts
index dd79c934b..015ca02a8 100644
--- a/packages/server/src/lib/Mail/index.ts
+++ b/packages/server/src/lib/Mail/index.ts
@@ -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 {
+ 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');
}
}
diff --git a/packages/server/src/lib/Plaid/Plaid.ts b/packages/server/src/lib/Plaid/Plaid.ts
new file mode 100644
index 000000000..d067757fb
--- /dev/null
+++ b/packages/server/src/lib/Plaid/Plaid.ts
@@ -0,0 +1,103 @@
+import { forEach } from 'lodash';
+import { Configuration, PlaidApi, PlaidEnvironments } from 'plaid';
+import { createPlaidApiEvent } from './PlaidApiEventsDBSync';
+import config from '@/config';
+
+const OPTIONS = { clientApp: 'Plaid-Pattern' };
+
+// We want to log requests to / responses from the Plaid API (via the Plaid client), as this data
+// can be useful for troubleshooting.
+
+/**
+ * Logging function for Plaid client methods that use an access_token as an argument. Associates
+ * the Plaid API event log entry with the item and user the request is for.
+ *
+ * @param {string} clientMethod the name of the Plaid client method called.
+ * @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
+ * @param {Object} response the response from the Plaid client.
+ */
+const defaultLogger = async (clientMethod, clientMethodArgs, response) => {
+ const accessToken = clientMethodArgs[0].access_token;
+ // const { id: itemId, user_id: userId } = await retrieveItemByPlaidAccessToken(
+ // accessToken
+ // );
+ // await createPlaidApiEvent(1, 1, clientMethod, clientMethodArgs, response);
+
+ // console.log(response);
+};
+
+/**
+ * Logging function for Plaid client methods that do not use access_token as an argument. These
+ * Plaid API event log entries will not be associated with an item or user.
+ *
+ * @param {string} clientMethod the name of the Plaid client method called.
+ * @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
+ * @param {Object} response the response from the Plaid client.
+ */
+const noAccessTokenLogger = async (
+ clientMethod,
+ clientMethodArgs,
+ response
+) => {
+ // console.log(response);
+
+ // await createPlaidApiEvent(
+ // undefined,
+ // undefined,
+ // clientMethod,
+ // clientMethodArgs,
+ // response
+ // );
+};
+
+// Plaid client methods used in this app, mapped to their appropriate logging functions.
+const clientMethodLoggingFns = {
+ accountsGet: defaultLogger,
+ institutionsGet: noAccessTokenLogger,
+ institutionsGetById: noAccessTokenLogger,
+ itemPublicTokenExchange: noAccessTokenLogger,
+ itemRemove: defaultLogger,
+ linkTokenCreate: noAccessTokenLogger,
+ transactionsSync: defaultLogger,
+ sandboxItemResetLogin: defaultLogger,
+};
+// Wrapper for the Plaid client. This allows us to easily log data for all Plaid client requests.
+export class PlaidClientWrapper {
+ constructor() {
+ // Initialize the Plaid client.
+ const configuration = new Configuration({
+ basePath: PlaidEnvironments[config.plaid.env],
+ baseOptions: {
+ headers: {
+ 'PLAID-CLIENT-ID': config.plaid.clientId,
+ 'PLAID-SECRET':
+ config.plaid.env === 'development'
+ ? config.plaid.secretDevelopment
+ : config.plaid.secretSandbox,
+ 'Plaid-Version': '2020-09-14',
+ },
+ },
+ });
+
+ this.client = new PlaidApi(configuration);
+
+ // Wrap the Plaid client methods to add a logging function.
+ forEach(clientMethodLoggingFns, (logFn, method) => {
+ this[method] = this.createWrappedClientMethod(method, logFn);
+ });
+ }
+
+ // Allows us to log API request data for troubleshooting purposes.
+ createWrappedClientMethod(clientMethod, log) {
+ return async (...args) => {
+ try {
+ const res = await this.client[clientMethod](...args);
+ await log(clientMethod, args, res);
+ return res;
+ } catch (err) {
+ await log(clientMethod, args, err?.response?.data);
+ throw err;
+ }
+ };
+ }
+}
diff --git a/packages/server/src/lib/Plaid/PlaidApiEventsDBSync.ts b/packages/server/src/lib/Plaid/PlaidApiEventsDBSync.ts
new file mode 100644
index 000000000..9d257b727
--- /dev/null
+++ b/packages/server/src/lib/Plaid/PlaidApiEventsDBSync.ts
@@ -0,0 +1,48 @@
+/**
+ * Creates a single Plaid api event log entry.
+ *
+ * @param {string} itemId the item id in the request.
+ * @param {string} userId the user id in the request.
+ * @param {string} plaidMethod the Plaid client method called.
+ * @param {Array} clientMethodArgs the arguments passed to the Plaid client method.
+ * @param {Object} response the Plaid api response object.
+ */
+export const createPlaidApiEvent = async (
+ itemId,
+ userId,
+ plaidMethod,
+ clientMethodArgs,
+ response
+) => {
+ const {
+ error_code: errorCode,
+ error_type: errorType,
+ request_id: requestId,
+ } = response;
+ const query = {
+ text: `
+ INSERT INTO plaid_api_events_table
+ (
+ item_id,
+ user_id,
+ plaid_method,
+ arguments,
+ request_id,
+ error_type,
+ error_code
+ )
+ VALUES
+ ($1, $2, $3, $4, $5, $6, $7);
+ `,
+ values: [
+ itemId,
+ userId,
+ plaidMethod,
+ JSON.stringify(clientMethodArgs),
+ requestId,
+ errorType,
+ errorCode,
+ ],
+ };
+ // await db.query(query);
+};
diff --git a/packages/server/src/lib/Plaid/index.ts b/packages/server/src/lib/Plaid/index.ts
new file mode 100644
index 000000000..9f3f903ca
--- /dev/null
+++ b/packages/server/src/lib/Plaid/index.ts
@@ -0,0 +1 @@
+export * from './Plaid';
diff --git a/packages/server/src/lib/Xlsx/TableSheet.tsx b/packages/server/src/lib/Xlsx/TableSheet.ts
similarity index 100%
rename from packages/server/src/lib/Xlsx/TableSheet.tsx
rename to packages/server/src/lib/Xlsx/TableSheet.ts
diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts
index fa1942b72..584fb22d9 100644
--- a/packages/server/src/loaders/eventEmitter.ts
+++ b/packages/server/src/loaders/eventEmitter.ts
@@ -84,6 +84,12 @@ import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subsc
import { BillTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/BillTaxRateValidateSubscriber';
import { WriteBillTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteBillTaxTransactionsSubscriber';
import { SyncItemTaxRateOnEditTaxSubscriber } from '@/services/TaxRates/SyncItemTaxRateOnEditTaxSubscriber';
+import { PlaidUpdateTransactionsOnItemCreatedSubscriber } from '@/services/Banking/Plaid/subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber';
+import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber';
+import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
+import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
+import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
+import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; }
export default () => {
return new EventPublisher();
@@ -104,8 +110,12 @@ export const susbcribers = () => {
InventorySubscriber,
CustomerWriteGLOpeningBalanceSubscriber,
VendorsWriteGLOpeningSubscriber,
+
+ // # Estimate
SaleEstimateAutoSerialSubscriber,
SaleEstimateSmsNotificationSubscriber,
+ SaleEstimateMarkApprovedOnMailSent,
+
ExpensesWriteGLSubscriber,
SaleReceiptAutoSerialSubscriber,
SaleInvoiceAutoIncrementSubscriber,
@@ -152,11 +162,13 @@ export const susbcribers = () => {
// #Invoices
InvoicePaymentGLRewriteSubscriber,
InvoiceCostGLEntriesSubscriber,
+ InvoiceChangeStatusOnMailSentSubscriber,
BillPaymentsGLEntriesRewriteSubscriber,
// # Receipts
SaleReceiptCostGLEntriesSubscriber,
+ SaleReceiptMarkClosedOnMailSentSubcriber,
// Transaction locking.
SalesTransactionLockingGuardSubscriber,
@@ -199,6 +211,13 @@ export const susbcribers = () => {
BillTaxRateValidateSubscriber,
WriteBillTaxTransactionsSubscriber,
- SyncItemTaxRateOnEditTaxSubscriber
+ SyncItemTaxRateOnEditTaxSubscriber,
+
+ // Plaid
+ PlaidUpdateTransactionsOnItemCreatedSubscriber,
+
+ // Cashflow
+ DeleteCashflowTransactionOnUncategorize,
+ PreventDeleteTransactionOnDelete
];
};
diff --git a/packages/server/src/loaders/express.ts b/packages/server/src/loaders/express.ts
index cf9787a95..694de8574 100644
--- a/packages/server/src/loaders/express.ts
+++ b/packages/server/src/loaders/express.ts
@@ -5,6 +5,8 @@ import boom from 'express-boom';
import errorHandler from 'errorhandler';
import bodyParser from 'body-parser';
import fileUpload from 'express-fileupload';
+import { Server } from 'socket.io';
+import Container from 'typedi';
import routes from 'api';
import LoggerMiddleware from '@/api/middleware/LoggerMiddleware';
import AgendashController from '@/api/controllers/Agendash';
@@ -72,4 +74,32 @@ export default ({ app }) => {
app.use((req: Request, res: Response, next: NextFunction) => {
return res.boom.notFound();
});
+ const server = app.listen(app.get('port'), (err) => {
+ if (err) {
+ console.log(err);
+ process.exit(1);
+ return;
+ }
+ console.log(`
+ ################################################
+ Server listening on port: ${app.get('port')}
+ ################################################
+ `);
+ });
+ const io = new Server(server, { path: '/socket' });
+
+ // Set socket.io listeners.
+ io.on('connection', (socket) => {
+ console.log('SOCKET CONNECTED');
+
+ socket.on('disconnect', () => {
+ console.log('SOCKET DISCONNECTED');
+ });
+ });
+ // Middleware to pass socket to each request object.
+ app.use((req: Request, res: Response, next: NextFunction) => {
+ req.io = io;
+ next();
+ });
+ Container.set('socket', io);
};
diff --git a/packages/server/src/loaders/jobs.ts b/packages/server/src/loaders/jobs.ts
index 4fa3aadb1..3215c9558 100644
--- a/packages/server/src/loaders/jobs.ts
+++ b/packages/server/src/loaders/jobs.ts
@@ -5,6 +5,12 @@ 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';
+import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda);
@@ -13,6 +19,12 @@ 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);
+ new PlaidFetchTransactionsJob(agenda);
agenda.start();
};
diff --git a/packages/server/src/loaders/tenantModels.ts b/packages/server/src/loaders/tenantModels.ts
index 8640819d4..0608d5e94 100644
--- a/packages/server/src/loaders/tenantModels.ts
+++ b/packages/server/src/loaders/tenantModels.ts
@@ -62,6 +62,8 @@ import TaxRate from 'models/TaxRate';
import TaxRateTransaction from 'models/TaxRateTransaction';
import Attachment from 'models/Attachment';
import Import from 'models/Import';
+import PlaidItem from 'models/PlaidItem';
+import UncategorizedCashflowTransaction from 'models/UncategorizedCashflowTransaction';
export default (knex) => {
const models = {
@@ -126,7 +128,9 @@ export default (knex) => {
TaxRate,
TaxRateTransaction,
Attachment,
- Import
+ Import,
+ PlaidItem,
+ UncategorizedCashflowTransaction
};
return mapValues(models, (model) => model.bindKnex(knex));
};
diff --git a/packages/server/src/models/Account.ts b/packages/server/src/models/Account.ts
index 9d4fb053e..7e0d8d6e4 100644
--- a/packages/server/src/models/Account.ts
+++ b/packages/server/src/models/Account.ts
@@ -196,6 +196,7 @@ export default class Account extends mixin(TenantModel, [
const Expense = require('models/Expense');
const ExpenseEntry = require('models/ExpenseCategory');
const ItemEntry = require('models/ItemEntry');
+ const UncategorizedTransaction = require('models/UncategorizedCashflowTransaction');
return {
/**
@@ -305,6 +306,21 @@ export default class Account extends mixin(TenantModel, [
to: 'items_entries.sellAccountId',
},
},
+
+ /**
+ * Associated uncategorized transactions.
+ */
+ uncategorizedTransactions: {
+ relation: Model.HasManyRelation,
+ modelClass: UncategorizedTransaction.default,
+ join: {
+ from: 'accounts.id',
+ to: 'uncategorized_cashflow_transactions.accountId',
+ },
+ filter: (query) => {
+ query.where('categorized', false);
+ },
+ },
};
}
diff --git a/packages/server/src/models/CashflowTransaction.ts b/packages/server/src/models/CashflowTransaction.ts
index 2184d1434..3cc2baba7 100644
--- a/packages/server/src/models/CashflowTransaction.ts
+++ b/packages/server/src/models/CashflowTransaction.ts
@@ -7,8 +7,14 @@ 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;
+ uncategorize: boolean;
+ uncategorizedTransaction!: boolean;
+
/**
* Table name.
*/
@@ -55,9 +61,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() {
@@ -80,6 +87,14 @@ export default class CashflowTransaction extends TenantModel {
return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN;
}
+ /**
+ * Detarmines whether the transaction imported from uncategorized transaction.
+ * @returns {boolean}
+ */
+ get isCategroizedTranasction() {
+ return !!this.uncategorizedTransaction;
+ }
+
/**
* Relationship mapping.
*/
diff --git a/packages/server/src/models/Contact.ts b/packages/server/src/models/Contact.ts
index d63a2ea60..69661f639 100644
--- a/packages/server/src/models/Contact.ts
+++ b/packages/server/src/models/Contact.ts
@@ -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
*/
diff --git a/packages/server/src/models/Customer.ts b/packages/server/src/models/Customer.ts
index 690b77d55..631763b71 100644
--- a/packages/server/src/models/Customer.ts
+++ b/packages/server/src/models/Customer.ts
@@ -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.
*/
diff --git a/packages/server/src/models/Expense.ts b/packages/server/src/models/Expense.ts
index ed756e2bb..b2fce9a65 100644
--- a/packages/server/src/models/Expense.ts
+++ b/packages/server/src/models/Expense.ts
@@ -215,6 +215,10 @@ export default class Expense extends mixin(TenantModel, [
to: 'branches.id',
},
},
+
+ /**
+ *
+ */
media: {
relation: Model.ManyToManyRelation,
modelClass: Media.default,
diff --git a/packages/server/src/models/ExpenseCategory.ts b/packages/server/src/models/ExpenseCategory.ts
index 50416805e..21d61f7e8 100644
--- a/packages/server/src/models/ExpenseCategory.ts
+++ b/packages/server/src/models/ExpenseCategory.ts
@@ -2,6 +2,8 @@ import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class ExpenseCategory extends TenantModel {
+ amount: number;
+
/**
* Table name
*/
diff --git a/packages/server/src/models/PlaidItem.ts b/packages/server/src/models/PlaidItem.ts
new file mode 100644
index 000000000..6dc515394
--- /dev/null
+++ b/packages/server/src/models/PlaidItem.ts
@@ -0,0 +1,24 @@
+import TenantModel from 'models/TenantModel';
+
+export default class PlaidItem extends TenantModel {
+ /**
+ * Table name.
+ */
+ static get tableName() {
+ return 'plaid_items';
+ }
+
+ /**
+ * Timestamps columns.
+ */
+ get timestamps() {
+ return [];
+ }
+
+ /**
+ * Relationship mapping.
+ */
+ static get relationMappings() {
+ return {};
+ }
+}
diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts
new file mode 100644
index 000000000..928db9a4d
--- /dev/null
+++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts
@@ -0,0 +1,140 @@
+/* eslint-disable global-require */
+import TenantModel from 'models/TenantModel';
+import { Model, ModelOptions, QueryContext } from 'objection';
+import Account from './Account';
+
+export default class UncategorizedCashflowTransaction extends TenantModel {
+ id!: number;
+ amount!: number;
+ categorized!: boolean;
+ accountId!: number;
+
+ /**
+ * Table name.
+ */
+ static get tableName() {
+ return 'uncategorized_cashflow_transactions';
+ }
+
+ /**
+ * Timestamps columns.
+ */
+ static get timestamps() {
+ return ['createdAt', 'updatedAt'];
+ }
+
+ /**
+ * Virtual attributes.
+ */
+ static get virtualAttributes() {
+ return [
+ 'withdrawal',
+ 'deposit',
+ 'isDepositTransaction',
+ 'isWithdrawalTransaction',
+ ];
+ }
+
+ /**
+ * Retrieves the withdrawal amount.
+ * @returns {number}
+ */
+ public get withdrawal() {
+ return this.amount < 0 ? Math.abs(this.amount) : 0;
+ }
+
+ /**
+ * Retrieves the deposit amount.
+ * @returns {number}
+ */
+ public get deposit(): number {
+ return this.amount > 0 ? Math.abs(this.amount) : 0;
+ }
+
+ /**
+ * Detarmines whether the transaction is deposit transaction.
+ */
+ public get isDepositTransaction(): boolean {
+ return 0 < this.deposit;
+ }
+
+ /**
+ * Detarmines whether the transaction is withdrawal transaction.
+ */
+ public get isWithdrawalTransaction(): boolean {
+ return 0 < this.withdrawal;
+ }
+
+ /**
+ * Relationship mapping.
+ */
+ static get relationMappings() {
+ const Account = require('models/Account');
+
+ return {
+ /**
+ * Transaction may has associated to account.
+ */
+ account: {
+ relation: Model.BelongsToOneRelation,
+ modelClass: Account.default,
+ join: {
+ from: 'uncategorized_cashflow_transactions.accountId',
+ to: 'accounts.id',
+ },
+ },
+ };
+ }
+
+ /**
+ * Updates the count of uncategorized transactions for the associated account
+ * based on the specified operation.
+ * @param {QueryContext} queryContext - The query context for the transaction.
+ * @param {boolean} increment - Indicates whether to increment or decrement the count.
+ */
+ private async updateUncategorizedTransactionCount(
+ queryContext: QueryContext,
+ increment: boolean
+ ) {
+ const operation = increment ? 'increment' : 'decrement';
+ const amount = increment ? 1 : -1;
+
+ await Account.query(queryContext.transaction)
+ .findById(this.accountId)
+ [operation]('uncategorized_transactions', amount);
+ }
+
+ /**
+ * Runs after insert.
+ * @param {QueryContext} queryContext
+ */
+ public async $afterInsert(queryContext) {
+ await super.$afterInsert(queryContext);
+ await this.updateUncategorizedTransactionCount(queryContext, true);
+ }
+
+ /**
+ * Runs after update.
+ * @param {ModelOptions} opt
+ * @param {QueryContext} queryContext
+ */
+ public async $afterUpdate(
+ opt: ModelOptions,
+ queryContext: QueryContext
+ ): Promise {
+ await super.$afterUpdate(opt, queryContext);
+
+ if (this.id && this.categorized) {
+ await this.updateUncategorizedTransactionCount(queryContext, false);
+ }
+ }
+
+ /**
+ * Runs after delete.
+ * @param {QueryContext} queryContext
+ */
+ public async $afterDelete(queryContext: QueryContext) {
+ await super.$afterDelete(queryContext);
+ await this.updateUncategorizedTransactionCount(queryContext, false);
+ }
+}
diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts
index 2bbc9a789..9c08e6533 100644
--- a/packages/server/src/server.ts
+++ b/packages/server/src/server.ts
@@ -10,19 +10,6 @@ async function startServer() {
// Intiialize all registered loaders.
await loadersFactory({ expressApp: app });
-
- app.listen(app.get('port'), (err) => {
- if (err) {
- console.log(err);
- process.exit(1);
- return;
- }
- console.log(`
- ################################################
- Server listening on port: ${app.get('port')}
- ################################################
- `);
- });
}
startServer();
diff --git a/packages/server/src/services/Accounts/AccountTransactionTransformer.ts b/packages/server/src/services/Accounts/AccountTransactionTransformer.ts
index 857fe5ccc..d0e3e487f 100644
--- a/packages/server/src/services/Accounts/AccountTransactionTransformer.ts
+++ b/packages/server/src/services/Accounts/AccountTransactionTransformer.ts
@@ -1,6 +1,5 @@
import { IAccountTransaction } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
-import { transaction } from 'objection';
export default class AccountTransactionTransformer extends Transformer {
/**
diff --git a/packages/server/src/services/Accounts/AccountTransform.ts b/packages/server/src/services/Accounts/AccountTransform.ts
index 9297994be..cb58a9be9 100644
--- a/packages/server/src/services/Accounts/AccountTransform.ts
+++ b/packages/server/src/services/Accounts/AccountTransform.ts
@@ -13,7 +13,7 @@ export class AccountTransformer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
- return ['formattedAmount', 'flattenName'];
+ return ['formattedAmount', 'flattenName', 'bankBalanceFormatted'];
};
/**
@@ -34,13 +34,24 @@ export class AccountTransformer extends Transformer {
/**
* Retrieve formatted account amount.
- * @param {IAccount} invoice
+ * @param {IAccount} invoice
* @returns {string}
*/
protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, { currencyCode: account.currencyCode });
};
+ /**
+ * Retrieves the formatted bank balance.
+ * @param {IAccount} account
+ * @returns {string}
+ */
+ protected bankBalanceFormatted = (account: IAccount): string => {
+ return formatNumber(account.bankBalance, {
+ currencyCode: account.currencyCode,
+ });
+ };
+
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}
diff --git a/packages/server/src/services/Accounts/AccountsApplication.ts b/packages/server/src/services/Accounts/AccountsApplication.ts
index 5e7043b9a..8182b3058 100644
--- a/packages/server/src/services/Accounts/AccountsApplication.ts
+++ b/packages/server/src/services/Accounts/AccountsApplication.ts
@@ -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}
+ * @param {number} tenantId
+ * @param {IAccountsTransactionsFilter} filter
+ * @returns {Promise}
*/
public getAccountsTransactions = (
tenantId: number,
diff --git a/packages/server/src/services/Accounts/ActivateAccount.ts b/packages/server/src/services/Accounts/ActivateAccount.ts
index 1fcd104f6..26afd836d 100644
--- a/packages/server/src/services/Accounts/ActivateAccount.ts
+++ b/packages/server/src/services/Accounts/ActivateAccount.ts
@@ -5,7 +5,6 @@ import { IAccountEventActivatedPayload } from '@/interfaces';
import events from '@/subscribers/events';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
-import { CommandAccountValidators } from './CommandAccountValidators';
@Service()
export class ActivateAccount {
@@ -18,9 +17,6 @@ export class ActivateAccount {
@Inject()
private uow: UnitOfWork;
- @Inject()
- private validator: CommandAccountValidators;
-
/**
* Activates/Inactivates the given account.
* @param {number} tenantId
diff --git a/packages/server/src/services/Banking/Plaid/PlaidApplication.ts b/packages/server/src/services/Banking/Plaid/PlaidApplication.ts
new file mode 100644
index 000000000..5b6b5d827
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidApplication.ts
@@ -0,0 +1,59 @@
+import { Inject, Service } from 'typedi';
+import { PlaidLinkTokenService } from './PlaidLinkToken';
+import { PlaidItemService } from './PlaidItem';
+import { PlaidItemDTO } from '@/interfaces';
+import { PlaidWebooks } from './PlaidWebhooks';
+
+@Service()
+export class PlaidApplication {
+ @Inject()
+ private getLinkTokenService: PlaidLinkTokenService;
+
+ @Inject()
+ private plaidItemService: PlaidItemService;
+
+ @Inject()
+ private plaidWebhooks: PlaidWebooks;
+
+ /**
+ * Retrieves the Plaid link token.
+ * @param {number} tenantId
+ * @param {number} itemId
+ * @returns
+ */
+ public getLinkToken(tenantId: number) {
+ return this.getLinkTokenService.getLinkToken(tenantId);
+ }
+
+ /**
+ * Exchanges the Plaid access token.
+ * @param {number} tenantId
+ * @param {PlaidItemDTO} itemDTO
+ * @returns
+ */
+ public exchangeToken(tenantId: number, itemDTO: PlaidItemDTO): Promise {
+ return this.plaidItemService.item(tenantId, itemDTO);
+ }
+
+ /**
+ * Listens to Plaid webhooks
+ * @param {number} tenantId
+ * @param {string} webhookType
+ * @param {string} plaidItemId
+ * @param {string} webhookCode
+ * @returns
+ */
+ public webhooks(
+ tenantId: number,
+ plaidItemId: string,
+ webhookType: string,
+ webhookCode: string
+ ) {
+ return this.plaidWebhooks.webhooks(
+ tenantId,
+ plaidItemId,
+ webhookType,
+ webhookCode
+ );
+ }
+}
diff --git a/packages/server/src/services/Banking/Plaid/PlaidFetchTransactionsJob.ts b/packages/server/src/services/Banking/Plaid/PlaidFetchTransactionsJob.ts
new file mode 100644
index 000000000..a397037c2
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidFetchTransactionsJob.ts
@@ -0,0 +1,43 @@
+import Container, { Service } from 'typedi';
+import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
+import { IPlaidItemCreatedEventPayload } from '@/interfaces';
+
+@Service()
+export class PlaidFetchTransactionsJob {
+ /**
+ * Constructor method.
+ */
+ constructor(agenda) {
+ agenda.define(
+ 'plaid-update-account-transactions',
+ { priority: 'high', concurrency: 2 },
+ this.handler
+ );
+ }
+
+ /**
+ * Triggers the function.
+ */
+ private handler = async (job, done: Function) => {
+ const { tenantId, plaidItemId } = job.attrs
+ .data as IPlaidItemCreatedEventPayload;
+
+ const plaidFetchTransactionsService = Container.get(
+ PlaidUpdateTransactions
+ );
+ const io = Container.get('socket');
+
+ try {
+ await plaidFetchTransactionsService.updateTransactions(
+ tenantId,
+ plaidItemId
+ );
+ // Notify the frontend to reflect the new transactions changes.
+ io.emit('NEW_TRANSACTIONS_DATA', { plaidItemId });
+ done();
+ } catch (error) {
+ console.log(error);
+ done(error);
+ }
+ };
+}
diff --git a/packages/server/src/services/Banking/Plaid/PlaidItem.ts b/packages/server/src/services/Banking/Plaid/PlaidItem.ts
new file mode 100644
index 000000000..9e83202f9
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidItem.ts
@@ -0,0 +1,58 @@
+import { Inject, Service } from 'typedi';
+import { PlaidClientWrapper } from '@/lib/Plaid';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import events from '@/subscribers/events';
+import {
+ IPlaidItemCreatedEventPayload,
+ PlaidItemDTO,
+} from '@/interfaces/Plaid';
+import SystemPlaidItem from '@/system/models/SystemPlaidItem';
+
+@Service()
+export class PlaidItemService {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ /**
+ * Exchanges the public token to get access token and item id and then creates
+ * a new Plaid item.
+ * @param {number} tenantId
+ * @param {PlaidItemDTO} itemDTO
+ * @returns {Promise}
+ */
+ public async item(tenantId: number, itemDTO: PlaidItemDTO): Promise {
+ const { PlaidItem } = this.tenancy.models(tenantId);
+ const { publicToken, institutionId } = itemDTO;
+
+ const plaidInstance = new PlaidClientWrapper();
+
+ // Exchange the public token for a private access token and store with the item.
+ const response = await plaidInstance.itemPublicTokenExchange({
+ public_token: publicToken,
+ });
+ const plaidAccessToken = response.data.access_token;
+ const plaidItemId = response.data.item_id;
+
+ // Store the Plaid item metadata on tenant scope.
+ const plaidItem = await PlaidItem.query().insertAndFetch({
+ tenantId,
+ plaidAccessToken,
+ plaidItemId,
+ plaidInstitutionId: institutionId,
+ });
+ // Stores the Plaid item id on system scope.
+ await SystemPlaidItem.query().insert({ tenantId, plaidItemId });
+
+ // Triggers `onPlaidItemCreated` event.
+ await this.eventPublisher.emitAsync(events.plaid.onItemCreated, {
+ tenantId,
+ plaidAccessToken,
+ plaidItemId,
+ plaidInstitutionId: institutionId,
+ } as IPlaidItemCreatedEventPayload);
+ }
+}
diff --git a/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts b/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts
new file mode 100644
index 000000000..89203df72
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidLinkToken.ts
@@ -0,0 +1,34 @@
+import { PlaidClientWrapper } from '@/lib/Plaid';
+import { Service } from 'typedi';
+import config from '@/config';
+
+@Service()
+export class PlaidLinkTokenService {
+ /**
+ * Retrieves the plaid link token.
+ * @param {number} tenantId
+ * @returns
+ */
+ async getLinkToken(tenantId: number) {
+ const accessToken = null;
+
+ // Must include transactions in order to receive transactions webhooks
+ const products = ['transactions'];
+ const linkTokenParams = {
+ user: {
+ // This should correspond to a unique id for the current user.
+ client_user_id: 'uniqueId' + tenantId,
+ },
+ client_name: 'Pattern',
+ products,
+ country_codes: ['US'],
+ language: 'en',
+ webhook: config.plaid.linkWebhook,
+ access_token: accessToken,
+ };
+ const plaidInstance = new PlaidClientWrapper();
+ const createResponse = await plaidInstance.linkTokenCreate(linkTokenParams);
+
+ return createResponse.data;
+ }
+}
diff --git a/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts b/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts
new file mode 100644
index 000000000..b3bf85ddc
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts
@@ -0,0 +1,198 @@
+import * as R from 'ramda';
+import { Inject, Service } from 'typedi';
+import bluebird from 'bluebird';
+import { entries, groupBy } from 'lodash';
+import { CreateAccount } from '@/services/Accounts/CreateAccount';
+import { PlaidAccount, PlaidTransaction } from '@/interfaces';
+import {
+ transformPlaidAccountToCreateAccount,
+ transformPlaidTrxsToCashflowCreate,
+} from './utils';
+import { DeleteCashflowTransaction } from '@/services/Cashflow/DeleteCashflowTransactionService';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
+
+const CONCURRENCY_ASYNC = 10;
+
+@Service()
+export class PlaidSyncDb {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private createAccountService: CreateAccount;
+
+ @Inject()
+ private cashflowApp: CashflowApplication;
+
+ @Inject()
+ private deleteCashflowTransactionService: DeleteCashflowTransaction;
+
+ /**
+ * Syncs the plaid accounts to the system accounts.
+ * @param {number} tenantId Tenant ID.
+ * @param {PlaidAccount[]} plaidAccounts
+ * @returns {Promise}
+ */
+ public async syncBankAccounts(
+ tenantId: number,
+ plaidAccounts: PlaidAccount[],
+ institution: any
+ ): Promise {
+ const transformToPlaidAccounts =
+ transformPlaidAccountToCreateAccount(institution);
+
+ const accountCreateDTOs = R.map(transformToPlaidAccounts)(plaidAccounts);
+
+ await bluebird.map(
+ accountCreateDTOs,
+ (createAccountDTO: any) =>
+ this.createAccountService.createAccount(tenantId, createAccountDTO),
+ { concurrency: CONCURRENCY_ASYNC }
+ );
+ }
+
+ /**
+ * Synsc the Plaid transactions to the system GL entries.
+ * @param {number} tenantId - Tenant ID.
+ * @param {number} plaidAccountId - Plaid account ID.
+ * @param {PlaidTransaction[]} plaidTranasctions - Plaid transactions
+ */
+ public async syncAccountTranactions(
+ tenantId: number,
+ plaidAccountId: number,
+ plaidTranasctions: PlaidTransaction[]
+ ): Promise {
+ const { Account } = this.tenancy.models(tenantId);
+
+ const cashflowAccount = await Account.query()
+ .findOne({ plaidAccountId })
+ .throwIfNotFound();
+
+ const openingEquityBalance = await Account.query().findOne(
+ 'slug',
+ 'opening-balance-equity'
+ );
+ // Transformes the Plaid transactions to cashflow create DTOs.
+ const transformTransaction = transformPlaidTrxsToCashflowCreate(
+ cashflowAccount.id,
+ openingEquityBalance.id
+ );
+ const uncategorizedTransDTOs =
+ R.map(transformTransaction)(plaidTranasctions);
+
+ // Creating account transaction queue.
+ await bluebird.map(
+ uncategorizedTransDTOs,
+ (uncategoriedDTO) =>
+ this.cashflowApp.createUncategorizedTransaction(
+ tenantId,
+ uncategoriedDTO
+ ),
+ { concurrency: 1 }
+ );
+ }
+
+ /**
+ * Syncs the accounts transactions in paraller under controlled concurrency.
+ * @param {number} tenantId
+ * @param {PlaidTransaction[]} plaidTransactions
+ */
+ public async syncAccountsTransactions(
+ tenantId: number,
+ plaidAccountsTransactions: PlaidTransaction[]
+ ): Promise {
+ const groupedTrnsxByAccountId = entries(
+ groupBy(plaidAccountsTransactions, 'account_id')
+ );
+ await bluebird.map(
+ groupedTrnsxByAccountId,
+ ([plaidAccountId, plaidTransactions]: [number, PlaidTransaction[]]) => {
+ return this.syncAccountTranactions(
+ tenantId,
+ plaidAccountId,
+ plaidTransactions
+ );
+ },
+ { concurrency: CONCURRENCY_ASYNC }
+ );
+ }
+
+ /**
+ * Syncs the removed Plaid transactions ids from the cashflow system transactions.
+ * @param {string[]} plaidTransactionsIds - Plaid Transactions IDs.
+ */
+ public async syncRemoveTransactions(
+ tenantId: number,
+ plaidTransactionsIds: string[]
+ ) {
+ const { CashflowTransaction } = this.tenancy.models(tenantId);
+
+ const cashflowTransactions = await CashflowTransaction.query().whereIn(
+ 'plaidTransactionId',
+ plaidTransactionsIds
+ );
+ const cashflowTransactionsIds = cashflowTransactions.map(
+ (trans) => trans.id
+ );
+ await bluebird.map(
+ cashflowTransactionsIds,
+ (transactionId: number) =>
+ this.deleteCashflowTransactionService.deleteCashflowTransaction(
+ tenantId,
+ transactionId
+ ),
+ { concurrency: CONCURRENCY_ASYNC }
+ );
+ }
+
+ /**
+ * Syncs the Plaid item last transaction cursor.
+ * @param {number} tenantId - Tenant ID.
+ * @param {string} itemId - Plaid item ID.
+ * @param {string} lastCursor - Last transaction cursor.
+ */
+ public async syncTransactionsCursor(
+ tenantId: number,
+ plaidItemId: string,
+ lastCursor: string
+ ) {
+ const { PlaidItem } = this.tenancy.models(tenantId);
+
+ await PlaidItem.query().findOne({ plaidItemId }).patch({ lastCursor });
+ }
+
+ /**
+ * Updates the last feeds updated at of the given Plaid accounts ids.
+ * @param {number} tenantId
+ * @param {string[]} plaidAccountIds
+ */
+ public async updateLastFeedsUpdatedAt(
+ tenantId: number,
+ plaidAccountIds: string[]
+ ) {
+ const { Account } = this.tenancy.models(tenantId);
+
+ await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
+ lastFeedsUpdatedAt: new Date(),
+ });
+ }
+
+ /**
+ * Updates the accounts feed active status of the given Plaid accounts ids.
+ * @param {number} tenantId
+ * @param {number[]} plaidAccountIds
+ * @param {boolean} isFeedsActive
+ */
+ public async updateAccountsFeedsActive(
+ tenantId: number,
+ plaidAccountIds: string[],
+ isFeedsActive: boolean = true
+ ) {
+ const { Account } = this.tenancy.models(tenantId);
+
+ await Account.query().whereIn('plaid_account_id', plaidAccountIds).patch({
+ isFeedsActive,
+ });
+ }
+}
diff --git a/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts b/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts
new file mode 100644
index 000000000..c740e4705
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts
@@ -0,0 +1,119 @@
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+import { Inject, Service } from 'typedi';
+import { PlaidClientWrapper } from '@/lib/Plaid/Plaid';
+import { PlaidSyncDb } from './PlaidSyncDB';
+import { PlaidFetchedTransactionsUpdates } from '@/interfaces';
+
+@Service()
+export class PlaidUpdateTransactions {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private plaidSync: PlaidSyncDb;
+
+ /**
+ * Handles the fetching and storing of new, modified, or removed transactions
+ * @param {number} tenantId Tenant ID.
+ * @param {string} plaidItemId the Plaid ID for the item.
+ */
+ public async updateTransactions(tenantId: number, plaidItemId: string) {
+ // Fetch new transactions from plaid api.
+ const { added, modified, removed, cursor, accessToken } =
+ await this.fetchTransactionUpdates(tenantId, plaidItemId);
+
+ const request = { access_token: accessToken };
+ const plaidInstance = new PlaidClientWrapper();
+ const {
+ data: { accounts, item },
+ } = await plaidInstance.accountsGet(request);
+
+ const plaidAccountsIds = accounts.map((a) => a.account_id);
+
+ const {
+ data: { institution },
+ } = await plaidInstance.institutionsGetById({
+ institution_id: item.institution_id,
+ country_codes: ['US', 'UK'],
+ });
+ // Update the DB.
+ await this.plaidSync.syncBankAccounts(tenantId, accounts, institution);
+ await this.plaidSync.syncAccountsTransactions(
+ tenantId,
+ added.concat(modified)
+ );
+ await this.plaidSync.syncRemoveTransactions(tenantId, removed);
+ await this.plaidSync.syncTransactionsCursor(tenantId, plaidItemId, cursor);
+
+ // Update the last feeds updated at of the updated accounts.
+ await this.plaidSync.updateLastFeedsUpdatedAt(tenantId, plaidAccountsIds);
+
+ // Turn on the accounts feeds flag.
+ await this.plaidSync.updateAccountsFeedsActive(tenantId, plaidAccountsIds);
+
+ return {
+ addedCount: added.length,
+ modifiedCount: modified.length,
+ removedCount: removed.length,
+ };
+ }
+
+ /**
+ * Fetches transactions from the `Plaid API` for a given item.
+ * @param {number} tenantId - Tenant ID.
+ * @param {string} plaidItemId - The Plaid ID for the item.
+ * @returns {Promise}
+ */
+ private async fetchTransactionUpdates(
+ tenantId: number,
+ plaidItemId: string
+ ): Promise {
+ // the transactions endpoint is paginated, so we may need to hit it multiple times to
+ // retrieve all available transactions.
+ const { PlaidItem } = this.tenancy.models(tenantId);
+
+ const plaidItem = await PlaidItem.query().findOne(
+ 'plaidItemId',
+ plaidItemId
+ );
+ if (!plaidItem) {
+ throw new Error('The given Plaid item id is not found.');
+ }
+ const { plaidAccessToken, lastCursor } = plaidItem;
+ let cursor = lastCursor;
+
+ // New transaction updates since "cursor"
+ let added = [];
+ let modified = [];
+ // Removed transaction ids
+ let removed = [];
+ let hasMore = true;
+
+ const batchSize = 100;
+ try {
+ // Iterate through each page of new transaction updates for item
+ /* eslint-disable no-await-in-loop */
+ while (hasMore) {
+ const request = {
+ access_token: plaidAccessToken,
+ cursor: cursor,
+ count: batchSize,
+ };
+ const plaidInstance = new PlaidClientWrapper();
+ const response = await plaidInstance.transactionsSync(request);
+ const data = response.data;
+ // Add this page of results
+ added = added.concat(data.added);
+ modified = modified.concat(data.modified);
+ removed = removed.concat(data.removed);
+ hasMore = data.has_more;
+ // Update cursor to the next cursor
+ cursor = data.next_cursor;
+ }
+ } catch (err) {
+ console.error(`Error fetching transactions: ${err.message}`);
+ cursor = lastCursor;
+ }
+ return { added, modified, removed, cursor, accessToken: plaidAccessToken };
+ }
+}
diff --git a/packages/server/src/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware.ts b/packages/server/src/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware.ts
new file mode 100644
index 000000000..6a1c177ba
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware.ts
@@ -0,0 +1,32 @@
+import { Request, Response, NextFunction } from 'express';
+import { SystemPlaidItem, Tenant } from '@/system/models';
+import tenantDependencyInjection from '@/api/middleware/TenantDependencyInjection';
+
+export const PlaidWebhookTenantBootMiddleware = async (
+ req: Request,
+ res: Response,
+ next: NextFunction
+) => {
+ const { item_id: plaidItemId } = req.body;
+ const plaidItem = await SystemPlaidItem.query().findOne({ plaidItemId });
+
+ const notFoundOrganization = () => {
+ return res.boom.unauthorized('Organization identication not found.', {
+ errors: [{ type: 'ORGANIZATION.ID.NOT.FOUND', code: 100 }],
+ });
+ };
+ // In case the given organization not found.
+ if (!plaidItem) {
+ return notFoundOrganization();
+ }
+ const tenant = await Tenant.query()
+ .findById(plaidItem.tenantId)
+ .withGraphFetched('metadata');
+
+ // When the given organization id not found on the system storage.
+ if (!tenant) {
+ return notFoundOrganization();
+ }
+ tenantDependencyInjection(req, tenant);
+ next();
+};
diff --git a/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts
new file mode 100644
index 000000000..5c3afb1ec
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/PlaidWebhooks.ts
@@ -0,0 +1,140 @@
+import { Inject, Service } from 'typedi';
+import { PlaidUpdateTransactions } from './PlaidUpdateTransactions';
+
+@Service()
+export class PlaidWebooks {
+ @Inject()
+ private updateTransactionsService: PlaidUpdateTransactions;
+
+ /**
+ * Listens to Plaid webhooks
+ * @param {number} tenantId - Tenant Id.
+ * @param {string} webhookType - Webhook type.
+ * @param {string} plaidItemId - Plaid item Id.
+ * @param {string} webhookCode - webhook code.
+ */
+ public async webhooks(
+ tenantId: number,
+ plaidItemId: string,
+ webhookType: string,
+ webhookCode: string
+ ): Promise {
+ const _webhookType = webhookType.toLowerCase();
+
+ // There are five types of webhooks: AUTH, TRANSACTIONS, ITEM, INCOME, and ASSETS.
+ // @TODO implement handling for remaining webhook types.
+ const webhookHandlerMap = {
+ transactions: this.handleTransactionsWebooks.bind(this),
+ item: this.itemsHandler.bind(this),
+ };
+ const webhookHandler =
+ webhookHandlerMap[_webhookType] || this.unhandledWebhook;
+
+ await webhookHandler(tenantId, plaidItemId, webhookCode);
+ }
+
+ /**
+ * Handles all unhandled/not yet implemented webhook events.
+ * @param {string} webhookType
+ * @param {string} webhookCode
+ * @param {string} plaidItemId
+ */
+ private async unhandledWebhook(
+ webhookType: string,
+ webhookCode: string,
+ plaidItemId: string
+ ): Promise {
+ console.log(
+ `UNHANDLED ${webhookType} WEBHOOK: ${webhookCode}: Plaid item id ${plaidItemId}: unhandled webhook type received.`
+ );
+ }
+
+ /**
+ * Logs to console and emits to socket
+ * @param {string} additionalInfo
+ * @param {string} webhookCode
+ * @param {string} plaidItemId
+ */
+ private serverLogAndEmitSocket(
+ additionalInfo: string,
+ webhookCode: string,
+ plaidItemId: string
+ ): void {
+ console.log(
+ `WEBHOOK: TRANSACTIONS: ${webhookCode}: Plaid_item_id ${plaidItemId}: ${additionalInfo}`
+ );
+ }
+
+ /**
+ * Handles all transaction webhook events. The transaction webhook notifies
+ * you that a single item has new transactions available.
+ * @param {number} tenantId
+ * @param {string} plaidItemId
+ * @param {string} webhookCode
+ * @returns {Promise}
+ */
+ public async handleTransactionsWebooks(
+ tenantId: number,
+ plaidItemId: string,
+ webhookCode: string
+ ): Promise {
+ switch (webhookCode) {
+ case 'SYNC_UPDATES_AVAILABLE': {
+ // Fired when new transactions data becomes available.
+ const { addedCount, modifiedCount, removedCount } =
+ await this.updateTransactionsService.updateTransactions(
+ tenantId,
+ plaidItemId
+ );
+ this.serverLogAndEmitSocket(
+ `Transactions: ${addedCount} added, ${modifiedCount} modified, ${removedCount} removed`,
+ webhookCode,
+ plaidItemId
+ );
+ break;
+ }
+ case 'DEFAULT_UPDATE':
+ case 'INITIAL_UPDATE':
+ case 'HISTORICAL_UPDATE':
+ /* ignore - not needed if using sync endpoint + webhook */
+ break;
+ default:
+ this.serverLogAndEmitSocket(
+ `unhandled webhook type received.`,
+ webhookCode,
+ plaidItemId
+ );
+ }
+ }
+
+ /**
+ * Handles all Item webhook events.
+ * @param {number} tenantId - Tenant ID
+ * @param {string} webhookCode - The webhook code
+ * @param {string} plaidItemId - The Plaid ID for the item
+ * @returns {Promise}
+ */
+ public async itemsHandler(
+ tenantId: number,
+ plaidItemId: string,
+ webhookCode: string
+ ): Promise {
+ switch (webhookCode) {
+ case 'WEBHOOK_UPDATE_ACKNOWLEDGED':
+ this.serverLogAndEmitSocket('is updated', plaidItemId, error);
+ break;
+ case 'ERROR': {
+ break;
+ }
+ case 'PENDING_EXPIRATION': {
+ break;
+ }
+ default:
+ this.serverLogAndEmitSocket(
+ 'unhandled webhook type received.',
+ webhookCode,
+ plaidItemId
+ );
+ }
+ }
+}
diff --git a/packages/server/src/services/Banking/Plaid/subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber.ts b/packages/server/src/services/Banking/Plaid/subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber.ts
new file mode 100644
index 000000000..216eae2f2
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/subscribers/PlaidUpdateTransactionsOnItemCreatedSubscriber.ts
@@ -0,0 +1,34 @@
+import { Inject, Service } from 'typedi';
+import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
+import { IPlaidItemCreatedEventPayload } from '@/interfaces/Plaid';
+import events from '@/subscribers/events';
+
+@Service()
+export class PlaidUpdateTransactionsOnItemCreatedSubscriber extends EventSubscriber {
+ @Inject('agenda')
+ private agenda: any;
+
+ /**
+ * Constructor method.
+ */
+ public attach(bus) {
+ bus.subscribe(
+ events.plaid.onItemCreated,
+ this.handleUpdateTransactionsOnItemCreated
+ );
+ }
+
+ /**
+ * Updates the Plaid item transactions
+ * @param {IPlaidItemCreatedEventPayload} payload - Event payload.
+ */
+ private handleUpdateTransactionsOnItemCreated = async ({
+ tenantId,
+ plaidItemId,
+ plaidAccessToken,
+ plaidInstitutionId,
+ }: IPlaidItemCreatedEventPayload) => {
+ const payload = { tenantId, plaidItemId };
+ await this.agenda.now('plaid-update-account-transactions', payload);
+ };
+}
diff --git a/packages/server/src/services/Banking/Plaid/utils.ts b/packages/server/src/services/Banking/Plaid/utils.ts
new file mode 100644
index 000000000..c8a3cf528
--- /dev/null
+++ b/packages/server/src/services/Banking/Plaid/utils.ts
@@ -0,0 +1,54 @@
+import * as R from 'ramda';
+import {
+ CreateUncategorizedTransactionDTO,
+ IAccountCreateDTO,
+ PlaidAccount,
+ PlaidTransaction,
+} from '@/interfaces';
+
+/**
+ * Transformes the Plaid account to create cashflow account DTO.
+ * @param {PlaidAccount} plaidAccount
+ * @returns {IAccountCreateDTO}
+ */
+export const transformPlaidAccountToCreateAccount = R.curry(
+ (institution: any, plaidAccount: PlaidAccount): IAccountCreateDTO => {
+ return {
+ name: `${institution.name} - ${plaidAccount.name}`,
+ code: '',
+ description: plaidAccount.official_name,
+ currencyCode: plaidAccount.balances.iso_currency_code,
+ accountType: 'cash',
+ active: true,
+ plaidAccountId: plaidAccount.account_id,
+ bankBalance: plaidAccount.balances.current,
+ accountMask: plaidAccount.mask,
+ };
+ }
+);
+
+/**
+ * Transformes the plaid transaction to cashflow create DTO.
+ * @param {number} cashflowAccountId - Cashflow account ID.
+ * @param {number} creditAccountId - Credit account ID.
+ * @param {PlaidTransaction} plaidTranasction - Plaid transaction.
+ * @returns {CreateUncategorizedTransactionDTO}
+ */
+export const transformPlaidTrxsToCashflowCreate = R.curry(
+ (
+ cashflowAccountId: number,
+ creditAccountId: number,
+ plaidTranasction: PlaidTransaction
+ ): CreateUncategorizedTransactionDTO => {
+ return {
+ date: plaidTranasction.date,
+ amount: plaidTranasction.amount,
+ description: plaidTranasction.name,
+ payee: plaidTranasction.payment_meta?.payee,
+ currencyCode: plaidTranasction.iso_currency_code,
+ accountId: cashflowAccountId,
+ referenceNo: plaidTranasction.payment_meta?.reference_number,
+ plaidTransactionId: plaidTranasction.transaction_id,
+ };
+ }
+);
diff --git a/packages/server/src/services/Cashflow/CashflowApplication.ts b/packages/server/src/services/Cashflow/CashflowApplication.ts
new file mode 100644
index 000000000..6688c9016
--- /dev/null
+++ b/packages/server/src/services/Cashflow/CashflowApplication.ts
@@ -0,0 +1,213 @@
+import { Inject, Service } from 'typedi';
+import { DeleteCashflowTransaction } from './DeleteCashflowTransactionService';
+import { UncategorizeCashflowTransaction } from './UncategorizeCashflowTransaction';
+import { CategorizeCashflowTransaction } from './CategorizeCashflowTransaction';
+import {
+ CategorizeTransactionAsExpenseDTO,
+ CreateUncategorizedTransactionDTO,
+ ICashflowAccountsFilter,
+ ICashflowNewCommandDTO,
+ ICategorizeCashflowTransactioDTO,
+ IGetUncategorizedTransactionsQuery,
+} from '@/interfaces';
+import { CategorizeTransactionAsExpense } from './CategorizeTransactionAsExpense';
+import { GetUncategorizedTransactions } from './GetUncategorizedTransactions';
+import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
+import { GetUncategorizedTransaction } from './GetUncategorizedTransaction';
+import NewCashflowTransactionService from './NewCashflowTransactionService';
+import GetCashflowAccountsService from './GetCashflowAccountsService';
+import { GetCashflowTransactionService } from './GetCashflowTransactionsService';
+
+@Service()
+export class CashflowApplication {
+ @Inject()
+ private createTransactionService: NewCashflowTransactionService;
+
+ @Inject()
+ private deleteTransactionService: DeleteCashflowTransaction;
+
+ @Inject()
+ private getCashflowAccountsService: GetCashflowAccountsService;
+
+ @Inject()
+ private getCashflowTransactionService: GetCashflowTransactionService;
+
+ @Inject()
+ private uncategorizeTransactionService: UncategorizeCashflowTransaction;
+
+ @Inject()
+ private categorizeTransactionService: CategorizeCashflowTransaction;
+
+ @Inject()
+ private categorizeAsExpenseService: CategorizeTransactionAsExpense;
+
+ @Inject()
+ private getUncategorizedTransactionsService: GetUncategorizedTransactions;
+
+ @Inject()
+ private getUncategorizedTransactionService: GetUncategorizedTransaction;
+
+ @Inject()
+ private createUncategorizedTransactionService: CreateUncategorizedTransaction;
+
+ /**
+ * Creates a new cashflow transaction.
+ * @param {number} tenantId
+ * @param {ICashflowNewCommandDTO} transactionDTO
+ * @param {number} userId
+ * @returns
+ */
+ public createTransaction(
+ tenantId: number,
+ transactionDTO: ICashflowNewCommandDTO,
+ userId?: number
+ ) {
+ return this.createTransactionService.newCashflowTransaction(
+ tenantId,
+ transactionDTO,
+ userId
+ );
+ }
+
+ /**
+ * Deletes the given cashflow transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ * @returns
+ */
+ public deleteTransaction(tenantId: number, cashflowTransactionId: number) {
+ return this.deleteTransactionService.deleteCashflowTransaction(
+ tenantId,
+ cashflowTransactionId
+ );
+ }
+
+ /**
+ * Retrieves specific cashflow transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ * @returns
+ */
+ public getTransaction(tenantId: number, cashflowTransactionId: number) {
+ return this.getCashflowTransactionService.getCashflowTransaction(
+ tenantId,
+ cashflowTransactionId
+ );
+ }
+
+ /**
+ * Retrieves the cashflow accounts.
+ * @param {number} tenantId
+ * @param {ICashflowAccountsFilter} filterDTO
+ * @returns
+ */
+ public getCashflowAccounts(
+ tenantId: number,
+ filterDTO: ICashflowAccountsFilter
+ ) {
+ return this.getCashflowAccountsService.getCashflowAccounts(
+ tenantId,
+ filterDTO
+ );
+ }
+
+ /**
+ * Creates a new uncategorized cash transaction.
+ * @param {number} tenantId
+ * @param {CreateUncategorizedTransactionDTO} createUncategorizedTransactionDTO
+ * @returns {IUncategorizedCashflowTransaction}
+ */
+ public createUncategorizedTransaction(
+ tenantId: number,
+ createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO
+ ) {
+ return this.createUncategorizedTransactionService.create(
+ tenantId,
+ createUncategorizedTransactionDTO
+ );
+ }
+
+ /**
+ * Uncategorize the given cashflow transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ * @returns
+ */
+ public uncategorizeTransaction(
+ tenantId: number,
+ cashflowTransactionId: number
+ ) {
+ return this.uncategorizeTransactionService.uncategorize(
+ tenantId,
+ cashflowTransactionId
+ );
+ }
+
+ /**
+ * Categorize the given cashflow transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ * @param {ICategorizeCashflowTransactioDTO} categorizeDTO
+ * @returns
+ */
+ public categorizeTransaction(
+ tenantId: number,
+ cashflowTransactionId: number,
+ categorizeDTO: ICategorizeCashflowTransactioDTO
+ ) {
+ return this.categorizeTransactionService.categorize(
+ tenantId,
+ cashflowTransactionId,
+ categorizeDTO
+ );
+ }
+
+ /**
+ * Categorizes the given cashflow transaction as expense transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ * @param {CategorizeTransactionAsExpenseDTO} transactionDTO
+ */
+ public categorizeAsExpense(
+ tenantId: number,
+ cashflowTransactionId: number,
+ transactionDTO: CategorizeTransactionAsExpenseDTO
+ ) {
+ return this.categorizeAsExpenseService.categorize(
+ tenantId,
+ cashflowTransactionId,
+ transactionDTO
+ );
+ }
+
+ /**
+ * Retrieves the uncategorized cashflow transactions.
+ * @param {number} tenantId
+ */
+ public getUncategorizedTransactions(
+ tenantId: number,
+ accountId: number,
+ query: IGetUncategorizedTransactionsQuery
+ ) {
+ return this.getUncategorizedTransactionsService.getTransactions(
+ tenantId,
+ accountId,
+ query
+ );
+ }
+
+ /**
+ * Retrieves specific uncategorized transaction.
+ * @param {number} tenantId
+ * @param {number} uncategorizedTransactionId
+ */
+ public getUncategorizedTransaction(
+ tenantId: number,
+ uncategorizedTransactionId: number
+ ) {
+ return this.getUncategorizedTransactionService.getTransaction(
+ tenantId,
+ uncategorizedTransactionId
+ );
+ }
+}
diff --git a/packages/server/src/services/Cashflow/CashflowTransactionJournalEntries.ts b/packages/server/src/services/Cashflow/CashflowTransactionJournalEntries.ts
index 21df84f34..c1c590d55 100644
--- a/packages/server/src/services/Cashflow/CashflowTransactionJournalEntries.ts
+++ b/packages/server/src/services/Cashflow/CashflowTransactionJournalEntries.ts
@@ -1,11 +1,9 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
-import * as R from 'ramda';
import {
ILedgerEntry,
ICashflowTransaction,
AccountNormal,
- ICashflowTransactionLine,
} from '../../interfaces';
import {
transformCashflowTransactionType,
diff --git a/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts b/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts
new file mode 100644
index 000000000..3d19e1547
--- /dev/null
+++ b/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts
@@ -0,0 +1,101 @@
+import { Inject, Service } from 'typedi';
+import HasTenancyService from '../Tenancy/TenancyService';
+import events from '@/subscribers/events';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import UnitOfWork from '../UnitOfWork';
+import {
+ ICashflowTransactionCategorizedPayload,
+ ICashflowTransactionUncategorizingPayload,
+ ICategorizeCashflowTransactioDTO,
+} from '@/interfaces';
+import { Knex } from 'knex';
+import { transformCategorizeTransToCashflow } from './utils';
+import { CommandCashflowValidator } from './CommandCasflowValidator';
+import NewCashflowTransactionService from './NewCashflowTransactionService';
+import { TransferAuthorizationGuaranteeDecision } from 'plaid';
+
+@Service()
+export class CategorizeCashflowTransaction {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ @Inject()
+ private uow: UnitOfWork;
+
+ @Inject()
+ private commandValidators: CommandCashflowValidator;
+
+ @Inject()
+ private createCashflow: NewCashflowTransactionService;
+
+ /**
+ * Categorize the given cashflow transaction.
+ * @param {number} tenantId
+ * @param {ICategorizeCashflowTransactioDTO} categorizeDTO
+ */
+ public async categorize(
+ tenantId: number,
+ uncategorizedTransactionId: number,
+ categorizeDTO: ICategorizeCashflowTransactioDTO
+ ) {
+ const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
+
+ // Retrieves the uncategorized transaction or throw an error.
+ const transaction = await UncategorizedCashflowTransaction.query()
+ .findById(uncategorizedTransactionId)
+ .throwIfNotFound();
+
+ // Validates the transaction shouldn't be categorized before.
+ this.commandValidators.validateTransactionShouldNotCategorized(transaction);
+
+ // Validate the uncateogirzed transaction if it's deposit the transaction direction
+ // should `IN` and the same thing if it's withdrawal the direction should be OUT.
+ this.commandValidators.validateUncategorizeTransactionType(
+ transaction,
+ categorizeDTO.transactionType
+ );
+ // Edits the cashflow transaction under UOW env.
+ return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
+ // Triggers `onTransactionCategorizing` event.
+ await this.eventPublisher.emitAsync(
+ events.cashflow.onTransactionCategorizing,
+ {
+ tenantId,
+ trx,
+ } as ICashflowTransactionUncategorizingPayload
+ );
+ // Transformes the categorize DTO to the cashflow transaction.
+ const cashflowTransactionDTO = transformCategorizeTransToCashflow(
+ transaction,
+ categorizeDTO
+ );
+ // Creates a new cashflow transaction.
+ const cashflowTransaction =
+ await this.createCashflow.newCashflowTransaction(
+ tenantId,
+ cashflowTransactionDTO
+ );
+ // Updates the uncategorized transaction as categorized.
+ await UncategorizedCashflowTransaction.query(trx).patchAndFetchById(
+ uncategorizedTransactionId,
+ {
+ categorized: true,
+ categorizeRefType: 'CashflowTransaction',
+ categorizeRefId: cashflowTransaction.id,
+ }
+ );
+ // Triggers `onCashflowTransactionCategorized` event.
+ await this.eventPublisher.emitAsync(
+ events.cashflow.onTransactionCategorized,
+ {
+ tenantId,
+ // cashflowTransaction,
+ trx,
+ } as ICashflowTransactionCategorizedPayload
+ );
+ });
+ }
+}
diff --git a/packages/server/src/services/Cashflow/CategorizeTransactionAsExpense.ts b/packages/server/src/services/Cashflow/CategorizeTransactionAsExpense.ts
new file mode 100644
index 000000000..119f0cc7b
--- /dev/null
+++ b/packages/server/src/services/Cashflow/CategorizeTransactionAsExpense.ts
@@ -0,0 +1,80 @@
+import {
+ CategorizeTransactionAsExpenseDTO,
+ ICashflowTransactionCategorizedPayload,
+} from '@/interfaces';
+import { Inject, Service } from 'typedi';
+import UnitOfWork from '../UnitOfWork';
+import events from '@/subscribers/events';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import HasTenancyService from '../Tenancy/TenancyService';
+import { Knex } from 'knex';
+import { CreateExpense } from '../Expenses/CRUD/CreateExpense';
+
+@Service()
+export class CategorizeTransactionAsExpense {
+ @Inject()
+ private uow: UnitOfWork;
+
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ @Inject()
+ private createExpenseService: CreateExpense;
+
+ /**
+ * Categorize the transaction as expense transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ * @param {CategorizeTransactionAsExpenseDTO} transactionDTO
+ */
+ public async categorize(
+ tenantId: number,
+ cashflowTransactionId: number,
+ transactionDTO: CategorizeTransactionAsExpenseDTO
+ ) {
+ const { CashflowTransaction } = this.tenancy.models(tenantId);
+
+ const transaction = await CashflowTransaction.query()
+ .findById(cashflowTransactionId)
+ .throwIfNotFound();
+
+ return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
+ // Triggers `onTransactionUncategorizing` event.
+ await this.eventPublisher.emitAsync(
+ events.cashflow.onTransactionCategorizingAsExpense,
+ {
+ tenantId,
+ trx,
+ } as ICashflowTransactionCategorizedPayload
+ );
+ // Creates a new expense transaction.
+ const expenseTransaction = await this.createExpenseService.newExpense(
+ tenantId,
+ {
+
+ },
+ 1
+ );
+ // Updates the item on the storage and fetches the updated once.
+ const cashflowTransaction = await CashflowTransaction.query(
+ trx
+ ).patchAndFetchById(cashflowTransactionId, {
+ categorizeRefType: 'Expense',
+ categorizeRefId: expenseTransaction.id,
+ uncategorized: true,
+ });
+ // Triggers `onTransactionUncategorized` event.
+ await this.eventPublisher.emitAsync(
+ events.cashflow.onTransactionCategorizedAsExpense,
+ {
+ tenantId,
+ cashflowTransaction,
+ trx,
+ } as ICashflowTransactionUncategorizedPayload
+ );
+ });
+ }
+}
diff --git a/packages/server/src/services/Cashflow/CommandCasflowValidator.ts b/packages/server/src/services/Cashflow/CommandCasflowValidator.ts
index 07722e50e..7a6c5c973 100644
--- a/packages/server/src/services/Cashflow/CommandCasflowValidator.ts
+++ b/packages/server/src/services/Cashflow/CommandCasflowValidator.ts
@@ -1,9 +1,14 @@
import { Service } from 'typedi';
import { includes, camelCase, upperFirst } from 'lodash';
-import { IAccount } from '@/interfaces';
+import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces';
import { getCashflowTransactionType } from './utils';
import { ServiceError } from '@/exceptions';
-import { CASHFLOW_TRANSACTION_TYPE, ERRORS } from './constants';
+import {
+ CASHFLOW_DIRECTION,
+ CASHFLOW_TRANSACTION_TYPE,
+ ERRORS,
+} from './constants';
+import CashflowTransaction from '@/models/CashflowTransaction';
@Service()
export class CommandCashflowValidator {
@@ -46,4 +51,52 @@ export class CommandCashflowValidator {
}
return transformedType;
};
+
+ /**
+ * Validate the given transaction should be categorized.
+ * @param {CashflowTransaction} cashflowTransaction
+ */
+ public validateTransactionShouldCategorized(
+ cashflowTransaction: CashflowTransaction
+ ) {
+ if (!cashflowTransaction.uncategorize) {
+ throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
+ }
+ }
+
+ /**
+ * Validate the given transcation shouldn't be categorized.
+ * @param {CashflowTransaction} cashflowTransaction
+ */
+ public validateTransactionShouldNotCategorized(
+ cashflowTransaction: CashflowTransaction
+ ) {
+ if (cashflowTransaction.uncategorize) {
+ throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
+ }
+ }
+
+ /**
+ *
+ * @param {uncategorizeTransaction}
+ * @param {string} transactionType
+ * @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)}
+ */
+ public validateUncategorizeTransactionType(
+ uncategorizeTransaction: IUncategorizedCashflowTransaction,
+ transactionType: string
+ ) {
+ const type = getCashflowTransactionType(
+ upperFirst(camelCase(transactionType)) as CASHFLOW_TRANSACTION_TYPE
+ );
+ if (
+ (type.direction === CASHFLOW_DIRECTION.IN &&
+ uncategorizeTransaction.isDepositTransaction) ||
+ (type.direction === CASHFLOW_DIRECTION.OUT &&
+ uncategorizeTransaction.isWithdrawalTransaction)
+ ) {
+ return;
+ }
+ throw new ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID);
+ }
}
diff --git a/packages/server/src/services/Cashflow/CreateUncategorizedTransaction.ts b/packages/server/src/services/Cashflow/CreateUncategorizedTransaction.ts
new file mode 100644
index 000000000..ccb2aca25
--- /dev/null
+++ b/packages/server/src/services/Cashflow/CreateUncategorizedTransaction.ts
@@ -0,0 +1,40 @@
+import { Inject, Service } from 'typedi';
+import HasTenancyService from '../Tenancy/TenancyService';
+import UnitOfWork, { IsolationLevel } from '../UnitOfWork';
+import { Knex } from 'knex';
+import { CreateUncategorizedTransactionDTO } from '@/interfaces';
+
+@Service()
+export class CreateUncategorizedTransaction {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private uow: UnitOfWork;
+
+ /**
+ * Creates an uncategorized cashflow transaction.
+ * @param {number} tenantId
+ * @param {CreateUncategorizedTransactionDTO} createDTO
+ */
+ public create(
+ tenantId: number,
+ createDTO: CreateUncategorizedTransactionDTO
+ ) {
+ const { UncategorizedCashflowTransaction, Account } =
+ this.tenancy.models(tenantId);
+
+ return this.uow.withTransaction(
+ tenantId,
+ async (trx: Knex.Transaction) => {
+ const transaction = await UncategorizedCashflowTransaction.query(
+ trx
+ ).insertAndFetch({
+ ...createDTO,
+ });
+
+ return transaction;
+ },
+ );
+ }
+}
diff --git a/packages/server/src/services/Cashflow/DeleteCashflowTransactionService.ts b/packages/server/src/services/Cashflow/DeleteCashflowTransactionService.ts
index e07073c3d..dd6c3002a 100644
--- a/packages/server/src/services/Cashflow/DeleteCashflowTransactionService.ts
+++ b/packages/server/src/services/Cashflow/DeleteCashflowTransactionService.ts
@@ -13,15 +13,15 @@ import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
-export default class CommandCashflowTransactionService {
+export class DeleteCashflowTransaction {
@Inject()
- tenancy: HasTenancyService;
+ private tenancy: HasTenancyService;
@Inject()
- eventPublisher: EventPublisher;
+ private eventPublisher: EventPublisher;
@Inject()
- uow: UnitOfWork;
+ private uow: UnitOfWork;
/**
* Deletes the cashflow transaction with associated journal entries.
diff --git a/packages/server/src/services/Cashflow/GetCashflowTransactionsService.ts b/packages/server/src/services/Cashflow/GetCashflowTransactionsService.ts
index eb22dd305..64afd2194 100644
--- a/packages/server/src/services/Cashflow/GetCashflowTransactionsService.ts
+++ b/packages/server/src/services/Cashflow/GetCashflowTransactionsService.ts
@@ -4,17 +4,13 @@ import { CashflowTransactionTransformer } from './CashflowTransactionTransformer
import { ERRORS } from './constants';
import { ICashflowTransaction } from '@/interfaces';
import { ServiceError } from '@/exceptions';
-import I18nService from '@/services/I18n/I18nService';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
@Service()
-export default class GetCashflowTransactionsService {
+export class GetCashflowTransactionService {
@Inject()
private tenancy: HasTenancyService;
- @Inject()
- private i18nService: I18nService;
-
@Inject()
private transfromer: TransformerInjectable;
@@ -35,6 +31,7 @@ export default class GetCashflowTransactionsService {
.withGraphFetched('entries.cashflowAccount')
.withGraphFetched('entries.creditAccount')
.withGraphFetched('transactions.account')
+ .orderBy('date', 'DESC')
.throwIfNotFound();
this.throwErrorCashflowTranscationNotFound(cashflowTransaction);
diff --git a/packages/server/src/services/Cashflow/GetUncategorizedTransaction.ts b/packages/server/src/services/Cashflow/GetUncategorizedTransaction.ts
new file mode 100644
index 000000000..82cf531a8
--- /dev/null
+++ b/packages/server/src/services/Cashflow/GetUncategorizedTransaction.ts
@@ -0,0 +1,36 @@
+import { Inject, Service } from 'typedi';
+import HasTenancyService from '../Tenancy/TenancyService';
+import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
+import { UncategorizedTransactionTransformer } from './UncategorizedTransactionTransformer';
+
+@Service()
+export class GetUncategorizedTransaction {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private transformer: TransformerInjectable;
+
+ /**
+ * Retrieves specific uncategorized cashflow transaction.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} uncategorizedTransactionId - Uncategorized transaction id.
+ */
+ public async getTransaction(
+ tenantId: number,
+ uncategorizedTransactionId: number
+ ) {
+ const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
+
+ const transaction = await UncategorizedCashflowTransaction.query()
+ .findById(uncategorizedTransactionId)
+ .withGraphFetched('account')
+ .throwIfNotFound();
+
+ return this.transformer.transform(
+ tenantId,
+ transaction,
+ new UncategorizedTransactionTransformer()
+ );
+ }
+}
diff --git a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts
new file mode 100644
index 000000000..36606f582
--- /dev/null
+++ b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts
@@ -0,0 +1,51 @@
+import { Inject, Service } from 'typedi';
+import HasTenancyService from '../Tenancy/TenancyService';
+import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
+import { UncategorizedTransactionTransformer } from './UncategorizedTransactionTransformer';
+import { IGetUncategorizedTransactionsQuery } from '@/interfaces';
+
+@Service()
+export class GetUncategorizedTransactions {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private transformer: TransformerInjectable;
+
+ /**
+ * Retrieves the uncategorized cashflow transactions.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} accountId - Account Id.
+ */
+ public async getTransactions(
+ tenantId: number,
+ accountId: number,
+ query: IGetUncategorizedTransactionsQuery
+ ) {
+ const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
+
+ // Parsed query with default values.
+ const _query = {
+ page: 1,
+ pageSize: 20,
+ ...query,
+ };
+ const { results, pagination } =
+ await UncategorizedCashflowTransaction.query()
+ .where('accountId', accountId)
+ .where('categorized', false)
+ .withGraphFetched('account')
+ .orderBy('date', 'DESC')
+ .pagination(_query.page - 1, _query.pageSize);
+
+ const data = await this.transformer.transform(
+ tenantId,
+ results,
+ new UncategorizedTransactionTransformer()
+ );
+ return {
+ data,
+ pagination,
+ };
+ }
+}
diff --git a/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts b/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts
index 7c1c39d6f..ecc0d3267 100644
--- a/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts
+++ b/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts
@@ -1,11 +1,10 @@
import { Service, Inject } from 'typedi';
-import { isEmpty, pick } from 'lodash';
+import { pick } from 'lodash';
import { Knex } from 'knex';
import * as R from 'ramda';
import {
ICashflowNewCommandDTO,
ICashflowTransaction,
- ICashflowTransactionLine,
ICommandCashflowCreatedPayload,
ICommandCashflowCreatingPayload,
ICashflowTransactionInput,
@@ -86,6 +85,8 @@ export default class NewCashflowTransactionService {
'cashflowAccountId',
'creditAccountId',
'branchId',
+ 'plaidTransactionId',
+ 'uncategorizedTransactionId',
]);
// Retreive the next invoice number.
const autoNextNumber =
@@ -124,8 +125,8 @@ export default class NewCashflowTransactionService {
public newCashflowTransaction = async (
tenantId: number,
newTransactionDTO: ICashflowNewCommandDTO,
- userId: number
- ): Promise<{ cashflowTransaction: ICashflowTransaction }> => {
+ userId?: number
+ ): Promise => {
const { CashflowTransaction, Account } = this.tenancy.models(tenantId);
// Retrieves the cashflow account or throw not found error.
@@ -174,7 +175,7 @@ export default class NewCashflowTransactionService {
trx,
} as ICommandCashflowCreatedPayload
);
- return { cashflowTransaction };
+ return cashflowTransaction;
});
};
}
diff --git a/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts b/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts
new file mode 100644
index 000000000..ba6740685
--- /dev/null
+++ b/packages/server/src/services/Cashflow/UncategorizeCashflowTransaction.ts
@@ -0,0 +1,72 @@
+import { Knex } from 'knex';
+import { Inject, Service } from 'typedi';
+import HasTenancyService from '../Tenancy/TenancyService';
+import UnitOfWork from '../UnitOfWork';
+import events from '@/subscribers/events';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import {
+ ICashflowTransactionUncategorizedPayload,
+ ICashflowTransactionUncategorizingPayload,
+} from '@/interfaces';
+
+@Service()
+export class UncategorizeCashflowTransaction {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ @Inject()
+ private uow: UnitOfWork;
+
+ /**
+ * Uncategorizes the given cashflow transaction.
+ * @param {number} tenantId
+ * @param {number} cashflowTransactionId
+ */
+ public async uncategorize(
+ tenantId: number,
+ uncategorizedTransactionId: number
+ ) {
+ const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
+
+ const oldUncategorizedTransaction =
+ await UncategorizedCashflowTransaction.query()
+ .findById(uncategorizedTransactionId)
+ .throwIfNotFound();
+
+ // Updates the transaction under UOW.
+ return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
+ // Triggers `onTransactionUncategorizing` event.
+ await this.eventPublisher.emitAsync(
+ events.cashflow.onTransactionUncategorizing,
+ {
+ tenantId,
+ trx,
+ } as ICashflowTransactionUncategorizingPayload
+ );
+ // Removes the ref relation with the related transaction.
+ const uncategorizedTransaction =
+ await UncategorizedCashflowTransaction.query(trx).updateAndFetchById(
+ uncategorizedTransactionId,
+ {
+ categorized: false,
+ categorizeRefId: null,
+ categorizeRefType: null,
+ }
+ );
+ // Triggers `onTransactionUncategorized` event.
+ await this.eventPublisher.emitAsync(
+ events.cashflow.onTransactionUncategorized,
+ {
+ tenantId,
+ uncategorizedTransaction,
+ oldUncategorizedTransaction,
+ trx,
+ } as ICashflowTransactionUncategorizedPayload
+ );
+ return uncategorizedTransaction;
+ });
+ }
+}
diff --git a/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts b/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts
new file mode 100644
index 000000000..85d1a1fbb
--- /dev/null
+++ b/packages/server/src/services/Cashflow/UncategorizedTransactionTransformer.ts
@@ -0,0 +1,65 @@
+import { Transformer } from '@/lib/Transformer/Transformer';
+import { formatNumber } from '@/utils';
+
+export class UncategorizedTransactionTransformer extends Transformer {
+ /**
+ * Include these attributes to sale invoice object.
+ * @returns {string[]}
+ */
+ public includeAttributes = (): string[] => {
+ return [
+ 'formattedAmount',
+ 'formattedDate',
+ 'formattetDepositAmount',
+ 'formattedWithdrawalAmount',
+ ];
+ };
+
+ /**
+ * Formattes the transaction date.
+ * @param transaction
+ * @returns {string}
+ */
+ public formattedDate(transaction) {
+ return this.formatDate(transaction.date);
+ }
+
+ /**
+ * Formatted amount.
+ * @param transaction
+ * @returns {string}
+ */
+ public formattedAmount(transaction) {
+ return formatNumber(transaction.amount, {
+ currencyCode: transaction.currencyCode,
+ });
+ }
+
+ /**
+ * Formatted deposit amount.
+ * @param transaction
+ * @returns {string}
+ */
+ protected formattetDepositAmount(transaction) {
+ if (transaction.isDepositTransaction) {
+ return formatNumber(transaction.deposit, {
+ currencyCode: transaction.currencyCode,
+ });
+ }
+ return '';
+ }
+
+ /**
+ * Formatted withdrawal amount.
+ * @param transaction
+ * @returns {string}
+ */
+ protected formattedWithdrawalAmount(transaction) {
+ if (transaction.isWithdrawalTransaction) {
+ return formatNumber(transaction.withdrawal, {
+ currencyCode: transaction.currencyCode,
+ });
+ }
+ return '';
+ }
+}
diff --git a/packages/server/src/services/Cashflow/constants.ts b/packages/server/src/services/Cashflow/constants.ts
index 2e664a519..bf448a549 100644
--- a/packages/server/src/services/Cashflow/constants.ts
+++ b/packages/server/src/services/Cashflow/constants.ts
@@ -8,7 +8,11 @@ export const ERRORS = {
CREDIT_ACCOUNTS_IDS_NOT_FOUND: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND',
CREDIT_ACCOUNTS_HAS_INVALID_TYPE: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE',
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
- ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions'
+ ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
+ TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
+ TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED',
+ UNCATEGORIZED_TRANSACTION_TYPE_INVALID: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
+ CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED'
};
export enum CASHFLOW_DIRECTION {
diff --git a/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts b/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts
new file mode 100644
index 000000000..9460cc739
--- /dev/null
+++ b/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts
@@ -0,0 +1,42 @@
+import { Inject, Service } from 'typedi';
+import events from '@/subscribers/events';
+import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
+import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+
+@Service()
+export class DeleteCashflowTransactionOnUncategorize {
+ @Inject()
+ private deleteCashflowTransactionService: DeleteCashflowTransaction;
+
+ /**
+ * Attaches events with handlers.
+ */
+ public attach = (bus) => {
+ bus.subscribe(
+ events.cashflow.onTransactionUncategorized,
+ this.deleteCashflowTransactionOnUncategorize.bind(this)
+ );
+ };
+
+ /**
+ * Deletes the cashflow transaction on uncategorize transaction.
+ * @param {ICashflowTransactionUncategorizedPayload} payload
+ */
+ public async deleteCashflowTransactionOnUncategorize({
+ tenantId,
+ oldUncategorizedTransaction,
+ trx,
+ }: ICashflowTransactionUncategorizedPayload) {
+ // Deletes the cashflow transaction.
+ if (
+ oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction'
+ ) {
+ await this.deleteCashflowTransactionService.deleteCashflowTransaction(
+ tenantId,
+
+ oldUncategorizedTransaction.categorizeRefId
+ );
+ }
+ }
+}
diff --git a/packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts b/packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts
new file mode 100644
index 000000000..e45404461
--- /dev/null
+++ b/packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts
@@ -0,0 +1,52 @@
+import { Inject, Service } from 'typedi';
+import events from '@/subscribers/events';
+import { ICommandCashflowDeletingPayload } from '@/interfaces';
+import { ServiceError } from '@/exceptions';
+import { ERRORS } from '../constants';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+
+@Service()
+export class PreventDeleteTransactionOnDelete {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ /**
+ * Attaches events with handlers.
+ */
+ public attach = (bus) => {
+ bus.subscribe(
+ events.cashflow.onTransactionDeleting,
+ this.preventDeleteCashflowTransactionHasUncategorizedTransaction.bind(
+ this
+ )
+ );
+ };
+
+ /**
+ * Prevent delete cashflow transaction has converted from uncategorized transaction.
+ * @param {ICommandCashflowDeletingPayload} payload
+ */
+ public async preventDeleteCashflowTransactionHasUncategorizedTransaction({
+ tenantId,
+ oldCashflowTransaction,
+ trx,
+ }: ICommandCashflowDeletingPayload) {
+ const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
+ if (oldCashflowTransaction.uncategorizedTransactionId) {
+ const foundTransactions = await UncategorizedCashflowTransaction.query(
+ trx
+ ).where({
+ categorized: true,
+ categorizeRefId: oldCashflowTransaction.id,
+ categorizeRefType: 'CashflowTransaction',
+ });
+ // Throw the error if the cashflow transaction still linked to uncategorized transaction.
+ if (foundTransactions.length > 0) {
+ throw new ServiceError(
+ ERRORS.CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED,
+ 'Cannot delete cashflow transaction converted from uncategorized transaction.'
+ );
+ }
+ }
+ }
+}
diff --git a/packages/server/src/services/Cashflow/utils.ts b/packages/server/src/services/Cashflow/utils.ts
index 31af4b1bc..ce95e8416 100644
--- a/packages/server/src/services/Cashflow/utils.ts
+++ b/packages/server/src/services/Cashflow/utils.ts
@@ -1,13 +1,19 @@
-import { upperFirst, camelCase } from 'lodash';
+import { upperFirst, camelCase, omit } from 'lodash';
import {
CASHFLOW_TRANSACTION_TYPE,
CASHFLOW_TRANSACTION_TYPE_META,
ICashflowTransactionTypeMeta,
} from './constants';
+import {
+ ICashflowNewCommandDTO,
+ ICashflowTransaction,
+ ICategorizeCashflowTransactioDTO,
+ IUncategorizedCashflowTransaction,
+} from '@/interfaces';
/**
* Ensures the given transaction type to transformed to appropriate format.
- * @param {string} type
+ * @param {string} type
* @returns {string}
*/
export const transformCashflowTransactionType = (type) => {
@@ -32,3 +38,30 @@ export function getCashflowTransactionType(
export const getCashflowAccountTransactionsTypes = () => {
return Object.values(CASHFLOW_TRANSACTION_TYPE_META).map((meta) => meta.type);
};
+
+/**
+ * Tranasformes the given uncategorized transaction and categorized DTO
+ * to cashflow create DTO.
+ * @param {IUncategorizedCashflowTransaction} uncategorizeModel
+ * @param {ICategorizeCashflowTransactioDTO} categorizeDTO
+ * @returns {ICashflowNewCommandDTO}
+ */
+export const transformCategorizeTransToCashflow = (
+ uncategorizeModel: IUncategorizedCashflowTransaction,
+ categorizeDTO: ICategorizeCashflowTransactioDTO
+): ICashflowNewCommandDTO => {
+ return {
+ date: uncategorizeModel.date,
+ referenceNo: categorizeDTO.referenceNo || uncategorizeModel.referenceNo,
+ description: categorizeDTO.description || uncategorizeModel.description,
+ cashflowAccountId: uncategorizeModel.accountId,
+ creditAccountId: categorizeDTO.creditAccountId,
+ exchangeRate: categorizeDTO.exchangeRate || 1,
+ currencyCode: uncategorizeModel.currencyCode,
+ amount: uncategorizeModel.amount,
+ transactionNumber: categorizeDTO.transactionNumber,
+ transactionType: categorizeDTO.transactionType,
+ uncategorizedTransactionId: uncategorizeModel.id,
+ publish: true,
+ };
+};
diff --git a/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts b/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts
index f532c2eab..6ed80a6f0 100644
--- a/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts
+++ b/packages/server/src/services/CreditNotes/CreditNoteTransformer.ts
@@ -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
diff --git a/packages/server/src/services/CreditNotes/EditCreditNote.ts b/packages/server/src/services/CreditNotes/EditCreditNote.ts
index 074115c04..0e045227d 100644
--- a/packages/server/src/services/CreditNotes/EditCreditNote.ts
+++ b/packages/server/src/services/CreditNotes/EditCreditNote.ts
@@ -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,
});
diff --git a/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts b/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts
index c34dfd330..c54c8096e 100644
--- a/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts
+++ b/packages/server/src/services/CreditNotes/GetCreditNotePdf.ts
@@ -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',
diff --git a/packages/server/src/services/ExchangeRates/ExchangeRateApplication.ts b/packages/server/src/services/ExchangeRates/ExchangeRateApplication.ts
new file mode 100644
index 000000000..9e51c5d72
--- /dev/null
+++ b/packages/server/src/services/ExchangeRates/ExchangeRateApplication.ts
@@ -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}
+ */
+ public latest(
+ tenantId: number,
+ exchangeRateLatestDTO: ExchangeRateLatestDTO
+ ): Promise {
+ return this.exchangeRateService.latest(tenantId, exchangeRateLatestDTO);
+ }
+}
diff --git a/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts b/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts
index 9bc63fbfd..4cde544ad 100644
--- a/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts
+++ b/packages/server/src/services/ExchangeRates/ExchangeRatesService.ts
@@ -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}
+ * @param {number} exchangeRateLatestDTO
+ * @returns {EchangeRateLatestPOJO}
*/
- public async newExchangeRate(
+ public async latest(
tenantId: number,
- exchangeRateDTO: IExchangeRateDTO
- ): Promise {
- const { ExchangeRate } = this.tenancy.models(tenantId);
+ exchangeRateLatestDTO: ExchangeRateLatestDTO
+ ): Promise {
+ 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 {
- 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 {
- 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 {
- 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}
- */
- private async validateExchangeRatePeriodExistance(
- tenantId: number,
- exchangeRateDTO: IExchangeRateDTO
- ): Promise {
- 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}
- */
- 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,
+ };
}
}
diff --git a/packages/server/src/services/Expenses/CRUD/EditExpense.ts b/packages/server/src/services/Expenses/CRUD/EditExpense.ts
index 93b0acc62..e3aeb06ce 100644
--- a/packages/server/src/services/Expenses/CRUD/EditExpense.ts
+++ b/packages/server/src/services/Expenses/CRUD/EditExpense.ts
@@ -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,
});
diff --git a/packages/server/src/services/Expenses/CRUD/ExpenseCategoryTransformer.ts b/packages/server/src/services/Expenses/CRUD/ExpenseCategoryTransformer.ts
new file mode 100644
index 000000000..3f8383c03
--- /dev/null
+++ b/packages/server/src/services/Expenses/CRUD/ExpenseCategoryTransformer.ts
@@ -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,
+ });
+ }
+}
diff --git a/packages/server/src/services/Expenses/CRUD/ExpenseTransformer.ts b/packages/server/src/services/Expenses/CRUD/ExpenseTransformer.ts
index 2812a9261..89f461934 100644
--- a/packages/server/src/services/Expenses/CRUD/ExpenseTransformer.ts
+++ b/packages/server/src/services/Expenses/CRUD/ExpenseTransformer.ts
@@ -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,
+ });
+ };
}
diff --git a/packages/server/src/services/Expenses/ExpenseGLEntriesStorage.ts b/packages/server/src/services/Expenses/ExpenseGLEntriesStorage.ts
index 0821c3bd4..76b450c39 100644
--- a/packages/server/src/services/Expenses/ExpenseGLEntriesStorage.ts
+++ b/packages/server/src/services/Expenses/ExpenseGLEntriesStorage.ts
@@ -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()
diff --git a/packages/server/src/services/Expenses/ExpenseGLEntriesSubscriber.ts b/packages/server/src/services/Expenses/ExpenseGLEntriesSubscriber.ts
index 1a177876e..4c69b7f04 100644
--- a/packages/server/src/services/Expenses/ExpenseGLEntriesSubscriber.ts
+++ b/packages/server/src/services/Expenses/ExpenseGLEntriesSubscriber.ts
@@ -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
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryApplication.ts b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryApplication.ts
index 73c08b2d8..52e6db251 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryApplication.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryApplication.ts
@@ -3,6 +3,7 @@ import { APAgingSummaryExportInjectable } from './APAgingSummaryExportInjectable
import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
import { IAPAgingSummaryQuery } from '@/interfaces';
import { APAgingSummaryService } from './APAgingSummaryService';
+import { APAgingSummaryPdfInjectable } from './APAgingSummaryPdfInjectable';
@Service()
export class APAgingSummaryApplication {
@@ -15,6 +16,9 @@ export class APAgingSummaryApplication {
@Inject()
private APAgingSummarySheet: APAgingSummaryService;
+ @Inject()
+ private APAgingSumaryPdf: APAgingSummaryPdfInjectable;
+
/**
* Retrieve the A/P aging summary in sheet format.
* @param {number} tenantId
@@ -50,4 +54,14 @@ export class APAgingSummaryApplication {
public xlsx(tenantId: number, query: IAPAgingSummaryQuery) {
return this.APAgingSummaryExport.xlsx(tenantId, query);
}
+
+ /**
+ * Retrieves the A/P aging summary in pdf format.
+ * @param {number} tenantId
+ * @param {IAPAgingSummaryQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IAPAgingSummaryQuery) {
+ return this.APAgingSumaryPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryMeta.ts b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryMeta.ts
new file mode 100644
index 000000000..a32fe6457
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryMeta.ts
@@ -0,0 +1,26 @@
+import { Inject, Service } from 'typedi';
+import { IAgingSummaryMeta, IAgingSummaryQuery } from '@/interfaces';
+import { AgingSummaryMeta } from './AgingSummaryMeta';
+
+@Service()
+export class APAgingSummaryMeta {
+ @Inject()
+ private agingSummaryMeta: AgingSummaryMeta;
+
+ /**
+ * Retrieve the aging summary meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IAgingSummaryQuery
+ ): Promise {
+ const commonMeta = await this.agingSummaryMeta.meta(tenantId, query);
+
+ return {
+ ...commonMeta,
+ sheetName: 'A/P Aging Summary',
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryPdfInjectable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryPdfInjectable.ts
new file mode 100644
index 000000000..50ef588e8
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryPdfInjectable.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { IAPAgingSummaryQuery } from '@/interfaces';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { APAgingSummaryTableInjectable } from './APAgingSummaryTableInjectable';
+import { HtmlTableCss } from './_constants';
+
+@Service()
+export class APAgingSummaryPdfInjectable {
+ @Inject()
+ private APAgingSummaryTable: APAgingSummaryTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given A/P aging summary sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IAPAgingSummaryQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IAPAgingSummaryQuery
+ ): Promise {
+ const table = await this.APAgingSummaryTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedAsDate,
+ HtmlTableCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts
index fa3e6a2b3..49ee46448 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/APAgingSummaryService.ts
@@ -1,18 +1,19 @@
import moment from 'moment';
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
-import { IAPAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces';
+import { IAPAgingSummaryQuery, IAPAgingSummarySheet } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import APAgingSummarySheet from './APAgingSummarySheet';
import { Tenant } from '@/system/models';
+import { APAgingSummaryMeta } from './APAgingSummaryMeta';
@Service()
export class APAgingSummaryService {
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
- @Inject('logger')
- logger: any;
+ @Inject()
+ private APAgingSummaryMeta: APAgingSummaryMeta;
/**
* Default report query.
@@ -35,35 +36,16 @@ export class APAgingSummaryService {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- reportMetadata(tenantId: number): IARAgingSummaryMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve A/P aging summary report.
* @param {number} tenantId -
* @param {IAPAgingSummaryQuery} query -
+ * @returns {Promise}
*/
- async APAgingSummary(tenantId: number, query: IAPAgingSummaryQuery) {
+ public async APAgingSummary(
+ tenantId: number,
+ query: IAPAgingSummaryQuery
+ ): Promise {
const { Bill } = this.tenancy.models(tenantId);
const { vendorRepository } = this.tenancy.repositories(tenantId);
@@ -111,11 +93,14 @@ export class APAgingSummaryService {
const data = APAgingSummaryReport.reportData();
const columns = APAgingSummaryReport.reportColumns();
+ // Retrieve the aging summary report meta.
+ const meta = await this.APAgingSummaryMeta.meta(tenantId, filter);
+
return {
data,
columns,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryApplication.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryApplication.ts
index d3282ca4b..f24932f13 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryApplication.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryApplication.ts
@@ -3,6 +3,7 @@ import { IARAgingSummaryQuery } from '@/interfaces';
import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
import { ARAgingSummaryExportInjectable } from './ARAgingSummaryExportInjectable';
import ARAgingSummaryService from './ARAgingSummaryService';
+import { ARAgingSummaryPdfInjectable } from './ARAgingSummaryPdfInjectable';
@Service()
export class ARAgingSummaryApplication {
@@ -15,6 +16,9 @@ export class ARAgingSummaryApplication {
@Inject()
private ARAgingSummarySheet: ARAgingSummaryService;
+ @Inject()
+ private ARAgingSummaryPdf: ARAgingSummaryPdfInjectable;
+
/**
* Retrieve the A/R aging summary sheet.
* @param {number} tenantId
@@ -50,4 +54,14 @@ export class ARAgingSummaryApplication {
public csv(tenantId: number, query: IARAgingSummaryQuery) {
return this.ARAgingSummaryExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the A/R aging summary in pdf format.
+ * @param {number} tenantId
+ * @param {IARAgingSummaryQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IARAgingSummaryQuery) {
+ return this.ARAgingSummaryPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryMeta.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryMeta.ts
new file mode 100644
index 000000000..dce2cb9d0
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryMeta.ts
@@ -0,0 +1,26 @@
+import { Inject, Service } from 'typedi';
+import { IAgingSummaryMeta, IAgingSummaryQuery } from '@/interfaces';
+import { AgingSummaryMeta } from './AgingSummaryMeta';
+
+@Service()
+export class ARAgingSummaryMeta {
+ @Inject()
+ private agingSummaryMeta: AgingSummaryMeta;
+
+ /**
+ * Retrieve the aging summary meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IAgingSummaryQuery
+ ): Promise {
+ const commonMeta = await this.agingSummaryMeta.meta(tenantId, query);
+
+ return {
+ ...commonMeta,
+ sheetName: 'A/R Aging Summary',
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryPdfInjectable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryPdfInjectable.ts
new file mode 100644
index 000000000..a3b75f6d3
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryPdfInjectable.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { IARAgingSummaryQuery } from '@/interfaces';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { ARAgingSummaryTableInjectable } from './ARAgingSummaryTableInjectable';
+import { HtmlTableCss } from './_constants';
+
+@Service()
+export class ARAgingSummaryPdfInjectable {
+ @Inject()
+ private ARAgingSummaryTable: ARAgingSummaryTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given balance sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IARAgingSummaryQuery
+ ): Promise {
+ const table = await this.ARAgingSummaryTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts
index e13dfc276..d52709027 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryService.ts
@@ -1,18 +1,19 @@
import moment from 'moment';
import { Inject, Service } from 'typedi';
import { isEmpty } from 'lodash';
-import { IARAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces';
+import { IARAgingSummaryQuery } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import ARAgingSummarySheet from './ARAgingSummarySheet';
import { Tenant } from '@/system/models';
+import { ARAgingSummaryMeta } from './ARAgingSummaryMeta';
@Service()
export default class ARAgingSummaryService {
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
- @Inject('logger')
- logger: any;
+ @Inject()
+ private ARAgingSummaryMeta: ARAgingSummaryMeta;
/**
* Default report query.
@@ -35,29 +36,6 @@ export default class ARAgingSummaryService {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- reportMetadata(tenantId: number): IARAgingSummaryMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve A/R aging summary report.
* @param {number} tenantId - Tenant id.
@@ -110,11 +88,14 @@ export default class ARAgingSummaryService {
const data = ARAgingSummaryReport.reportData();
const columns = ARAgingSummaryReport.reportColumns();
+ // Retrieve the aging summary report meta.
+ const meta = await this.ARAgingSummaryMeta.meta(tenantId, filter);
+
return {
data,
columns,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTableInjectable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTableInjectable.ts
index 10ac9ee8c..6f702cfe4 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTableInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/ARAgingSummaryTableInjectable.ts
@@ -19,6 +19,7 @@ export class ARAgingSummaryTableInjectable {
query: IARAgingSummaryQuery
): Promise {
const report = await this.ARAgingSummarySheet.ARAgingSummary(
+
tenantId,
query
);
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryMeta.ts b/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryMeta.ts
new file mode 100644
index 000000000..5d3599997
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryMeta.ts
@@ -0,0 +1,30 @@
+import { Inject } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import { IAgingSummaryMeta, IAgingSummaryQuery } from '@/interfaces';
+import moment from 'moment';
+
+export class AgingSummaryMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the aging summary meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IAgingSummaryQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
+ const formattedDateRange = `As ${formattedAsDate}`;
+
+ return {
+ ...commonMeta,
+ sheetName: 'A/P Aging Summary',
+ formattedAsDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts b/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts
index 3b74c76ac..0b6c61cbd 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/AgingSummaryTable.ts
@@ -56,7 +56,7 @@ export default abstract class AgingSummaryTable extends R.compose(
node: IAgingSummaryContact | IAgingSummaryTotal
): ITableColumnAccessor[] => {
return node.aging.map((aging, index) => ({
- key: 'aging',
+ key: 'aging_period',
accessor: `aging[${index}].total.formattedAmount`,
}));
};
diff --git a/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts b/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts
index 961f0b7ed..fd818e160 100644
--- a/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts
+++ b/packages/server/src/services/FinancialStatements/AgingSummary/_constants.ts
@@ -2,3 +2,20 @@ export enum AgingSummaryRowType {
Contact = 'contact',
Total = 'total',
}
+
+export const HtmlTableCss = `
+table tr.row-type--total td{
+ font-weight: 600;
+ border-top: 1px solid #bbb;
+ border-bottom: 3px double #333;
+}
+
+table .column--current,
+table .column--aging_period,
+table .column--total,
+table .cell--current,
+table .cell--aging_period,
+table .cell--total {
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetApplication.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetApplication.ts
index 01ab77bfe..0c965a5f5 100644
--- a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetApplication.ts
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetApplication.ts
@@ -54,4 +54,14 @@ export class BalanceSheetApplication {
public csv(tenantId: number, query: IBalanceSheetQuery): Promise {
return this.balanceSheetExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the balance sheet in pdf format.
+ * @param {number} tenantId
+ * @param {IBalanceSheetQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IBalanceSheetQuery) {
+ return this.balanceSheetExport.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetExportInjectable.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetExportInjectable.ts
index 2c43d5f80..9198d8536 100644
--- a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetExportInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetExportInjectable.ts
@@ -2,12 +2,16 @@ import { Inject, Service } from 'typedi';
import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { IBalanceSheetQuery } from '@/interfaces';
+import { BalanceSheetPdfInjectable } from './BalanceSheetPdfInjectable';
@Service()
export class BalanceSheetExportInjectable {
@Inject()
private balanceSheetTable: BalanceSheetTableInjectable;
+ @Inject()
+ private balanceSheetPdf: BalanceSheetPdfInjectable;
+
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
@@ -40,4 +44,17 @@ export class BalanceSheetExportInjectable {
return tableCsv;
}
+
+ /**
+ * Retrieves the balance sheet in pdf format.
+ * @param {number} tenantId
+ * @param {IBalanceSheetQuery} query
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IBalanceSheetQuery
+ ): Promise {
+ return this.balanceSheetPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetInjectable.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetInjectable.ts
index 02e136ca1..5e171110b 100644
--- a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetInjectable.ts
@@ -4,15 +4,12 @@ import {
IBalanceSheetStatementService,
IBalanceSheetQuery,
IBalanceSheetStatement,
- IBalanceSheetMeta,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
-import Journal from '@/services/Accounting/JournalPoster';
import BalanceSheetStatement from './BalanceSheet';
-import InventoryService from '@/services/Inventory/Inventory';
-import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import BalanceSheetRepository from './BalanceSheetRepository';
+import { BalanceSheetMetaInjectable } from './BalanceSheetMeta';
@Service()
export default class BalanceSheetStatementService
@@ -22,7 +19,7 @@ export default class BalanceSheetStatementService
private tenancy: TenancyService;
@Inject()
- private inventoryService: InventoryService;
+ private balanceSheetMeta: BalanceSheetMetaInjectable;
/**
* Defaults balance sheet filter query.
@@ -62,33 +59,6 @@ export default class BalanceSheetStatementService
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- private reportMetadata(tenantId: number): IBalanceSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning =
- this.inventoryService.isItemsCostComputeRunning(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve balance sheet statement.
* @param {number} tenantId
@@ -112,6 +82,7 @@ export default class BalanceSheetStatementService
const models = this.tenancy.models(tenantId);
const balanceSheetRepo = new BalanceSheetRepository(models, filter);
+ // Loads all resources.
await balanceSheetRepo.asyncInitialize();
// Balance sheet report instance.
@@ -122,12 +93,15 @@ export default class BalanceSheetStatementService
i18n
);
// Balance sheet data.
- const balanceSheetData = balanceSheetInstanace.reportData();
+ const data = balanceSheetInstanace.reportData();
+
+ // Balance sheet meta.
+ const meta = await this.balanceSheetMeta.meta(tenantId, filter);
return {
- data: balanceSheetData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ data,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetMeta.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetMeta.ts
new file mode 100644
index 000000000..c7a3e87c0
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetMeta.ts
@@ -0,0 +1,32 @@
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import { IBalanceSheetMeta, IBalanceSheetQuery } from '@/interfaces';
+import moment from 'moment';
+
+@Service()
+export class BalanceSheetMetaInjectable {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the balance sheet meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IBalanceSheetQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedAsDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedDateRange = `As ${formattedAsDate}`;
+ const sheetName = 'Balance Sheet Statement';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedAsDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetPdfInjectable.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetPdfInjectable.ts
new file mode 100644
index 000000000..4cbb43e8a
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetPdfInjectable.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { IBalanceSheetQuery } from '@/interfaces';
+import { BalanceSheetTableInjectable } from './BalanceSheetTableInjectable';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class BalanceSheetPdfInjectable {
+ @Inject()
+ private balanceSheetTable: BalanceSheetTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given balance sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IBalanceSheetQuery
+ ): Promise {
+ const table = await this.balanceSheetTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetTable.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetTable.ts
index 349b1a8f4..8e6a1f92b 100644
--- a/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetTable.ts
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetTable.ts
@@ -41,15 +41,16 @@ export default class BalanceSheetTable extends R.compose(
BalanceSheetBase
)(FinancialSheet) {
/**
- * @param {}
+ * Balance sheet data.
+ * @param {IBalanceSheetStatementData}
*/
- reportData: IBalanceSheetStatementData;
+ private reportData: IBalanceSheetStatementData;
/**
* Balance sheet query.
- * @parma {}
+ * @parma {BalanceSheetQuery}
*/
- query: BalanceSheetQuery;
+ private query: BalanceSheetQuery;
/**
* Constructor method.
@@ -215,13 +216,13 @@ export default class BalanceSheetTable extends R.compose(
/**
* Retrieves the total children columns.
- * @returns {ITableColumn[]}
+ * @returns {ITableColumn[]}
*/
private totalColumnChildren = (): ITableColumn[] => {
return R.compose(
R.unless(
R.isEmpty,
- R.concat([{ key: 'total', Label: this.i18n.__('balance_sheet.total') }])
+ R.concat([{ key: 'total', label: this.i18n.__('balance_sheet.total') }])
),
R.concat(this.percentageColumns()),
R.concat(this.getPreviousYearColumns()),
diff --git a/packages/server/src/services/FinancialStatements/BalanceSheet/constants.ts b/packages/server/src/services/FinancialStatements/BalanceSheet/constants.ts
index 8862408a1..2081d2859 100644
--- a/packages/server/src/services/FinancialStatements/BalanceSheet/constants.ts
+++ b/packages/server/src/services/FinancialStatements/BalanceSheet/constants.ts
@@ -12,3 +12,53 @@ export enum IROW_TYPE {
NET_INCOME = 'NET_INCOME',
TOTAL = 'TOTAL',
}
+
+export const HtmlTableCustomCss = `
+table tr.row-type--total td {
+ font-weight: 600;
+ border-top: 1px solid #bbb;
+ color: #000;
+}
+table tr.row-type--total.row-id--assets td,
+table tr.row-type--total.row-id--liability-equity td {
+ border-bottom: 3px double #000;
+}
+table .column--name,
+table .cell--name {
+ width: 400px;
+}
+
+table .column--total {
+ width: 25%;
+}
+
+table td.cell--total,
+table td.cell--previous_year,
+table td.cell--previous_year_change,
+table td.cell--previous_year_percentage,
+
+table td.cell--previous_period,
+table td.cell--previous_period_change,
+table td.cell--previous_period_percentage,
+
+table td.cell--percentage_of_row,
+table td.cell--percentage_of_column,
+table td[class*="cell--date-range"] {
+ text-align: right;
+}
+
+table .column--total,
+table .column--previous_year,
+table .column--previous_year_change,
+table .column--previous_year_percentage,
+
+table .column--previous_period,
+table .column--previous_period_change,
+table .column--previous_period_percentage,
+
+table .column--percentage_of_row,
+table .column--percentage_of_column,
+table [class*="column--date-range"] {
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts b/packages/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts
index 09fdf91ca..201a31be4 100644
--- a/packages/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts
+++ b/packages/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts
@@ -8,7 +8,7 @@ import {
ICashFlowStatementQuery,
ICashFlowStatementDOO,
IAccountTransaction,
- ICashFlowStatementMeta
+ ICashFlowStatementMeta,
} from '@/interfaces';
import CashFlowStatement from './CashFlow';
import Ledger from '@/services/Accounting/Ledger';
@@ -16,6 +16,7 @@ import CashFlowRepository from './CashFlowRepository';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
+import { CashflowSheetMeta } from './CashflowSheetMeta';
@Service()
export default class CashFlowStatementService
@@ -31,6 +32,9 @@ export default class CashFlowStatementService
@Inject()
inventoryService: InventoryService;
+ @Inject()
+ private cashflowSheetMeta: CashflowSheetMeta;
+
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
@@ -138,38 +142,13 @@ export default class CashFlowStatementService
tenant.metadata.baseCurrency,
i18n
);
+ // Retrieve the cashflow sheet meta.
+ const meta = await this.cashflowSheetMeta.meta(tenantId, filter);
return {
data: cashFlowInstance.reportData(),
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
-
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {ICashFlowStatementMeta}
- */
- private reportMetadata(tenantId: number): ICashFlowStatementMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning = this.inventoryService
- .isItemsCostComputeRunning(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency
- };
- }
}
diff --git a/packages/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts b/packages/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts
index b3470e1a4..7b49a685c 100644
--- a/packages/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts
+++ b/packages/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts
@@ -76,7 +76,7 @@ export default class CashFlowTable implements ICashFlowTable {
*/
private commonColumns = () => {
return R.compose(
- R.concat([{ key: 'label', accessor: 'label' }]),
+ R.concat([{ key: 'name', accessor: 'label' }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumnsAccessors())
diff --git a/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetApplication.ts b/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetApplication.ts
index 0fd8b7357..72a587f52 100644
--- a/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetApplication.ts
+++ b/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetApplication.ts
@@ -3,6 +3,7 @@ import { CashflowExportInjectable } from './CashflowExportInjectable';
import { ICashFlowStatementQuery } from '@/interfaces';
import CashFlowStatementService from './CashFlowService';
import { CashflowTableInjectable } from './CashflowTableInjectable';
+import { CashflowTablePdfInjectable } from './CashflowTablePdfInjectable';
@Service()
export class CashflowSheetApplication {
@@ -15,6 +16,9 @@ export class CashflowSheetApplication {
@Inject()
private cashflowTable: CashflowTableInjectable;
+ @Inject()
+ private cashflowPdf: CashflowTablePdfInjectable;
+
/**
* Retrieves the cashflow sheet
* @param {number} tenantId
@@ -55,4 +59,17 @@ export class CashflowSheetApplication {
): Promise {
return this.cashflowExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the cashflow sheet in pdf format.
+ * @param {number} tenantId
+ * @param {ICashFlowStatementQuery} query
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ICashFlowStatementQuery
+ ): Promise {
+ return this.cashflowPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetMeta.ts b/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetMeta.ts
new file mode 100644
index 000000000..3a1dd40dc
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/CashFlow/CashflowSheetMeta.ts
@@ -0,0 +1,36 @@
+import { Inject, Service } from 'typedi';
+import moment from 'moment';
+import { ICashFlowStatementMeta, ICashFlowStatementQuery } from '@/interfaces';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+
+@Service()
+export class CashflowSheetMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * CAshflow sheet meta.
+ * @param {number} tenantId
+ * @param {ICashFlowStatementQuery} query
+ * @returns {Promise}
+ */
+ public async meta(
+ tenantId: number,
+ query: ICashFlowStatementQuery
+ ): Promise {
+ const meta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ const sheetName = 'Statement of Cash Flow';
+
+ return {
+ ...meta,
+ sheetName,
+ formattedToDate,
+ formattedFromDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/CashFlow/CashflowTablePdfInjectable.ts b/packages/server/src/services/FinancialStatements/CashFlow/CashflowTablePdfInjectable.ts
new file mode 100644
index 000000000..be7cb5382
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/CashFlow/CashflowTablePdfInjectable.ts
@@ -0,0 +1,34 @@
+import { Inject } from 'typedi';
+import { CashflowTableInjectable } from './CashflowTableInjectable';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { ICashFlowStatementQuery } from '@/interfaces';
+import { HtmlTableCustomCss } from './constants';
+
+export class CashflowTablePdfInjectable {
+ @Inject()
+ private cashflowTable: CashflowTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given cashflow sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ICashFlowStatementQuery
+ ): Promise {
+ const table = await this.cashflowTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/CashFlow/constants.ts b/packages/server/src/services/FinancialStatements/CashFlow/constants.ts
index 58a92ec19..f3f1858fb 100644
--- a/packages/server/src/services/FinancialStatements/CashFlow/constants.ts
+++ b/packages/server/src/services/FinancialStatements/CashFlow/constants.ts
@@ -1,8 +1,33 @@
-
-
export const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
-export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
\ No newline at end of file
+export const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
+export const HtmlTableCustomCss = `
+table tr.row-type--accounts td {
+ border-top: 1px solid #bbb;
+}
+table tr.row-id--cash-end-period td {
+ border-bottom: 3px double #333;
+}
+table tr.row-type--total {
+ font-weight: 600;
+}
+table tr.row-type--total td {
+ color: #000;
+}
+table tr.row-type--total:not(:first-child) td {
+ border-top: 1px solid #bbb;
+}
+table .column--name,
+table .cell--name {
+ width: 400px;
+}
+table .column--total,
+table .cell--total,
+table [class*="column--date-range"],
+table [class*="cell--date-range"] {
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryApplication.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryApplication.ts
index 964cd91a9..78c532d08 100644
--- a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryApplication.ts
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryApplication.ts
@@ -3,6 +3,7 @@ import { CustomerBalanceSummaryExportInjectable } from './CustomerBalanceSummary
import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
import { ICustomerBalanceSummaryQuery } from '@/interfaces';
import { CustomerBalanceSummaryService } from './CustomerBalanceSummaryService';
+import { CustomerBalanceSummaryPdf } from './CustomerBalanceSummaryPdf';
@Service()
export class CustomerBalanceSummaryApplication {
@@ -14,6 +15,9 @@ export class CustomerBalanceSummaryApplication {
@Inject()
private customerBalanceSummarySheet: CustomerBalanceSummaryService;
+
+ @Inject()
+ private customerBalanceSummaryPdf: CustomerBalanceSummaryPdf;
/**
* Retrieves the customer balance sheet in json format.
@@ -57,4 +61,14 @@ export class CustomerBalanceSummaryApplication {
public csv(tenantId: number, query: ICustomerBalanceSummaryQuery) {
return this.customerBalanceSummaryExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the customer balance sheet in PDF format.
+ * @param {number} tenantId
+ * @param {ICustomerBalanceSummaryQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: ICustomerBalanceSummaryQuery) {
+ return this.customerBalanceSummaryPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryMeta.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryMeta.ts
new file mode 100644
index 000000000..7639e8c4f
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryMeta.ts
@@ -0,0 +1,35 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import {
+ ICustomerBalanceSummaryMeta,
+ ICustomerBalanceSummaryQuery,
+} from '@/interfaces';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+
+@Service()
+export class CustomerBalanceSummaryMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the customer balance summary meta.
+ * @param {number} tenantId
+ * @param {ICustomerBalanceSummaryQuery} query
+ * @returns {Promise}
+ */
+ async meta(
+ tenantId: number,
+ query: ICustomerBalanceSummaryQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
+ const formattedDateRange = `As ${formattedAsDate}`;
+
+ return {
+ ...commonMeta,
+ sheetName: 'Customer Balance Summary',
+ formattedAsDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryPdf.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryPdf.ts
new file mode 100644
index 000000000..38d36fd48
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryPdf.ts
@@ -0,0 +1,36 @@
+import { Inject, Service } from 'typedi';
+import { ICustomerBalanceSummaryQuery } from '@/interfaces';
+
+import { TableSheetPdf } from '../TableSheetPdf';
+import { CustomerBalanceSummaryTableInjectable } from './CustomerBalanceSummaryTableInjectable';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class CustomerBalanceSummaryPdf {
+ @Inject()
+ private customerBalanceSummaryTable: CustomerBalanceSummaryTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given customer balance summary sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IAPAgingSummaryQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ICustomerBalanceSummaryQuery
+ ): Promise {
+ const table = await this.customerBalanceSummaryTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts
index 78afc3bb2..38edc5174 100644
--- a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts
@@ -1,4 +1,4 @@
-import { Inject } from 'typedi';
+import { Inject, Service } from 'typedi';
import moment from 'moment';
import * as R from 'ramda';
import {
@@ -12,13 +12,18 @@ import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
import Ledger from '@/services/Accounting/Ledger';
import CustomerBalanceSummaryRepository from './CustomerBalanceSummaryRepository';
import { Tenant } from '@/system/models';
+import { CustomerBalanceSummaryMeta } from './CustomerBalanceSummaryMeta';
+@Service()
export class CustomerBalanceSummaryService
implements ICustomerBalanceSummaryService
{
@Inject()
private reportRepository: CustomerBalanceSummaryRepository;
+ @Inject()
+ private customerBalanceSummaryMeta: CustomerBalanceSummaryMeta;
+
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
@@ -96,10 +101,13 @@ export class CustomerBalanceSummaryService
filter,
tenant.metadata.baseCurrency
);
+ // Retrieve the customer balance summary meta.
+ const meta = await this.customerBalanceSummaryMeta.meta(tenantId, filter);
return {
data: report.reportData(),
query: filter,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableInjectable.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableInjectable.ts
index 56450d4ca..ec5a9c021 100644
--- a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableInjectable.ts
@@ -26,19 +26,20 @@ export class CustomerBalanceSummaryTableInjectable {
filter: ICustomerBalanceSummaryQuery
): Promise {
const i18n = this.tenancy.i18n(tenantId);
- const { data, query } =
+ const { data, query, meta } =
await this.customerBalanceSummaryService.customerBalanceSummary(
tenantId,
filter
);
- const tableRows = new CustomerBalanceSummaryTable(data, filter, i18n);
+ const table = new CustomerBalanceSummaryTable(data, filter, i18n);
return {
table: {
- columns: tableRows.tableColumns(),
- rows: tableRows.tableRows(),
+ columns: table.tableColumns(),
+ rows: table.tableRows(),
},
query,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts
index e6cdc6415..e4ed687a4 100644
--- a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows.ts
@@ -52,7 +52,7 @@ export class CustomerBalanceSummaryTable {
*/
private getCustomerColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
- { key: 'customerName', accessor: 'customerName' },
+ { key: 'name', accessor: 'customerName' },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
@@ -85,7 +85,7 @@ export class CustomerBalanceSummaryTable {
*/
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
- { key: 'total', value: this.i18n.__('Total') },
+ { key: 'name', value: this.i18n.__('Total') },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
diff --git a/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/constants.ts b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/constants.ts
new file mode 100644
index 000000000..513a3dcdb
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/CustomerBalanceSummary/constants.ts
@@ -0,0 +1,14 @@
+export const HtmlTableCustomCss = `
+table tr.row-type--total td {
+ font-weight: 600;
+ border-top: 1px solid #bbb;
+ border-bottom: 3px double #333;
+}
+table .column--name {
+ width: 65%;
+}
+table .column--total,
+table .cell--total {
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/FinancialSheetMeta.ts b/packages/server/src/services/FinancialStatements/FinancialSheetMeta.ts
new file mode 100644
index 000000000..130f3caaa
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/FinancialSheetMeta.ts
@@ -0,0 +1,34 @@
+import { TenantMetadata } from '@/system/models';
+import { Inject, Service } from 'typedi';
+import InventoryService from '../Inventory/Inventory';
+import { IFinancialSheetCommonMeta } from '@/interfaces';
+
+@Service()
+export class FinancialSheetMeta {
+ @Inject()
+ private inventoryService: InventoryService;
+
+ /**
+ * Retrieves the common meta data of the financial sheet.
+ * @param {number} tenantId
+ * @returns {Promise}
+ */
+ async meta(tenantId: number): Promise {
+ const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
+
+ const organizationName = tenantMetadata.name;
+ const baseCurrency = tenantMetadata.baseCurrency;
+ const dateFormat = tenantMetadata.dateFormat;
+
+ const isCostComputeRunning =
+ this.inventoryService.isItemsCostComputeRunning(tenantId);
+
+ return {
+ organizationName,
+ baseCurrency,
+ dateFormat,
+ isCostComputeRunning,
+ sheetName: '',
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/FinancialSheetStructure.ts b/packages/server/src/services/FinancialStatements/FinancialSheetStructure.ts
index 722635d9a..5e2fbba55 100644
--- a/packages/server/src/services/FinancialStatements/FinancialSheetStructure.ts
+++ b/packages/server/src/services/FinancialStatements/FinancialSheetStructure.ts
@@ -61,14 +61,14 @@ export const FinancialSheetStructure = (Base: Class) =>
});
};
- findNodeDeep = (nodes, callback) => {
+ public findNodeDeep = (nodes, callback) => {
return findValueDeep(nodes, callback, {
childrenPath: 'children',
pathFormat: 'array',
});
};
- mapAccNodesDeep = (nodes, callback) => {
+ public mapAccNodesDeep = (nodes, callback) => {
return reduceDeep(
nodes,
(acc, value, key, parentValue, context) => {
@@ -97,11 +97,11 @@ export const FinancialSheetStructure = (Base: Class) =>
});
};
- getTotalOfChildrenNodes = (node) => {
+ public getTotalOfChildrenNodes = (node) => {
return this.getTotalOfNodes(node.children);
};
- getTotalOfNodes = (nodes) => {
+ public getTotalOfNodes = (nodes) => {
return sumBy(nodes, 'total.amount');
};
};
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts
index 512ed37d7..5e12e9078 100644
--- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts
@@ -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),
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts
new file mode 100644
index 000000000..6257e34d8
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication.ts
@@ -0,0 +1,83 @@
+import { Inject } from 'typedi';
+import {
+ IGeneralLedgerSheetQuery,
+ IGeneralLedgerTableData,
+} from '@/interfaces';
+import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
+import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
+import { GeneralLedgerService } from './GeneralLedgerService';
+import { GeneralLedgerPdf } from './GeneralLedgerPdf';
+
+export class GeneralLedgerApplication {
+ @Inject()
+ private GLTable: GeneralLedgerTableInjectable;
+
+ @Inject()
+ private GLExport: GeneralLedgerExportInjectable;
+
+ @Inject()
+ private GLSheet: GeneralLedgerService;
+
+ @Inject()
+ private GLPdf: GeneralLedgerPdf;
+
+ /**
+ * 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}
+ */
+ public table(
+ tenantId: number,
+ query: IGeneralLedgerSheetQuery
+ ): Promise {
+ 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 {
+ 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 {
+ return this.GLExport.csv(tenantId, query);
+ }
+
+ /**
+ * Retrieves the G/L sheet in pdf format.
+ * @param {number} tenantId
+ * @param {IGeneralLedgerSheetQuery} query
+ * @returns {Promise}
+ */
+ public pdf(
+ tenantId: number,
+ query: IGeneralLedgerSheetQuery
+ ): Promise {
+ return this.GLPdf.pdf(tenantId, query);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts
new file mode 100644
index 000000000..f05c817c2
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerExport.ts
@@ -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}
+ */
+ 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}
+ */
+ public async csv(
+ tenantId: number,
+ query: IGeneralLedgerSheetQuery
+ ): Promise {
+ const table = await this.generalLedgerTable.table(tenantId, query);
+
+ const tableSheet = new TableSheet(table.table);
+ const tableCsv = tableSheet.convertToCSV();
+
+ return tableCsv;
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerMeta.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerMeta.ts
new file mode 100644
index 000000000..00f312654
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerMeta.ts
@@ -0,0 +1,33 @@
+import { IGeneralLedgerMeta, IGeneralLedgerSheetQuery } from '@/interfaces';
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+
+@Service()
+export class GeneralLedgerMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the balance sheet meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IGeneralLedgerSheetQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ return {
+ ...commonMeta,
+ sheetName: 'Balance Sheet',
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerPdf.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerPdf.ts
new file mode 100644
index 000000000..efe082709
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerPdf.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
+import { IGeneralLedgerSheetQuery } from '@/interfaces';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class GeneralLedgerPdf {
+ @Inject()
+ private generalLedgerTable: GeneralLedgerTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the general ledger sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IGeneralLedgerSheetQuery} query -
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IGeneralLedgerSheetQuery
+ ): Promise {
+ const table = await this.generalLedgerTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts
index 012a05b94..53451d2d8 100644
--- a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts
@@ -6,24 +6,21 @@ import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger';
-import InventoryService from '@/services/Inventory/Inventory';
-import { transformToMap, parseBoolean } from 'utils';
+import { transformToMap } from 'utils';
import { Tenant } from '@/system/models';
+import { GeneralLedgerMeta } from './GeneralLedgerMeta';
const ERRORS = {
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
};
@Service()
-export default class GeneralLedgerService {
+export class GeneralLedgerService {
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
@Inject()
- inventoryService: InventoryService;
-
- @Inject('logger')
- logger: any;
+ private generalLedgerMeta: GeneralLedgerMeta;
/**
* Defaults general ledger report filter query.
@@ -59,36 +56,8 @@ export default class GeneralLedgerService {
}
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IGeneralLedgerMeta}
- */
- reportMetadata(tenantId: number): IGeneralLedgerMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning = this.inventoryService
- .isItemsCostComputeRunning(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency
- };
- }
-
/**
* Retrieve general ledger report statement.
- * ----------
* @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query
* @return {IGeneralLedgerStatement}
@@ -99,13 +68,10 @@ export default class GeneralLedgerService {
): Promise<{
data: any;
query: IGeneralLedgerSheetQuery;
- meta: IGeneralLedgerMeta
+ meta: IGeneralLedgerMeta;
}> {
- const {
- accountRepository,
- transactionsRepository,
- contactRepository
- } = this.tenancy.repositories(tenantId);
+ const { accountRepository, transactionsRepository, contactRepository } =
+ this.tenancy.repositories(tenantId);
const i18n = this.tenancy.i18n(tenantId);
@@ -129,13 +95,13 @@ export default class GeneralLedgerService {
const transactions = await transactionsRepository.journal({
fromDate: filter.fromDate,
toDate: filter.toDate,
- branchesIds: filter.branchesIds
+ branchesIds: filter.branchesIds,
});
// Retreive opening balance credit/debit sumation.
const openingBalanceTrans = await transactionsRepository.journal({
toDate: moment(filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
- branchesIds: filter.branchesIds
+ branchesIds: filter.branchesIds,
});
// Transform array transactions to journal collection.
const transactionsJournal = Journal.fromTransactions(
@@ -143,7 +109,7 @@ export default class GeneralLedgerService {
tenantId,
accountsGraph
);
- // Accounts opening transactions.
+ // Accounts opening transactions.
const openingTransJournal = Journal.fromTransactions(
openingBalanceTrans,
tenantId,
@@ -163,10 +129,13 @@ export default class GeneralLedgerService {
// Retrieve general ledger report data.
const reportData = generalLedgerInstance.reportData();
+ // Retrieve general ledger report metadata.
+ const meta = await this.generalLedgerMeta.meta(tenantId, filter);
+
return {
data: reportData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts
new file mode 100644
index 000000000..1820ab095
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTable.ts
@@ -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);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts
new file mode 100644
index 000000000..c830c29c2
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerTableInjectable.ts
@@ -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}
+ */
+ public async table(
+ tenantId: number,
+ query: IGeneralLedgerSheetQuery
+ ): Promise {
+ 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,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/constants.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/constants.ts
new file mode 100644
index 000000000..9e79a81da
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/constants.ts
@@ -0,0 +1,29 @@
+export const HtmlTableCustomCss = `
+table tr:last-child td {
+ border-bottom: 1px solid #ececec;
+}
+table tr.row-type--account td,
+table tr.row-type--opening-balance td,
+table tr.row-type--closing-balance td{
+ font-weight: 600;
+}
+table tr.row-type--closing-balance td {
+ border-bottom: 1px solid #ececec;
+}
+
+table .column--debit,
+table .column--credit,
+table .column--amount,
+table .column--running_balance,
+table .cell--debit,
+table .cell--credit,
+table .cell--amount,
+table .cell--running_balance{
+ text-align: right;
+}
+table tr.row-type--account .cell--date span,
+table tr.row-type--opening-balance .cell--account_name span,
+table tr.row-type--closing-balance .cell--account_name span{
+ white-space: nowrap;
+}
+`;
\ No newline at end of file
diff --git a/packages/server/src/services/FinancialStatements/GeneralLedger/utils.ts b/packages/server/src/services/FinancialStatements/GeneralLedger/utils.ts
new file mode 100644
index 000000000..07418ae37
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/GeneralLedger/utils.ts
@@ -0,0 +1,6 @@
+export enum ROW_TYPE {
+ ACCOUNT = 'ACCOUNT',
+ OPENING_BALANCE = 'OPENING_BALANCE',
+ TRANSACTION = 'TRANSACTION',
+ CLOSING_BALANCE = 'CLOSING_BALANCE',
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsApplication.ts b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsApplication.ts
index a2639094e..50cbd2a2f 100644
--- a/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsApplication.ts
+++ b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsApplication.ts
@@ -6,6 +6,7 @@ import { Inject, Service } from 'typedi';
import { InventoryDetailsExportInjectable } from './InventoryDetailsExportInjectable';
import { InventoryDetailsTableInjectable } from './InventoryDetailsTableInjectable';
import { InventoryDetailsService } from './InventoryDetailsService';
+import { InventoryDetailsTablePdf } from './InventoryDetailsTablePdf';
@Service()
export class InventortyDetailsApplication {
@@ -18,6 +19,9 @@ export class InventortyDetailsApplication {
@Inject()
private inventoryDetails: InventoryDetailsService;
+ @Inject()
+ private inventoryDetailsPdf: InventoryDetailsTablePdf;
+
/**
* Retrieves the inventory details report in sheet format.
* @param {number} tenantId
@@ -63,4 +67,14 @@ export class InventortyDetailsApplication {
public csv(tenantId: number, query: IInventoryDetailsQuery): Promise {
return this.inventoryDetailsExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the inventory details report in PDF format.
+ * @param {number} tenantId
+ * @param {IInventoryDetailsQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IInventoryDetailsQuery) {
+ return this.inventoryDetailsPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsMeta.ts b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsMeta.ts
new file mode 100644
index 000000000..578954833
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsMeta.ts
@@ -0,0 +1,35 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import { IInventoryDetailsQuery, IInventoryItemDetailMeta } from '@/interfaces';
+
+@Service()
+export class InventoryDetailsMetaInjectable {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the inventoy details meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IInventoryDetailsQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedToDay = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDay}`;
+
+ const sheetName = 'Inventory Item Details';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedFromDate,
+ formattedToDay,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts
index d74137753..e9b6614d6 100644
--- a/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts
+++ b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts
@@ -1,17 +1,12 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
-import {
- IInventoryDetailsQuery,
- IInvetoryItemDetailDOO,
- IInventoryItemDetailMeta,
-} from '@/interfaces';
+import { IInventoryDetailsQuery, IInvetoryItemDetailDOO } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { InventoryDetails } from './InventoryDetails';
import FinancialSheet from '../FinancialSheet';
import InventoryDetailsRepository from './InventoryDetailsRepository';
-import InventoryService from '@/services/Inventory/Inventory';
-import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
+import { InventoryDetailsMetaInjectable } from './InventoryDetailsMeta';
@Service()
export class InventoryDetailsService extends FinancialSheet {
@@ -22,7 +17,7 @@ export class InventoryDetailsService extends FinancialSheet {
private reportRepo: InventoryDetailsRepository;
@Inject()
- private inventoryService: InventoryService;
+ private inventoryDetailsMeta: InventoryDetailsMetaInjectable;
/**
* Defaults balance sheet filter query.
@@ -46,33 +41,6 @@ export class InventoryDetailsService extends FinancialSheet {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IInventoryItemDetailMeta}
- */
- private reportMetadata(tenantId: number): IInventoryItemDetailMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning =
- this.inventoryService.isItemsCostComputeRunning(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve the inventory details report data.
* @param {number} tenantId -
@@ -115,11 +83,12 @@ export class InventoryDetailsService extends FinancialSheet {
tenant.metadata.baseCurrency,
i18n
);
+ const meta = await this.inventoryDetailsMeta.meta(tenantId, query);
return {
data: inventoryDetailsInstance.reportData(),
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsTablePdf.ts b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsTablePdf.ts
new file mode 100644
index 000000000..b14b11c80
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsTablePdf.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { InventoryDetailsTableInjectable } from './InventoryDetailsTableInjectable';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { IInventoryDetailsQuery } from '@/interfaces';
+import { HtmlTableCustomCss } from './constant';
+
+@Service()
+export class InventoryDetailsTablePdf {
+ @Inject()
+ private inventoryDetailsTable: InventoryDetailsTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given inventory details sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IInventoryDetailsQuery
+ ): Promise {
+ const table = await this.inventoryDetailsTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryDetails/constant.ts b/packages/server/src/services/FinancialStatements/InventoryDetails/constant.ts
new file mode 100644
index 000000000..eeb199236
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryDetails/constant.ts
@@ -0,0 +1,7 @@
+export const HtmlTableCustomCss = `
+table tr.row-type--item td,
+table tr.row-type--opening-entry td,
+table tr.row-type--closing-entry td{
+ font-weight: 500;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheet.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheet.ts
index 693ee2692..fa27707e1 100644
--- a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheet.ts
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheet.ts
@@ -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;
@@ -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 };
}
}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetApplication.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetApplication.ts
new file mode 100644
index 000000000..7c4a65967
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetApplication.ts
@@ -0,0 +1,93 @@
+import {
+ IInventoryValuationReportQuery,
+ IInventoryValuationSheet,
+ IInventoryValuationTable,
+} from '@/interfaces';
+import { Inject, Service } from 'typedi';
+import { InventoryValuationSheetService } from './InventoryValuationSheetService';
+import { InventoryValuationSheetTableInjectable } from './InventoryValuationSheetTableInjectable';
+import { InventoryValuationSheetExportable } from './InventoryValuationSheetExportable';
+import { InventoryValuationSheetPdf } from './InventoryValuationSheetPdf';
+
+@Service()
+export class InventoryValuationSheetApplication {
+ @Inject()
+ private inventoryValuationSheet: InventoryValuationSheetService;
+
+ @Inject()
+ private inventoryValuationTable: InventoryValuationSheetTableInjectable;
+
+ @Inject()
+ private inventoryValuationExport: InventoryValuationSheetExportable;
+
+ @Inject()
+ private inventoryValuationPdf: InventoryValuationSheetPdf;
+
+ /**
+ * Retrieves the inventory valuation json format.
+ * @param {number} tenantId
+ * @param {IInventoryValuationReportQuery} query
+ * @returns
+ */
+ public sheet(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ return this.inventoryValuationSheet.inventoryValuationSheet(
+ tenantId,
+ query
+ );
+ }
+
+ /**
+ * Retrieves the inventory valuation json table format.
+ * @param {number} tenantId
+ * @param {IInventoryValuationReportQuery} query
+ * @returns {Promise}
+ */
+ public table(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ 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 {
+ 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 {
+ return this.inventoryValuationExport.csv(tenantId, query);
+ }
+
+ /**
+ * Retrieves the inventory valuation pdf format.
+ * @param {number} tenantId
+ * @param {IInventoryValuationReportQuery} query
+ * @returns {Promise}
+ */
+ public pdf(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ return this.inventoryValuationPdf.pdf(tenantId, query);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetExportable.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetExportable.ts
new file mode 100644
index 000000000..2403ed16b
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetExportable.ts
@@ -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}
+ */
+ public async xlsx(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ 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}
+ */
+ public async csv(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ const table = await this.inventoryValuationTable.table(tenantId, query);
+
+ const tableSheet = new TableSheet(table.table);
+ const tableCsv = tableSheet.convertToCSV();
+
+ return tableCsv;
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetMeta.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetMeta.ts
new file mode 100644
index 000000000..822253342
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetMeta.ts
@@ -0,0 +1,33 @@
+
+
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import { IBalanceSheetMeta, IBalanceSheetQuery, IInventoryValuationReportQuery } from '@/interfaces';
+import moment from 'moment';
+
+@Service()
+export class InventoryValuationMetaInjectable {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the balance sheet meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
+ const formattedDateRange = `As ${formattedAsDate}`;
+
+ return {
+ ...commonMeta,
+ sheetName: 'Inventory Valuation Sheet',
+ formattedAsDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetPdf.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetPdf.ts
new file mode 100644
index 000000000..449cec9cc
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetPdf.ts
@@ -0,0 +1,36 @@
+import { Inject, Service } from "typedi";
+import { InventoryValuationSheetTableInjectable } from "./InventoryValuationSheetTableInjectable";
+import { TableSheetPdf } from "../TableSheetPdf";
+import { IInventoryValuationReportQuery } from "@/interfaces";
+import { HtmlTableCustomCss } from "./_constants";
+
+
+@Service()
+export class InventoryValuationSheetPdf {
+ @Inject()
+ private inventoryValuationTable: InventoryValuationSheetTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given balance sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IInventoryValuationReportQuery
+ ): Promise {
+ const table = await this.inventoryValuationTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService.ts
index 75c871ce8..4da85b21d 100644
--- a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService.ts
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService.ts
@@ -3,15 +3,17 @@ 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';
+import { InventoryValuationMetaInjectable } from './InventoryValuationSheetMeta';
@Service()
-export default class InventoryValuationSheetService {
+export class InventoryValuationSheetService {
@Inject()
tenancy: TenancyService;
@@ -21,6 +23,9 @@ export default class InventoryValuationSheetService {
@Inject()
inventoryService: InventoryService;
+ @Inject()
+ private inventoryValuationMeta: InventoryValuationMetaInjectable;
+
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
@@ -45,33 +50,6 @@ export default class InventoryValuationSheetService {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning =
- this.inventoryService.isItemsCostComputeRunning(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- organizationName,
- baseCurrency,
- isCostComputeRunning,
- };
- }
-
/**
* Inventory valuation sheet.
* @param {number} tenantId - Tenant id.
@@ -80,7 +58,7 @@ export default class InventoryValuationSheetService {
public async inventoryValuationSheet(
tenantId: number,
query: IInventoryValuationReportQuery
- ) {
+ ): Promise {
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -135,10 +113,13 @@ export default class InventoryValuationSheetService {
// Retrieve the inventory valuation report data.
const inventoryValuationData = inventoryValuationInstance.reportData();
+ // Retrieves the inventorty valuation meta.
+ const meta = await this.inventoryValuationMeta.meta(tenantId, filter);
+
return {
data: inventoryValuationData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetTable.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetTable.ts
new file mode 100644
index 000000000..7e792f6f9
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetTable.ts
@@ -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);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetTableInjectable.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetTableInjectable.ts
new file mode 100644
index 000000000..cad8126b4
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetTableInjectable.ts
@@ -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}
+ */
+ public async table(
+ tenantId: number,
+ filter: IInventoryValuationReportQuery
+ ): Promise {
+ 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,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/InventoryValuationSheet/_constants.ts b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/_constants.ts
new file mode 100644
index 000000000..f41136b6f
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/InventoryValuationSheet/_constants.ts
@@ -0,0 +1,12 @@
+export enum ROW_TYPE {
+ ITEM = 'ITEM',
+ TOTAL = 'TOTAL',
+}
+
+export const HtmlTableCustomCss = `
+table tr.row-type--total td {
+ border-top: 1px solid #bbb;
+ font-weight: 600;
+ border-bottom: 3px double #000;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts
index ee184a5a1..89725db3b 100644
--- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts
@@ -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);
}
}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts
new file mode 100644
index 000000000..8d3f5d614
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetApplication.ts
@@ -0,0 +1,73 @@
+import { Inject } from 'typedi';
+import { JournalSheetService } from './JournalSheetService';
+import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
+import { IJournalReportQuery, IJournalTable } from '@/interfaces';
+import { JournalSheetExportInjectable } from './JournalSheetExport';
+import { JournalSheetPdfInjectable } from './JournalSheetPdfInjectable';
+
+export class JournalSheetApplication {
+ @Inject()
+ private journalSheetTable: JournalSheetTableInjectable;
+
+ @Inject()
+ private journalSheet: JournalSheetService;
+
+ @Inject()
+ private journalExport: JournalSheetExportInjectable;
+
+ @Inject()
+ private journalPdf: JournalSheetPdfInjectable;
+
+ /**
+ * 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}
+ */
+ public table(
+ tenantId: number,
+ query: IJournalReportQuery
+ ): Promise {
+ 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);
+ }
+
+ /**
+ * Retrieves the journal sheet in pdf format.
+ * @param {number} tenantId
+ * @param {IJournalReportQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IJournalReportQuery) {
+ return this.journalPdf.pdf(tenantId, query);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts
new file mode 100644
index 000000000..815c0a308
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetExport.ts
@@ -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}
+ */
+ 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}
+ */
+ public async csv(
+ tenantId: number,
+ query: IJournalReportQuery
+ ): Promise {
+ const table = await this.journalSheetTable.table(tenantId, query);
+
+ const tableSheet = new TableSheet(table.table);
+ const tableCsv = tableSheet.convertToCSV();
+
+ return tableCsv;
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetMeta.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetMeta.ts
new file mode 100644
index 000000000..5980e1e70
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetMeta.ts
@@ -0,0 +1,34 @@
+import { Service, Inject } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import moment from 'moment';
+import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
+
+@Service()
+export class JournalSheetMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the journal sheet meta.
+ * @param {number} tenantId
+ * @param {IJournalReportQuery} query
+ * @returns {Promise}
+ */
+ public async meta(
+ tenantId: number,
+ query: IJournalReportQuery
+ ): Promise {
+ const common = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ return {
+ ...common,
+ formattedDateRange,
+ formattedFromDate,
+ formattedToDate,
+ };
+ }
+}
+
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetPdfInjectable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetPdfInjectable.ts
new file mode 100644
index 000000000..6606d6705
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetPdfInjectable.ts
@@ -0,0 +1,35 @@
+import { IJournalReportQuery } from '@/interfaces';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
+import { Inject, Service } from 'typedi';
+import { HtmlTableCustomCss } from './constant';
+
+@Service()
+export class JournalSheetPdfInjectable {
+ @Inject()
+ private journalSheetTable: JournalSheetTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given journal sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IJournalReportQuery
+ ): Promise {
+ const table = await this.journalSheetTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts
index fe0a071ff..90027c3a1 100644
--- a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts
@@ -1,24 +1,20 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
-import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
-
+import { IJournalReportQuery, IJournalSheet } 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 { transformToMap } from 'utils';
+import { JournalSheetMeta } from './JournalSheetMeta';
@Service()
-export default class JournalSheetService {
+export class JournalSheetService {
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
@Inject()
- inventoryService: InventoryService;
-
- @Inject('logger')
- logger: any;
+ private journalSheetMeta: JournalSheetMeta;
/**
* Default journal sheet filter queyr.
@@ -37,39 +33,16 @@ export default class JournalSheetService {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- reportMetadata(tenantId: number): IJournalSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning =
- this.inventoryService.isItemsCostComputeRunning(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Journal sheet.
* @param {number} tenantId
- * @param {IJournalSheetFilterQuery} query
+ * @param {IJournalReportQuery} query
+ * @returns {Promise}
*/
- async journalSheet(tenantId: number, query: IJournalReportQuery) {
+ async journalSheet(
+ tenantId: number,
+ query: IJournalReportQuery
+ ): Promise {
const i18n = this.tenancy.i18n(tenantId);
const { accountRepository, transactionsRepository, contactRepository } =
this.tenancy.repositories(tenantId);
@@ -80,11 +53,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');
@@ -130,10 +98,13 @@ export default class JournalSheetService {
// Retrieve journal report columns.
const journalSheetData = journalSheetInstance.reportData();
+ // Retrieve the journal sheet meta.
+ const meta = await this.journalSheetMeta.meta(tenantId, filter);
+
return {
data: journalSheetData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts
new file mode 100644
index 000000000..724e2ec2b
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTable.ts
@@ -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);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts
new file mode 100644
index 000000000..0754d78f8
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/JournalSheetTableInjectable.ts
@@ -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}
+ */
+ public async table(
+ tenantId: number,
+ query: IJournalReportQuery
+ ): Promise {
+ 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,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/constant.ts b/packages/server/src/services/FinancialStatements/JournalSheet/constant.ts
new file mode 100644
index 000000000..58f5237f6
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/constant.ts
@@ -0,0 +1,17 @@
+export const HtmlTableCustomCss = `
+table tr.row-type--total td{
+ font-weight: 600;
+}
+table tr td:not(:first-child) {
+ border-left: 1px solid #ececec;
+}
+table tr:last-child td {
+ border-bottom: 1px solid #ececec;
+}
+table .cell--credit,
+table .cell--debit,
+table .column--credit,
+table .column--debit{
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/JournalSheet/types.ts b/packages/server/src/services/FinancialStatements/JournalSheet/types.ts
new file mode 100644
index 000000000..6eff84957
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/JournalSheet/types.ts
@@ -0,0 +1,5 @@
+
+export enum ROW_TYPE {
+ ENTRY = 'ENTRY',
+ TOTAL = 'TOTAL'
+};
\ No newline at end of file
diff --git a/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetApplication.ts b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetApplication.ts
index eee89e73a..6d15e2cc6 100644
--- a/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetApplication.ts
+++ b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetApplication.ts
@@ -3,6 +3,7 @@ import { ProfitLossSheetExportInjectable } from './ProfitLossSheetExportInjectab
import { ProfitLossSheetTableInjectable } from './ProfitLossSheetTableInjectable';
import { IProfitLossSheetQuery, IProfitLossSheetTable } from '@/interfaces';
import ProfitLossSheetService from './ProfitLossSheetService';
+import { ProfitLossTablePdfInjectable } from './ProfitLossTablePdfInjectable';
@Service()
export class ProfitLossSheetApplication {
@@ -15,6 +16,9 @@ export class ProfitLossSheetApplication {
@Inject()
private profitLossSheet: ProfitLossSheetService;
+ @Inject()
+ private profitLossPdf: ProfitLossTablePdfInjectable;
+
/**
* Retreives the profit/loss sheet.
* @param {number} tenantId
@@ -57,4 +61,14 @@ export class ProfitLossSheetApplication {
public xlsx(tenantId: number, query: IProfitLossSheetQuery): Promise {
return this.profitLossExport.xlsx(tenantId, query);
}
+
+ /**
+ * Retrieves the profit/loss sheet in pdf format.
+ * @param {number} tenantId
+ * @param {IProfitLossSheetQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IProfitLossSheetQuery): Promise {
+ return this.profitLossPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetMeta.ts b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetMeta.ts
new file mode 100644
index 000000000..c278420f5
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetMeta.ts
@@ -0,0 +1,35 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import { ICashFlowStatementMeta, ICashFlowStatementQuery } from '@/interfaces';
+
+@Service()
+export class ProfitLossSheetMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the P/L sheet meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: ICashFlowStatementQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ const sheetName = 'Cashflow Statement';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts
index 20cf9a170..f3b24e7a6 100644
--- a/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts
+++ b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService.ts
@@ -6,11 +6,10 @@ import {
} from '@/interfaces';
import ProfitLossSheet from './ProfitLossSheet';
import TenancyService from '@/services/Tenancy/TenancyService';
-import InventoryService from '@/services/Inventory/Inventory';
-import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import { mergeQueryWithDefaults } from './utils';
import { ProfitLossSheetRepository } from './ProfitLossSheetRepository';
+import { ProfitLossSheetMeta } from './ProfitLossSheetMeta';
// Profit/Loss sheet service.
@Service()
@@ -19,7 +18,7 @@ export default class ProfitLossSheetService {
private tenancy: TenancyService;
@Inject()
- private inventoryService: InventoryService;
+ private profitLossSheetMeta: ProfitLossSheetMeta;
/**
* Retrieve profit/loss sheet statement.
@@ -47,6 +46,7 @@ export default class ProfitLossSheetService {
const profitLossRepo = new ProfitLossSheetRepository(models, filter);
+ // Loads the profit/loss sheet data.
await profitLossRepo.asyncInitialize();
// Profit/Loss report instance.
@@ -57,38 +57,15 @@ export default class ProfitLossSheetService {
i18n
);
// Profit/loss report data and collumns.
- const profitLossData = profitLossInstance.reportData();
+ const data = profitLossInstance.reportData();
+
+ // Retrieve the profit/loss sheet meta.
+ const meta = await this.profitLossSheetMeta.meta(tenantId, filter);
return {
- data: profitLossData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ data,
+ meta,
};
};
-
- /**
- * Retrieve the trial balance sheet meta.
- * @param {number} tenantId - Tenant id.
- * @returns {ITrialBalanceSheetMeta}
- */
- private reportMetadata(tenantId: number): IProfitLossSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning =
- this.inventoryService.isItemsCostComputeRunning(tenantId);
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency,
- };
- }
}
diff --git a/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossTablePdfInjectable.ts b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossTablePdfInjectable.ts
new file mode 100644
index 000000000..4b0ba23de
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossTablePdfInjectable.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { IProfitLossSheetQuery } from '@/interfaces';
+import { ProfitLossSheetTableInjectable } from './ProfitLossSheetTableInjectable';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class ProfitLossTablePdfInjectable {
+ @Inject()
+ private profitLossTable: ProfitLossSheetTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Retrieves the profit/loss sheet in pdf format.
+ * @param {number} tenantId
+ * @param {number} query
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IProfitLossSheetQuery
+ ): Promise {
+ const table = await this.profitLossTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/ProfitLossSheet/constants.ts b/packages/server/src/services/FinancialStatements/ProfitLossSheet/constants.ts
index 56f4a14ba..1ca50c5ed 100644
--- a/packages/server/src/services/FinancialStatements/ProfitLossSheet/constants.ts
+++ b/packages/server/src/services/FinancialStatements/ProfitLossSheet/constants.ts
@@ -18,4 +18,52 @@ export const TOTAL_NODE_TYPES = [
ProfitLossNodeType.ACCOUNTS,
ProfitLossNodeType.AGGREGATE,
ProfitLossNodeType.EQUATION
-];
\ No newline at end of file
+];
+
+export const HtmlTableCustomCss =`
+table tr.row-type--total td {
+ font-weight: 600;
+ border-top: 1px solid #bbb;
+ color: #000;
+}
+table tr.row-id--net-income td{
+ border-bottom: 3px double #000;
+}
+table .column--name,
+table .cell--name {
+ width: 400px;
+}
+
+table .column--total {
+ width: 25%;
+}
+table td.cell--total,
+table td.cell--previous_year,
+table td.cell--previous_year_change,
+table td.cell--previous_year_percentage,
+
+table td.cell--previous_period,
+table td.cell--previous_period_change,
+table td.cell--previous_period_percentage,
+
+table td.cell--percentage_of_row,
+table td.cell--percentage_of_column,
+table td[class*="cell--date-range"] {
+ text-align: right;
+}
+
+table .column--total,
+table .column--previous_year,
+table .column--previous_year_change,
+table .column--previous_year_percentage,
+
+table .column--previous_period,
+table .column--previous_period_change,
+table .column--previous_period_percentage,
+
+table .column--percentage_of_row,
+table .column--percentage_of_column,
+table [class*="column--date-range"] {
+ text-align: right;
+}
+`;
\ No newline at end of file
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItems.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItems.ts
index 0dba04f03..b679334c6 100644
--- a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItems.ts
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItems.ts
@@ -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;
- 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 };
}
}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication.ts
new file mode 100644
index 000000000..eeecaff38
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication.ts
@@ -0,0 +1,90 @@
+import { Service, Inject } from 'typedi';
+import { PurchasesByItemsExport } from './PurchasesByItemsExport';
+import {
+ IPurchasesByItemsReportQuery,
+ IPurchasesByItemsSheet,
+ IPurchasesByItemsTable,
+} from '@/interfaces/PurchasesByItemsSheet';
+import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
+import { PurchasesByItemsService } from './PurchasesByItemsService';
+import { PurchasesByItemsPdf } from './PurchasesByItemsPdf';
+
+@Service()
+export class PurcahsesByItemsApplication {
+ @Inject()
+ private purchasesByItemsSheet: PurchasesByItemsService;
+
+ @Inject()
+ private purchasesByItemsTable: PurchasesByItemsTableInjectable;
+
+ @Inject()
+ private purchasesByItemsExport: PurchasesByItemsExport;
+
+ @Inject()
+ private purchasesByItemsPdf: PurchasesByItemsPdf;
+
+ /**
+ * Retrieves the purchases by items in json format.
+ * @param {number} tenantId
+ * @param {IPurchasesByItemsReportQuery} query
+ * @returns
+ */
+ public sheet(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ return this.purchasesByItemsSheet.purchasesByItems(tenantId, query);
+ }
+
+ /**
+ * Retrieves the purchases by items in table format.
+ * @param {number} tenantId
+ * @param {IPurchasesByItemsReportQuery} query
+ * @returns {Promise}
+ */
+ public table(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ return this.purchasesByItemsTable.table(tenantId, query);
+ }
+
+ /**
+ * Retrieves the purchases by items in csv format.
+ * @param {number} tenantId
+ * @param {IPurchasesByItemsReportQuery} query
+ * @returns {Promise}
+ */
+ public csv(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ return this.purchasesByItemsExport.csv(tenantId, query);
+ }
+
+ /**
+ * Retrieves the purchases by items in xlsx format.
+ * @param {number} tenantId
+ * @param {IPurchasesByItemsReportQuery} query
+ * @returns {Promise}
+ */
+ public xlsx(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ return this.purchasesByItemsExport.xlsx(tenantId, query);
+ }
+
+ /**
+ * Retrieves the purchases by items in pdf format.
+ * @param {number} tenantId
+ * @param {IPurchasesByItemsReportQuery} filter
+ * @returns {Promise}
+ */
+ public pdf(
+ tenantId: number,
+ filter: IPurchasesByItemsReportQuery
+ ): Promise {
+ return this.purchasesByItemsPdf.pdf(tenantId, filter);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsExport.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsExport.ts
new file mode 100644
index 000000000..96ed4daf7
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsExport.ts
@@ -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}
+ */
+ public async xlsx(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ 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}
+ */
+ public async csv(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ const table = await this.purchasesByItemsTable.table(tenantId, query);
+
+ const tableSheet = new TableSheet(table.table);
+ const tableCsv = tableSheet.convertToCSV();
+
+ return tableCsv;
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsMeta.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsMeta.ts
new file mode 100644
index 000000000..4bb22c583
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsMeta.ts
@@ -0,0 +1,36 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import {
+ IPurchasesByItemsReportQuery,
+ IPurchasesByItemsSheetMeta,
+} from '@/interfaces/PurchasesByItemsSheet';
+
+@Service()
+export class PurchasesByItemsMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the purchases by items meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ return {
+ ...commonMeta,
+ sheetName: 'Purchases By Items',
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsPdf.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsPdf.ts
new file mode 100644
index 000000000..67e3e7a24
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsPdf.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
+import { IPurchasesByItemsReportQuery } from '@/interfaces/PurchasesByItemsSheet';
+import { HtmlTableCustomCss } from './_types';
+
+@Service()
+export class PurchasesByItemsPdf {
+ @Inject()
+ private purchasesByItemsTable: PurchasesByItemsTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given journal sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
+ const table = await this.purchasesByItemsTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService.ts
index df4aee1ad..a2d6240cb 100644
--- a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService.ts
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService.ts
@@ -1,24 +1,27 @@
-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,
+} from '@/interfaces/PurchasesByItemsSheet';
+import { PurchasesByItemsMeta } from './PurchasesByItemsMeta';
@Service()
-export default class InventoryValuationReportService {
+export class PurchasesByItemsService {
@Inject()
private tenancy: TenancyService;
+ @Inject()
+ private purchasesByItemsMeta: PurchasesByItemsMeta;
+
/**
- * Defaults balance sheet filter query.
- * @return {IBalanceSheetQuery}
+ * Defaults purchases by items filter query.
+ * @return {IPurchasesByItemsReportQuery}
*/
- get defaultQuery(): IInventoryValuationReportQuery {
+ private get defaultQuery(): IPurchasesByItemsReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
@@ -35,45 +38,17 @@ export default class InventoryValuationReportService {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
- * @param {IBalanceSheetQuery} query
- *
- * @return {IBalanceSheetStatement}
+ * @param {IPurchasesByItemsReportQuery} query
+ * @return {Promise}
*/
public async purchasesByItems(
tenantId: number,
- query: IInventoryValuationReportQuery
- ): Promise<{
- data: IInventoryValuationStatement;
- query: IInventoryValuationReportQuery;
- meta: IInventoryValuationSheetMeta;
- }> {
+ query: IPurchasesByItemsReportQuery
+ ): Promise {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -106,7 +81,6 @@ export default class InventoryValuationReportService {
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
}
);
-
const purchasesByItemsInstance = new PurchasesByItems(
filter,
inventoryItems,
@@ -115,10 +89,13 @@ export default class InventoryValuationReportService {
);
const purchasesByItemsData = purchasesByItemsInstance.reportData();
+ // Retrieve the purchases by items meta.
+ const meta = await this.purchasesByItemsMeta.meta(tenantId, query);
+
return {
data: purchasesByItemsData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsTable.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsTable.ts
new file mode 100644
index 000000000..a7b6ee735
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsTable.ts
@@ -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[];
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsTableInjectable.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsTableInjectable.ts
new file mode 100644
index 000000000..066ac6f72
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/PurchasesByItemsTableInjectable.ts
@@ -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}
+ */
+ public async table(
+ tenantId: number,
+ filter: IPurchasesByItemsReportQuery
+ ): Promise {
+ 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,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/PurchasesByItems/_types.ts b/packages/server/src/services/FinancialStatements/PurchasesByItems/_types.ts
new file mode 100644
index 000000000..bb4927958
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/PurchasesByItems/_types.ts
@@ -0,0 +1,23 @@
+export enum ROW_TYPE {
+ TOTAL = 'TOTAL',
+ ITEM = 'ITEM',
+}
+
+export const HtmlTableCustomCss = `
+table tr.row-type--total td {
+ border-top: 1px solid #bbb;
+ border-bottom: 3px double #000;
+ font-weight: 600;
+}
+table .column--item_name{
+ width: 300px;
+}
+table .column--quantity_purchases,
+table .column--purchase_amount,
+table .column--average_cost,
+table .cell--quantity_purchases,
+table .cell--purchase_amount,
+table .cell--average_cost{
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItems.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItems.ts
index 1cb7f0074..9d96febd5 100644
--- a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItems.ts
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItems.ts
@@ -1,4 +1,5 @@
import { get, sumBy } from 'lodash';
+
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, transformToMap } from 'utils';
@@ -7,7 +8,7 @@ import {
IAccountTransaction,
ISalesByItemsItem,
ISalesByItemsTotal,
- ISalesByItemsSheetStatement,
+ ISalesByItemsSheetData,
IItem,
} from '@/interfaces';
@@ -146,7 +147,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 +164,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 };
}
}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsApplication.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsApplication.ts
new file mode 100644
index 000000000..3ca915e39
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsApplication.ts
@@ -0,0 +1,91 @@
+import { Inject, Service } from 'typedi';
+
+import {
+ ISalesByItemsReportQuery,
+ ISalesByItemsSheet,
+ ISalesByItemsTable,
+} from '@/interfaces';
+import { SalesByItemsReportService } from './SalesByItemsService';
+import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
+import { SalesByItemsExport } from './SalesByItemsExport';
+import { SalesByItemsPdfInjectable } from './SalesByItemsPdfInjectable';
+
+@Service()
+export class SalesByItemsApplication {
+ @Inject()
+ private salesByItemsSheet: SalesByItemsReportService;
+
+ @Inject()
+ private salesByItemsTable: SalesByItemsTableInjectable;
+
+ @Inject()
+ private salesByItemsExport: SalesByItemsExport;
+
+ @Inject()
+ private salesByItemsPdf: SalesByItemsPdfInjectable;
+
+ /**
+ * Retrieves the sales by items report in json format.
+ * @param {number} tenantId
+ * @param {ISalesByItemsReportQuery} filter
+ * @returns {Promise}
+ */
+ public sheet(
+ tenantId: number,
+ filter: ISalesByItemsReportQuery
+ ): Promise {
+ return this.salesByItemsSheet.salesByItems(tenantId, filter);
+ }
+
+ /**
+ * Retrieves the sales by items report in table format.
+ * @param {number} tenantId
+ * @param {ISalesByItemsReportQuery} filter
+ * @returns {Promise}
+ */
+ public table(
+ tenantId: number,
+ filter: ISalesByItemsReportQuery
+ ): Promise {
+ return this.salesByItemsTable.table(tenantId, filter);
+ }
+
+ /**
+ * Retrieves the sales by items report in csv format.
+ * @param {number} tenantId
+ * @param {ISalesByItemsReportQuery} filter
+ * @returns {Promise}
+ */
+ public csv(
+ tenantId: number,
+ filter: ISalesByItemsReportQuery
+ ): Promise {
+ return this.salesByItemsExport.csv(tenantId, filter);
+ }
+
+ /**
+ * Retrieves the sales by items report in xlsx format.
+ * @param {number} tenantId
+ * @param {ISalesByItemsReportQuery} filter
+ * @returns {Promise}
+ */
+ public xlsx(
+ tenantId: number,
+ filter: ISalesByItemsReportQuery
+ ): Promise {
+ return this.salesByItemsExport.xlsx(tenantId, filter);
+ }
+
+ /**
+ * Retrieves the sales by items in pdf format.
+ * @param {number} tenantId
+ * @param {ISalesByItemsReportQuery} query
+ * @returns {Promise}
+ */
+ public pdf(
+ tenantId: number,
+ query: ISalesByItemsReportQuery
+ ): Promise {
+ return this.salesByItemsPdf.pdf(tenantId, query);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsExport.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsExport.ts
new file mode 100644
index 000000000..067aab546
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsExport.ts
@@ -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}
+ */
+ 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}
+ */
+ public async csv(
+ tenantId: number,
+ query: ISalesByItemsReportQuery
+ ): Promise {
+ const table = await this.salesByItemsTable.table(tenantId, query);
+
+ const tableSheet = new TableSheet(table.table);
+ const tableCsv = tableSheet.convertToCSV();
+
+ return tableCsv;
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsMeta.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsMeta.ts
new file mode 100644
index 000000000..b47b45215
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsMeta.ts
@@ -0,0 +1,35 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import { ISalesByItemsReportQuery, ISalesByItemsSheetMeta } from '@/interfaces';
+
+@Service()
+export class SalesByItemsMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieve the sales by items meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: ISalesByItemsReportQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ const sheetName = 'Sales By Items';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsPdfInjectable.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsPdfInjectable.ts
new file mode 100644
index 000000000..874154086
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsPdfInjectable.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { ISalesByItemsReportQuery } from '@/interfaces';
+import { SalesByItemsTableInjectable } from './SalesByItemsTableInjectable';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class SalesByItemsPdfInjectable {
+ @Inject()
+ private salesByItemsTable: SalesByItemsTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Retrieves the sales by items sheet in pdf format.
+ * @param {number} tenantId
+ * @param {number} query
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ISalesByItemsReportQuery
+ ): Promise {
+ const table = await this.salesByItemsTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsService.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsService.ts
index 6f81e1489..1dc9b4f05 100644
--- a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsService.ts
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsService.ts
@@ -1,27 +1,24 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
-import {
- ISalesByItemsReportQuery,
- ISalesByItemsSheetStatement,
- ISalesByItemsSheetMeta
-} from '@/interfaces';
+import { ISalesByItemsReportQuery, ISalesByItemsSheet } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import SalesByItems from './SalesByItems';
import { Tenant } from '@/system/models';
+import { SalesByItemsMeta } from './SalesByItemsMeta';
@Service()
-export default class SalesByItemsReportService {
+export class SalesByItemsReportService {
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
- @Inject('logger')
- logger: any;
+ @Inject()
+ private salesByItemsMeta: SalesByItemsMeta;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
- get defaultQuery(): ISalesByItemsReportQuery {
+ private get defaultQuery(): ISalesByItemsReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
@@ -38,45 +35,16 @@ export default class SalesByItemsReportService {
};
}
- /**
- * Retrieve the balance sheet meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- reportMetadata(tenantId: number): ISalesByItemsSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve balance sheet statement.
- * -------------
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
- *
- * @return {IBalanceSheetStatement}
+ * @return {Promise}
*/
public async salesByItems(
tenantId: number,
query: ISalesByItemsReportQuery
- ): Promise<{
- data: ISalesByItemsSheetStatement,
- query: ISalesByItemsReportQuery,
- meta: ISalesByItemsSheetMeta,
- }> {
+ ): Promise {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -107,22 +75,24 @@ 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();
+
+ // Retrieve the sales by items meta.
+ const meta = await this.salesByItemsMeta.meta(tenantId, query);
return {
- data: purchasesByItemsData,
+ data: salesByItemsData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTable.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTable.ts
new file mode 100644
index 000000000..f06c7bd14
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTable.ts
@@ -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);
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTableInjectable.ts b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTableInjectable.ts
new file mode 100644
index 000000000..c947cfa86
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/SalesByItemsTableInjectable.ts
@@ -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}
+ */
+ 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,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesByItems/constants.ts b/packages/server/src/services/FinancialStatements/SalesByItems/constants.ts
new file mode 100644
index 000000000..761e37ec5
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesByItems/constants.ts
@@ -0,0 +1,23 @@
+export enum ROW_TYPE {
+ ITEM = 'ITEM',
+ TOTAL = 'TOTAL',
+}
+
+export const HtmlTableCustomCss = `
+table tr.row-type--total td {
+ border-top: 1px solid #bbb;
+ border-bottom: 3px double #000;
+ font-weight: 600;
+}
+table .column--item_name{
+ width: 300px;
+}
+table .column--average_price,
+table .column--sold_quantity,
+table .column--sold_amount,
+table .cell--average_price,
+table .cell--sold_quantity,
+table .cell--sold_amount{
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryApplication.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryApplication.ts
index f0e5a5248..f63123687 100644
--- a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryApplication.ts
+++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryApplication.ts
@@ -3,6 +3,7 @@ import { SalesTaxLiabilitySummaryQuery } from '@/interfaces/SalesTaxLiabilitySum
import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
import { SalesTaxLiabilitySummaryExportInjectable } from './SalesTaxLiabilitySummaryExportInjectable';
import { SalesTaxLiabilitySummaryService } from './SalesTaxLiabilitySummaryService';
+import { SalesTaxLiabiltiySummaryPdf } from './SalesTaxLiabiltiySummaryPdf';
@Service()
export class SalesTaxLiabilitySummaryApplication {
@@ -15,6 +16,9 @@ export class SalesTaxLiabilitySummaryApplication {
@Inject()
private salesTaxLiabilityTable: SalesTaxLiabilitySummaryTableInjectable;
+ @Inject()
+ private salesTaxLiabiltiyPdf: SalesTaxLiabiltiySummaryPdf;
+
/**
* Retrieves the sales tax liability summary in json format.
* @param {number} tenantId
@@ -60,4 +64,17 @@ export class SalesTaxLiabilitySummaryApplication {
): Promise {
return this.salesTaxLiabilityExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the sales tax liability summary in PDF format.
+ * @param {number} tenantId
+ * @param {SalesTaxLiabilitySummaryQuery} query
+ * @returns {Promise}
+ */
+ public pdf(
+ tenantId: number,
+ query: SalesTaxLiabilitySummaryQuery
+ ): Promise {
+ return this.salesTaxLiabiltiyPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryMeta.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryMeta.ts
new file mode 100644
index 000000000..c18a8f55d
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryMeta.ts
@@ -0,0 +1,32 @@
+import { Inject, Service } from 'typedi';
+import moment from 'moment';
+import { SalesTaxLiabilitySummaryQuery } from '@/interfaces/SalesTaxLiabilitySummary';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+
+@Service()
+export class SalesTaxLiabilitySummaryMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the report meta.
+ * @param {number} tenantId
+ * @param {SalesTaxLiabilitySummaryQuery} filter
+ */
+ public async meta(tenantId: number, query: SalesTaxLiabilitySummaryQuery) {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ const sheetName = 'Sales Tax Liability Summary';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts
index a1ec7a771..585e9e4d9 100644
--- a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts
+++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService.ts
@@ -1,11 +1,8 @@
import { Inject, Service } from 'typedi';
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
-import {
- SalesTaxLiabilitySummaryMeta,
- SalesTaxLiabilitySummaryQuery,
-} from '@/interfaces/SalesTaxLiabilitySummary';
+import { SalesTaxLiabilitySummaryQuery } from '@/interfaces/SalesTaxLiabilitySummary';
import { SalesTaxLiabilitySummary } from './SalesTaxLiabilitySummary';
-import HasTenancyService from '@/services/Tenancy/TenancyService';
+import { SalesTaxLiabilitySummaryMeta } from './SalesTaxLiabilitySummaryMeta';
@Service()
export class SalesTaxLiabilitySummaryService {
@@ -13,7 +10,7 @@ export class SalesTaxLiabilitySummaryService {
private repostiory: SalesTaxLiabilitySummaryRepository;
@Inject()
- private tenancy: HasTenancyService;
+ private salesTaxLiabilityMeta: SalesTaxLiabilitySummaryMeta;
/**
* Retrieve sales tax liability summary.
@@ -39,33 +36,12 @@ export class SalesTaxLiabilitySummaryService {
payableByRateId,
salesByRateId
);
+ const meta = await this.salesTaxLiabilityMeta.meta(tenantId, query);
+
return {
data: taxLiabilitySummary.reportData(),
query,
- meta: this.reportMetadata(tenantId),
- };
- }
-
- /**
- * Retrieve the report meta.
- * @param {number} tenantId -
- * @returns {IBalanceSheetMeta}
- */
- private reportMetadata(tenantId: number): SalesTaxLiabilitySummaryMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- organizationName,
- baseCurrency,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabiltiySummaryPdf.ts b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabiltiySummaryPdf.ts
new file mode 100644
index 000000000..742241842
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabiltiySummaryPdf.ts
@@ -0,0 +1,36 @@
+import { Inject, Service } from 'typedi';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { SalesTaxLiabilitySummaryTableInjectable } from './SalesTaxLiabilitySummaryTableInjectable';
+import { ISalesByItemsReportQuery } from '@/interfaces';
+import { SalesTaxLiabilitySummaryQuery } from '@/interfaces/SalesTaxLiabilitySummary';
+
+@Service()
+export class SalesTaxLiabiltiySummaryPdf {
+ @Inject()
+ private salesTaxLiabiltiySummaryTable: SalesTaxLiabilitySummaryTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given sales tax liability summary table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {ISalesByItemsReportQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: SalesTaxLiabilitySummaryQuery
+ ): Promise {
+ const table = await this.salesTaxLiabiltiySummaryTable.table(
+ tenantId,
+ query
+ );
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TableSheetPdf.ts b/packages/server/src/services/FinancialStatements/TableSheetPdf.ts
new file mode 100644
index 000000000..280adf187
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TableSheetPdf.ts
@@ -0,0 +1,79 @@
+import { Inject, Service } from 'typedi';
+import * as R from 'ramda';
+import { ITableColumn, ITableData, ITableRow } from '@/interfaces';
+import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
+import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
+import { FinancialTableStructure } from './FinancialTableStructure';
+import { tableClassNames } from './utils';
+
+@Service()
+export class TableSheetPdf {
+ @Inject()
+ private templateInjectable: TemplateInjectable;
+
+ @Inject()
+ private chromiumlyTenancy: ChromiumlyTenancy;
+
+ /**
+ * Converts the table data into a PDF format.
+ * @param {number} tenantId - The unique identifier for the tenant.
+ * @param {ITableData} table - The table data to be converted.
+ * @param {string} sheetName - The name of the sheet.
+ * @param {string} sheetDate - The date of the sheet.
+ * @returns A promise that resolves with the PDF conversion result.
+ */
+ public async convertToPdf(
+ tenantId: number,
+ table: ITableData,
+ sheetName: string,
+ sheetDate: string,
+ customCSS?: string
+ ): Promise {
+ // Prepare columns and rows for PDF conversion
+ const columns = this.tablePdfColumns(table.columns);
+ const rows = this.tablePdfRows(table.rows);
+
+ const landscape = columns.length > 4;
+
+ // Generate HTML content from the template
+ const htmlContent = await this.templateInjectable.render(
+ tenantId,
+ 'modules/financial-sheet',
+ {
+ table: { rows, columns },
+ sheetName,
+ sheetDate,
+ customCSS,
+ }
+ );
+ // Convert the HTML content to PDF
+ return this.chromiumlyTenancy.convertHtmlContent(tenantId, htmlContent, {
+ margins: { top: 0, bottom: 0, left: 0, right: 0 },
+ landscape,
+ });
+ }
+
+ /**
+ * Converts the table columns to pdf columns.
+ * @param {ITableColumn[]} columns
+ * @returns {ITableColumn[]}
+ */
+ private tablePdfColumns = (columns: ITableColumn[]): ITableColumn[] => {
+ return columns;
+ };
+
+ /**
+ * Converts the table rows to pdf rows.
+ * @param {ITableRow[]} rows -
+ * @returns {ITableRow[]}
+ */
+ private tablePdfRows = (rows: ITableRow[]): ITableRow[] => {
+ const curriedFlatNestedTree = R.curry(
+ FinancialTableStructure.flatNestedTree
+ );
+ const flatNestedTree = curriedFlatNestedTree(R.__, {
+ nestedPrefix: '',
+ });
+ return R.compose(tableClassNames, flatNestedTree)(rows);
+ };
+}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersApplication.ts b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersApplication.ts
index b729c219a..c3e4feb7e 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersApplication.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersApplication.ts
@@ -6,6 +6,7 @@ import {
import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable';
import { TransactionsByCustomersExportInjectable } from './TransactionsByCustomersExportInjectable';
import { TransactionsByCustomersSheet } from './TransactionsByCustomersService';
+import { TransactionsByCustomersPdf } from './TransactionsByCustomersPdf';
@Service()
export class TransactionsByCustomerApplication {
@@ -18,6 +19,9 @@ export class TransactionsByCustomerApplication {
@Inject()
private transactionsByCustomersSheet: TransactionsByCustomersSheet;
+ @Inject()
+ private transactionsByCustomersPdf: TransactionsByCustomersPdf;
+
/**
* Retrieves the transactions by customers sheet in json format.
* @param {number} tenantId
@@ -69,4 +73,17 @@ export class TransactionsByCustomerApplication {
): Promise {
return this.transactionsByCustomersExport.xlsx(tenantId, query);
}
+
+ /**
+ * Retrieves the transactions by vendors sheet in PDF format.
+ * @param {number} tenantId
+ * @param {ITransactionsByCustomersFilter} query
+ * @returns {Promise}
+ */
+ public pdf(
+ tenantId: number,
+ query: ITransactionsByCustomersFilter
+ ): Promise {
+ return this.transactionsByCustomersPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersMeta.ts b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersMeta.ts
new file mode 100644
index 000000000..710bc0bc4
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersMeta.ts
@@ -0,0 +1,36 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import {
+ ITransactionsByCustomersFilter,
+ ITransactionsByCustomersMeta,
+} from '@/interfaces';
+
+@Service()
+export class TransactionsByCustomersMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the transactions by customers meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: ITransactionsByCustomersFilter
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ return {
+ ...commonMeta,
+ sheetName: 'Transactions By Customers',
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersPdf.ts b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersPdf.ts
new file mode 100644
index 000000000..122cbbaf2
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersPdf.ts
@@ -0,0 +1,34 @@
+import { ITransactionsByCustomersFilter } from '@/interfaces';
+import { Inject } from 'typedi';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { TransactionsByCustomersTableInjectable } from './TransactionsByCustomersTableInjectable';
+
+export class TransactionsByCustomersPdf {
+ @Inject()
+ private transactionsByCustomersTable: TransactionsByCustomersTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Retrieves the transactions by customers in PDF format.
+ * @param {number} tenantId - Tenant ID.
+ * @param {ITransactionsByCustomersFilter} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ITransactionsByCustomersFilter
+ ): Promise {
+ const table = await this.transactionsByCustomersTable.table(
+ tenantId,
+ query
+ );
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts
index 3fbf8cf06..a97a13afe 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts
@@ -12,6 +12,7 @@ import TransactionsByCustomers from './TransactionsByCustomers';
import Ledger from '@/services/Accounting/Ledger';
import TransactionsByCustomersRepository from './TransactionsByCustomersRepository';
import { Tenant } from '@/system/models';
+import { TransactionsByCustomersMeta } from './TransactionsByCustomersMeta';
export class TransactionsByCustomersSheet
implements ITransactionsByCustomersService
@@ -22,6 +23,9 @@ export class TransactionsByCustomersSheet
@Inject()
private reportRepository: TransactionsByCustomersRepository;
+ @Inject()
+ private transactionsByCustomersMeta: TransactionsByCustomersMeta;
+
/**
* Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery}
@@ -160,9 +164,12 @@ export class TransactionsByCustomersSheet
i18n
);
+ const meta = await this.transactionsByCustomersMeta.meta(tenantId, filter);
+
return {
data: reportInstance.reportData(),
query: filter,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTable.ts b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTable.ts
index 7016cf853..2f38b9cde 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTable.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTable.ts
@@ -1,6 +1,10 @@
import * as R from 'ramda';
import { tableRowMapper } from 'utils';
-import { ITransactionsByCustomersCustomer, ITableRow, ITableColumn } from '@/interfaces';
+import {
+ ITransactionsByCustomersCustomer,
+ ITableRow,
+ ITableColumn,
+} from '@/interfaces';
import TransactionsByContactsTableRows from '../TransactionsByContact/TransactionsByContactTableRows';
enum ROW_TYPE {
@@ -78,6 +82,14 @@ export class TransactionsByCustomersTable extends TransactionsByContactsTableRow
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
- return [];
- }
+ return [
+ { key: 'customer_name', label: 'Customer name' },
+ { key: 'account_name', label: 'Account Name' },
+ { key: 'ref_type', label: 'Reference Type' },
+ { key: 'transaction_type', label: 'Transaction Type' },
+ { key: 'credit', label: 'Credit' },
+ { key: 'debit', label: 'Debit' },
+ { key: 'running_balance', label: 'Running Balance' },
+ ];
+ };
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts
index fb1c61311..2f2aa0277 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableInjectable.ts
@@ -39,6 +39,7 @@ export class TransactionsByCustomersTableInjectable {
columns: table.tableColumns(),
},
query: customersTransactions.query,
+ meta: customersTransactions.meta
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorApplication.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorApplication.ts
index d8d424a30..54a4c7ed4 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorApplication.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorApplication.ts
@@ -7,6 +7,7 @@ import {
import { TransactionsByVendorExportInjectable } from './TransactionsByVendorExportInjectable';
import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
import { TransactionsByVendorsInjectable } from './TransactionsByVendorInjectable';
+import { TransactionsByVendorsPdf } from './TransactionsByVendorPdf';
@Service()
export class TransactionsByVendorApplication {
@@ -19,6 +20,9 @@ export class TransactionsByVendorApplication {
@Inject()
private transactionsByVendorSheet: TransactionsByVendorsInjectable;
+ @Inject()
+ private transactionsByVendorPdf: TransactionsByVendorsPdf;
+
/**
* Retrieves the transactions by vendor in sheet format.
* @param {number} tenantId
@@ -65,6 +69,7 @@ export class TransactionsByVendorApplication {
* Retrieves the transactions by vendor in XLSX format.
* @param {number} tenantId
* @param {ITransactionsByVendorsFilter} query
+ * @returns {Promise}
*/
public xlsx(
tenantId: number,
@@ -72,4 +77,14 @@ export class TransactionsByVendorApplication {
): Promise {
return this.transactionsByVendorExport.xlsx(tenantId, query);
}
+
+ /**
+ * Retrieves the transactions by vendor in PDF format.
+ * @param {number} tenantId
+ * @param {ITransactionsByVendorsFilter} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: ITransactionsByVendorsFilter) {
+ return this.transactionsByVendorPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorInjectable.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorInjectable.ts
index a24041929..69b424957 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorInjectable.ts
@@ -12,6 +12,7 @@ import TransactionsByVendor from './TransactionsByVendor';
import Ledger from '@/services/Accounting/Ledger';
import TransactionsByVendorRepository from './TransactionsByVendorRepository';
import { Tenant } from '@/system/models';
+import { TransactionsByVendorMeta } from './TransactionsByVendorMeta';
export class TransactionsByVendorsInjectable
implements ITransactionsByVendorsService
@@ -22,6 +23,9 @@ export class TransactionsByVendorsInjectable
@Inject()
private reportRepository: TransactionsByVendorRepository;
+ @Inject()
+ private transactionsByVendorMeta: TransactionsByVendorMeta;
+
/**
* Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery}
@@ -165,9 +169,12 @@ export class TransactionsByVendorsInjectable
tenant.metadata.baseCurrency,
i18n
);
+ const meta = await this.transactionsByVendorMeta.meta(tenantId, filter);
+
return {
data: reportInstance.reportData(),
query: filter,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorMeta.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorMeta.ts
new file mode 100644
index 000000000..42295cc22
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorMeta.ts
@@ -0,0 +1,38 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import {
+ ITransactionsByVendorMeta,
+ ITransactionsByVendorsFilter,
+} from '@/interfaces';
+
+@Service()
+export class TransactionsByVendorMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the transactions by vendor meta.
+ * @param {number} tenantId -
+ * @returns {Promise}
+ */
+ public async meta(
+ tenantId: number,
+ query: ITransactionsByVendorsFilter
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
+
+ const sheetName = 'Transactions By Vendor';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorPdf.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorPdf.ts
new file mode 100644
index 000000000..d133a9ebc
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorPdf.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { ITransactionsByVendorsFilter } from '@/interfaces';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { TransactionsByVendorTableInjectable } from './TransactionsByVendorTableInjectable';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class TransactionsByVendorsPdf {
+ @Inject()
+ private transactionsByVendorTable: TransactionsByVendorTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given balance sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {IBalanceSheetQuery} query - Balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ITransactionsByVendorsFilter
+ ): Promise {
+ const table = await this.transactionsByVendorTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTable.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTable.ts
index 14ad3c1b4..2f8bf2498 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTable.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTable.ts
@@ -43,7 +43,6 @@ export class TransactionsByVendorsTable extends TransactionsByContactsTableRows
accessor: 'closingBalance.formattedAmount',
},
];
-
return {
...tableRowMapper(vendor, columns, { rowTypes: [ROW_TYPE.VENDOR] }),
children: R.pipe(
@@ -82,6 +81,14 @@ export class TransactionsByVendorsTable extends TransactionsByContactsTableRows
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
- return [];
+ return [
+ { key: 'vendor_name', label: 'Vendor name' },
+ { key: 'account_name', label: 'Account Name' },
+ { key: 'ref_type', label: 'Reference Type' },
+ { key: 'transaction_type', label: 'Transaction Type' },
+ { key: 'credit', label: 'Credit' },
+ { key: 'debit', label: 'Debit' },
+ { key: 'running_balance', label: 'Running Balance' },
+ ];
};
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableInjectable.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableInjectable.ts
index 5b42b88e7..5c5243059 100644
--- a/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableInjectable.ts
@@ -39,6 +39,7 @@ export class TransactionsByVendorTableInjectable {
columns: table.tableColumns(),
},
query,
+ meta: sheet.meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/TransactionsByVendor/constants.ts b/packages/server/src/services/FinancialStatements/TransactionsByVendor/constants.ts
new file mode 100644
index 000000000..d35a2f105
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TransactionsByVendor/constants.ts
@@ -0,0 +1,23 @@
+export const HtmlTableCustomCss = `
+table tr td:not(:first-child) {
+ border-left: 1px solid #ececec;
+}
+table tr:last-child td {
+ border-bottom: 1px solid #ececec;
+}
+table .cell--credit,
+table .cell--debit,
+table .column--credit,
+table .column--debit,
+table .column--running_balance,
+table .cell--running_balance{
+ text-align: right;
+}
+table tr.row-type--closing-balance td,
+table tr.row-type--opening-balance td {
+ font-weight: 600;
+}
+table tr.row-type--vendor:not(:first-child) td {
+ border-top: 1px solid #ddd;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceExportInjectable.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceExportInjectable.ts
index a515f1beb..62b847f7c 100644
--- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceExportInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceExportInjectable.ts
@@ -1,13 +1,17 @@
+import { Inject, Service } from 'typedi';
import { TableSheet } from '@/lib/Xlsx/TableSheet';
import { ITrialBalanceSheetQuery } from '@/interfaces';
-import { Inject, Service } from 'typedi';
import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable';
+import { TrialBalanceSheetPdfInjectable } from './TrialBalanceSheetPdfInjectsable';
@Service()
export class TrialBalanceExportInjectable {
@Inject()
private trialBalanceSheetTable: TrialBalanceSheetTableInjectable;
+ @Inject()
+ private trialBalanceSheetPdf: TrialBalanceSheetPdfInjectable;
+
/**
* Retrieves the trial balance sheet in XLSX format.
* @param {number} tenantId
@@ -40,4 +44,17 @@ export class TrialBalanceExportInjectable {
return tableCsv;
}
+
+ /**
+ * Retrieves the trial balance sheet in PDF format.
+ * @param {number} tenantId
+ * @param {ITrialBalanceSheetQuery} query
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ITrialBalanceSheetQuery
+ ): Promise {
+ return this.trialBalanceSheetPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts
index 38ed3a944..8554268e3 100644
--- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts
@@ -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);
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetApplication.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetApplication.ts
index a771c8f15..20c485ecc 100644
--- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetApplication.ts
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetApplication.ts
@@ -57,4 +57,14 @@ export class TrialBalanceSheetApplication {
public async xlsx(tenantId: number, query: ITrialBalanceSheetQuery) {
return this.exportable.xlsx(tenantId, query);
}
+
+ /**
+ * Retrieve the trial balance sheet in pdf format.
+ * @param {number} tenantId
+ * @param {ITrialBalanceSheetQuery} query
+ * @returns {Promise}
+ */
+ public async pdf(tenantId: number, query: ITrialBalanceSheetQuery) {
+ return this.exportable.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetInjectable.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetInjectable.ts
index bc880ec99..df498241b 100644
--- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetInjectable.ts
@@ -1,28 +1,20 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import TenancyService from '@/services/Tenancy/TenancyService';
-import {
- ITrialBalanceSheetMeta,
- ITrialBalanceSheetQuery,
- ITrialBalanceStatement,
-} from '@/interfaces';
+import { ITrialBalanceSheetQuery, ITrialBalanceStatement } from '@/interfaces';
import TrialBalanceSheet from './TrialBalanceSheet';
import FinancialSheet from '../FinancialSheet';
-import InventoryService from '@/services/Inventory/Inventory';
-import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
+import { TrialBalanceSheetMeta } from './TrialBalanceSheetMeta';
@Service()
export default class TrialBalanceSheetService extends FinancialSheet {
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
@Inject()
- inventoryService: InventoryService;
-
- @Inject('logger')
- logger: any;
+ private trialBalanceSheetMetaService: TrialBalanceSheetMeta;
/**
* Defaults trial balance sheet filter query.
@@ -47,32 +39,6 @@ export default class TrialBalanceSheetService extends FinancialSheet {
};
}
- /**
- * Retrieve the trial balance sheet meta.
- * @param {number} tenantId - Tenant id.
- * @returns {ITrialBalanceSheetMeta}
- */
- private reportMetadata(tenantId: number): ITrialBalanceSheetMeta {
- const settings = this.tenancy.settings(tenantId);
-
- const isCostComputeRunning =
- this.inventoryService.isItemsCostComputeRunning(tenantId);
- const organizationName = settings.get({
- group: 'organization',
- key: 'name',
- });
- const baseCurrency = settings.get({
- group: 'organization',
- key: 'base_currency',
- });
-
- return {
- isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
- organizationName,
- baseCurrency,
- };
- }
-
/**
* Retrieve trial balance sheet statement.
* @param {number} tenantId
@@ -99,6 +65,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
repos,
filter
);
+ // Loads the resources.
await trialBalanceSheetRepos.asyncInitialize();
// Trial balance report instance.
@@ -111,10 +78,13 @@ export default class TrialBalanceSheetService extends FinancialSheet {
// Trial balance sheet data.
const trialBalanceSheetData = trialBalanceInstance.reportData();
+ // Trial balance sheet meta.
+ const meta = await this.trialBalanceSheetMetaService.meta(tenantId, filter);
+
return {
data: trialBalanceSheetData,
query: filter,
- meta: this.reportMetadata(tenantId),
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetMeta.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetMeta.ts
new file mode 100644
index 000000000..7561e2b66
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetMeta.ts
@@ -0,0 +1,37 @@
+import { Inject, Service } from 'typedi';
+import moment from 'moment';
+import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery } from '@/interfaces';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+
+@Service()
+export class TrialBalanceSheetMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the trial balance sheet meta.
+ * @param {number} tenantId
+ * @param {ITrialBalanceSheetQuery} query
+ * @returns {Promise}
+ */
+ public async meta(
+ tenantId: number,
+ query: ITrialBalanceSheetQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+
+ const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
+ const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
+ const formattedDateRange = `From ${formattedFromDate} to ${formattedToDate}`;
+
+ const sheetName = 'Trial Balance Sheet';
+
+ return {
+ ...commonMeta,
+ sheetName,
+ formattedFromDate,
+ formattedToDate,
+ formattedDateRange,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetPdfInjectsable.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetPdfInjectsable.ts
new file mode 100644
index 000000000..b7b53d437
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetPdfInjectsable.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { ITrialBalanceSheetQuery } from '@/interfaces';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { TrialBalanceSheetTableInjectable } from './TrialBalanceSheetTableInjectable';
+import { HtmlTableCustomCss } from './_constants';
+
+@Service()
+export class TrialBalanceSheetPdfInjectable {
+ @Inject()
+ private trialBalanceSheetTable: TrialBalanceSheetTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Converts the given trial balance sheet table to pdf.
+ * @param {number} tenantId - Tenant ID.
+ * @param {ITrialBalanceSheetQuery} query - Trial balance sheet query.
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: ITrialBalanceSheetQuery
+ ): Promise {
+ const table = await this.trialBalanceSheetTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedDateRange,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts
index 1cbb2e7e6..0d4df537e 100644
--- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.ts
@@ -2,7 +2,6 @@ import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { FinancialTable } from '../FinancialTable';
import {
- IBalanceSheetStatementData,
ITableColumn,
ITableColumnAccessor,
ITableRow,
@@ -20,12 +19,13 @@ export class TrialBalanceSheetTable extends R.compose(
FinancialSheetStructure
)(FinancialSheet) {
/**
+ * Trial balance sheet data.
* @param {ITrialBalanceSheetData}
*/
public data: ITrialBalanceSheetData;
/**
- * Balance sheet query.
+ * Trial balance sheet query.
* @param {ITrialBalanceSheetQuery}
*/
public query: ITrialBalanceSheetQuery;
@@ -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())
)([]);
};
@@ -136,7 +136,7 @@ export class TrialBalanceSheetTable extends R.compose(
return R.compose(
this.tableColumnsCellIndexing,
R.concat([
- { key: 'account_name', label: 'Account' },
+ { key: 'account', label: 'Account' },
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
{ key: 'total', label: 'Total' },
diff --git a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts
index 91e8c595f..7a52e631b 100644
--- a/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts
+++ b/packages/server/src/services/FinancialStatements/TrialBalanceSheet/_constants.ts
@@ -1,5 +1,25 @@
-
export enum IROW_TYPE {
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
-}
\ No newline at end of file
+}
+
+export const HtmlTableCustomCss = `
+table tr.row-type--total td{
+ border-top: 1px solid #bbb;
+ font-weight: 600;
+ border-bottom: 3px double #000;
+}
+
+table .column--account {
+ width: 400px;
+}
+
+table .column--debit,
+table .column--credit,
+table .column--total,
+table .cell--debit,
+table .cell--credit,
+table .cell--total{
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts
index 2e5e09aa4..6d04461b6 100644
--- a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummary.ts
@@ -41,7 +41,7 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
/**
* Customer section mapper.
- * @param {IVendor} vendor
+ * @param {IVendor} vendor
* @returns {IVendorBalanceSummaryVendor}
*/
private vendorMapper = (vendor: IVendor): IVendorBalanceSummaryVendor => {
@@ -58,7 +58,7 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
/**
* Mappes the vendor model object to vendor balance summary section.
- * @param {IVendor[]} vendors - Customers.
+ * @param {IVendor[]} vendors - Customers.
* @returns {IVendorBalanceSummaryVendor[]}
*/
private vendorsMapper = (
@@ -77,7 +77,7 @@ export class VendorBalanceSummaryReport extends ContactBalanceSummaryReport {
/**
* Retrieve the vendors sections of the report.
- * @param {IVendor} vendors
+ * @param {IVendor} vendors
* @returns {IVendorBalanceSummaryVendor[]}
*/
private getVendorsSection(vendors: IVendor[]): IVendorBalanceSummaryVendor[] {
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryApplication.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryApplication.ts
index 5fe4bc74d..c02eac224 100644
--- a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryApplication.ts
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryApplication.ts
@@ -3,6 +3,7 @@ import { IVendorBalanceSummaryQuery } from '@/interfaces';
import { VendorBalanceSummaryTableInjectable } from './VendorBalanceSummaryTableInjectable';
import { VendorBalanceSummaryExportInjectable } from './VendorBalanceSummaryExportInjectable';
import { VendorBalanceSummaryService } from './VendorBalanceSummaryService';
+import { VendorBalanceSummaryPdf } from './VendorBalanceSummaryPdf';
@Service()
export class VendorBalanceSummaryApplication {
@@ -15,6 +16,9 @@ export class VendorBalanceSummaryApplication {
@Inject()
private vendorBalanceSummaryExport: VendorBalanceSummaryExportInjectable;
+ @Inject()
+ private vendorBalanceSummaryPdf: VendorBalanceSummaryPdf;
+
/**
* Retrieves the vendor balance summary sheet in sheet format.
* @param {number} tenantId
@@ -59,4 +63,14 @@ export class VendorBalanceSummaryApplication {
): Promise {
return this.vendorBalanceSummaryExport.csv(tenantId, query);
}
+
+ /**
+ * Retrieves the vendor balance summary sheet in pdf format.
+ * @param {number} tenantId
+ * @param {IVendorBalanceSummaryQuery} query
+ * @returns {Promise}
+ */
+ public pdf(tenantId: number, query: IVendorBalanceSummaryQuery) {
+ return this.vendorBalanceSummaryPdf.pdf(tenantId, query);
+ }
}
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryMeta.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryMeta.ts
new file mode 100644
index 000000000..80071f8ca
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryMeta.ts
@@ -0,0 +1,32 @@
+import moment from 'moment';
+import { Inject, Service } from 'typedi';
+import { FinancialSheetMeta } from '../FinancialSheetMeta';
+import {
+ IVendorBalanceSummaryMeta,
+ IVendorBalanceSummaryQuery,
+} from '@/interfaces';
+
+@Service()
+export class VendorBalanceSummaryMeta {
+ @Inject()
+ private financialSheetMeta: FinancialSheetMeta;
+
+ /**
+ * Retrieves the vendor balance summary meta.
+ * @param {number} tenantId -
+ * @returns {IBalanceSheetMeta}
+ */
+ public async meta(
+ tenantId: number,
+ query: IVendorBalanceSummaryQuery
+ ): Promise {
+ const commonMeta = await this.financialSheetMeta.meta(tenantId);
+ const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
+
+ return {
+ ...commonMeta,
+ sheetName: 'Vendor Balance Summary',
+ formattedAsDate,
+ };
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryPdf.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryPdf.ts
new file mode 100644
index 000000000..f13ffc98c
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryPdf.ts
@@ -0,0 +1,35 @@
+import { Inject, Service } from 'typedi';
+import { IVendorBalanceSummaryQuery } from '@/interfaces';
+import { TableSheetPdf } from '../TableSheetPdf';
+import { VendorBalanceSummaryTableInjectable } from './VendorBalanceSummaryTableInjectable';
+import { HtmlTableCustomCss } from './constants';
+
+@Service()
+export class VendorBalanceSummaryPdf {
+ @Inject()
+ private vendorBalanceSummaryTable: VendorBalanceSummaryTableInjectable;
+
+ @Inject()
+ private tableSheetPdf: TableSheetPdf;
+
+ /**
+ * Retrieves the sales by items sheet in pdf format.
+ * @param {number} tenantId
+ * @param {number} query
+ * @returns {Promise}
+ */
+ public async pdf(
+ tenantId: number,
+ query: IVendorBalanceSummaryQuery
+ ): Promise {
+ const table = await this.vendorBalanceSummaryTable.table(tenantId, query);
+
+ return this.tableSheetPdf.convertToPdf(
+ tenantId,
+ table.table,
+ table.meta.sheetName,
+ table.meta.formattedAsDate,
+ HtmlTableCustomCss
+ );
+ }
+}
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts
index b1a361b87..c64496b7b 100644
--- a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts
@@ -12,15 +12,21 @@ import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
import Ledger from '@/services/Accounting/Ledger';
import VendorBalanceSummaryRepository from './VendorBalanceSummaryRepository';
import { Tenant } from '@/system/models';
+import { JournalSheetMeta } from '../JournalSheet/JournalSheetMeta';
+
+import { VendorBalanceSummaryMeta } from './VendorBalanceSummaryMeta';
export class VendorBalanceSummaryService
implements IVendorBalanceSummaryService
{
@Inject()
- tenancy: TenancyService;
+ private tenancy: TenancyService;
@Inject()
- reportRepo: VendorBalanceSummaryRepository;
+ private reportRepo: VendorBalanceSummaryRepository;
+
+ @Inject()
+ private vendorBalanceSummaryMeta: VendorBalanceSummaryMeta;
/**
* Defaults balance sheet filter query.
@@ -43,6 +49,7 @@ export class VendorBalanceSummaryService
}
/**
+ *
* Retrieve the vendors ledger entrjes.
* @param {number} tenantId -
* @param {Date|string} date -
@@ -97,10 +104,13 @@ export class VendorBalanceSummaryService
filter,
tenant.metadata.baseCurrency
);
+ // Retrieve the vendor balance summary meta.
+ const meta = await this.vendorBalanceSummaryMeta.meta(tenantId, filter);
return {
data: reportInstance.reportData(),
query: filter,
+ meta
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts
index aec97e6f1..c055b1151 100644
--- a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableInjectable.ts
@@ -27,10 +27,12 @@ export class VendorBalanceSummaryTableInjectable {
): Promise {
const i18n = this.tenancy.i18n(tenantId);
- const { data } = await this.vendorBalanceSummarySheet.vendorBalanceSummary(
- tenantId,
- query
- );
+ const { data, meta } =
+ await this.vendorBalanceSummarySheet.vendorBalanceSummary(
+
+ tenantId,
+ query
+ );
const table = new VendorBalanceSummaryTable(data, query, i18n);
return {
@@ -39,6 +41,7 @@ export class VendorBalanceSummaryTableInjectable {
rows: table.tableRows(),
},
query,
+ meta,
};
}
}
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts
index 2095ea087..56fdbf924 100644
--- a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows.ts
@@ -54,7 +54,7 @@ export class VendorBalanceSummaryTable {
*/
private getVendorColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
- { key: 'vendorName', accessor: 'vendorName' },
+ { key: 'name', accessor: 'vendorName' },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
@@ -87,7 +87,7 @@ export class VendorBalanceSummaryTable {
*/
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
const columns = [
- { key: 'total', value: this.i18n.__('Total') },
+ { key: 'name', value: this.i18n.__('Total') },
{ key: 'total', accessor: 'total.formattedAmount' },
];
return R.compose(
diff --git a/packages/server/src/services/FinancialStatements/VendorBalanceSummary/constants.ts b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/constants.ts
new file mode 100644
index 000000000..513a3dcdb
--- /dev/null
+++ b/packages/server/src/services/FinancialStatements/VendorBalanceSummary/constants.ts
@@ -0,0 +1,14 @@
+export const HtmlTableCustomCss = `
+table tr.row-type--total td {
+ font-weight: 600;
+ border-top: 1px solid #bbb;
+ border-bottom: 3px double #333;
+}
+table .column--name {
+ width: 65%;
+}
+table .column--total,
+table .cell--total {
+ text-align: right;
+}
+`;
diff --git a/packages/server/src/services/FinancialStatements/utils.ts b/packages/server/src/services/FinancialStatements/utils.ts
index 1114131b9..5d304ca6f 100644
--- a/packages/server/src/services/FinancialStatements/utils.ts
+++ b/packages/server/src/services/FinancialStatements/utils.ts
@@ -1,4 +1,5 @@
-
+import { kebabCase } from 'lodash';
+import { ITableRow } from '@/interfaces';
export const formatNumber = (balance, { noCents, divideOn1000 }): string => {
let formattedBalance: number = parseFloat(balance);
@@ -10,4 +11,20 @@ export const formatNumber = (balance, { noCents, divideOn1000 }): string => {
formattedBalance /= 1000;
}
return formattedBalance;
-};
\ No newline at end of file
+};
+
+export const tableClassNames = (rows: ITableRow[]) => {
+ return rows.map((row) => {
+ const classNames =
+ row?.rowTypes?.map((rowType) => `row-type--${kebabCase(rowType)}`) || [];
+
+ if (row.id) {
+ classNames.push(`row-id--${kebabCase(row.id)}`);
+ }
+
+ return {
+ ...row,
+ classNames,
+ };
+ });
+};
diff --git a/packages/server/src/services/MailNotification/ContactMailNotification.ts b/packages/server/src/services/MailNotification/ContactMailNotification.ts
new file mode 100644
index 000000000..e1e733a79
--- /dev/null
+++ b/packages/server/src/services/MailNotification/ContactMailNotification.ts
@@ -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}
+ */
+ public async getDefaultMailOptions(
+ tenantId: number,
+ contactId: number,
+ subject: string = '',
+ body: string = ''
+ ): Promise {
+ 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}
+ */
+ public async getMailOptions(
+ tenantId: number,
+ contactId: number,
+ defaultSubject?: string,
+ defaultBody?: string,
+ formatterData?: Record
+ ): Promise {
+ 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>}
+ */
+ public async getCommonFormatArgs(
+ tenantId: number
+ ): Promise> {
+ const organization = await Tenant.query()
+ .findById(tenantId)
+ .withGraphFetched('metadata');
+
+ return {
+ CompanyName: organization.metadata.name,
+ };
+ }
+}
diff --git a/packages/server/src/services/MailNotification/constants.ts b/packages/server/src/services/MailNotification/constants.ts
new file mode 100644
index 000000000..95b720d70
--- /dev/null
+++ b/packages/server/src/services/MailNotification/constants.ts
@@ -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',
+};
diff --git a/packages/server/src/services/MailNotification/utils.ts b/packages/server/src/services/MailNotification/utils.ts
new file mode 100644
index 000000000..b9e37b297
--- /dev/null
+++ b/packages/server/src/services/MailNotification/utils.ts
@@ -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} mailOptions
+ * @param {Partial} overridedOptions
+ * @throws {ServiceError}
+ */
+export function parseAndValidateMailOptions(
+ mailOptions: Partial,
+ overridedOptions: Partial
+) {
+ 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;
+}
diff --git a/packages/server/src/services/MailTenancy/MailTenancy.ts b/packages/server/src/services/MailTenancy/MailTenancy.ts
new file mode 100644
index 000000000..6f8e82e11
--- /dev/null
+++ b/packages/server/src/services/MailTenancy/MailTenancy.ts
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts b/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts
index 3aa2902ca..8bbfadbff 100644
--- a/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts
+++ b/packages/server/src/services/Purchases/BillPayments/BillPaymentValidators.ts
@@ -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
diff --git a/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts
index 4ab1c9a25..e4a1ab1fa 100644
--- a/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts
+++ b/packages/server/src/services/Purchases/BillPayments/DeleteBillPayment.ts
@@ -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.
diff --git a/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts
index 20c72d38b..de18853bb 100644
--- a/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts
+++ b/packages/server/src/services/Purchases/BillPayments/EditBillPayment.ts
@@ -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,
diff --git a/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts b/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts
index 5984e8932..ccb1cab77 100644
--- a/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts
+++ b/packages/server/src/services/Purchases/BillPayments/GetBillPayment.ts
@@ -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,
diff --git a/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts b/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts
index 7b86c8f04..ec839411b 100644
--- a/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts
+++ b/packages/server/src/services/Purchases/BillPayments/GetPaymentBills.ts
@@ -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);
diff --git a/packages/server/src/services/Purchases/Bills/BillsValidators.ts b/packages/server/src/services/Purchases/Bills/BillsValidators.ts
index cba38dfbb..9f209e40d 100644
--- a/packages/server/src/services/Purchases/Bills/BillsValidators.ts
+++ b/packages/server/src/services/Purchases/Bills/BillsValidators.ts
@@ -21,6 +21,20 @@ export class BillsValidators {
}
}
+ /**
+ * Validates the bill amount is bigger than paid amount.
+ * @param {number} billAmount
+ * @param {number} paidAmount
+ */
+ public validateBillAmountBiggerPaidAmount(
+ billAmount: number,
+ paidAmount: number,
+ ) {
+ if (billAmount < paidAmount) {
+ throw new ServiceError(ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT);
+ }
+ }
+
/**
* Validates the bill number existance.
*/
diff --git a/packages/server/src/services/Purchases/Bills/EditBill.ts b/packages/server/src/services/Purchases/Bills/EditBill.ts
index 26121c120..7b10bb7f1 100644
--- a/packages/server/src/services/Purchases/Bills/EditBill.ts
+++ b/packages/server/src/services/Purchases/Bills/EditBill.ts
@@ -103,6 +103,7 @@ export class EditBill {
tenantId,
billDTO.entries
);
+
// Transforms the bill DTO to model object.
const billObj = await this.transformerDTO.billDTOToModel(
tenantId,
@@ -111,6 +112,11 @@ export class EditBill {
authorizedUser,
oldBill
);
+ // Validate bill total amount should be bigger than paid amount.
+ this.validators.validateBillAmountBiggerPaidAmount(
+ billObj.amount,
+ oldBill.paymentAmount
+ );
// Validate landed cost entries that have allocated cost could not be deleted.
await this.entriesService.validateLandedCostEntriesNotDeleted(
oldBill.entries,
diff --git a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts
index 4ec163d8b..4cb92adf9 100644
--- a/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts
+++ b/packages/server/src/services/Purchases/Bills/PurchaseInvoiceTransformer.ts
@@ -13,6 +13,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
return [
'formattedBillDate',
'formattedDueDate',
+ 'formattedAmount',
'formattedPaymentAmount',
'formattedBalance',
'formattedDueAmount',
diff --git a/packages/server/src/services/Purchases/Bills/constants.ts b/packages/server/src/services/Purchases/Bills/constants.ts
index 12afad4c7..9cc8566c8 100644
--- a/packages/server/src/services/Purchases/Bills/constants.ts
+++ b/packages/server/src/services/Purchases/Bills/constants.ts
@@ -18,6 +18,7 @@ export const ERRORS = {
LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS:
'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS',
BILL_HAS_APPLIED_TO_VENDOR_CREDIT: 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT',
+ BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT',
};
export const DEFAULT_VIEW_COLUMNS = [];
diff --git a/packages/server/src/services/Purchases/VendorCredits/EditVendorCredit.ts b/packages/server/src/services/Purchases/VendorCredits/EditVendorCredit.ts
index d76dd3690..a0ff0f421 100644
--- a/packages/server/src/services/Purchases/VendorCredits/EditVendorCredit.ts
+++ b/packages/server/src/services/Purchases/VendorCredits/EditVendorCredit.ts
@@ -9,6 +9,7 @@ import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class EditVendorCredit extends BaseVendorCredit {
@@ -21,6 +22,9 @@ export default class EditVendorCredit extends BaseVendorCredit {
@Inject()
private itemsEntriesService: ItemsEntriesService;
+ @Inject()
+ private tenancy: HasTenancyService;
+
/**
* Deletes the given vendor credit.
* @param {number} tenantId - Tenant id.
@@ -31,7 +35,7 @@ export default class EditVendorCredit extends BaseVendorCredit {
vendorCreditId: number,
vendorCreditDTO: IVendorCreditEditDTO
) => {
- const { VendorCredit } = this.tenancy.models(tenantId);
+ const { VendorCredit, Contact } = this.tenancy.models(tenantId);
// Retrieve the vendor credit or throw not found service error.
const oldVendorCredit = await this.getVendorCreditOrThrowError(
diff --git a/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts b/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts
index 3d74ee770..be1431ac2 100644
--- a/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts
+++ b/packages/server/src/services/Purchases/VendorCredits/VendorCreditTransformer.ts
@@ -11,6 +11,7 @@ export class VendorCreditTransformer extends Transformer {
public includeAttributes = (): string[] => {
return [
'formattedAmount',
+ 'formattedSubtotal',
'formattedVendorCreditDate',
'formattedCreditsRemaining',
'entries',
@@ -37,6 +38,15 @@ export class VendorCreditTransformer extends Transformer {
});
};
+ /**
+ * Retrieves the vendor credit formatted subtotal.
+ * @param {IVendorCredit} vendorCredit
+ * @returns {string}
+ */
+ protected formattedSubtotal = (vendorCredit): string => {
+ return formatNumber(vendorCredit.amount, { money: false });
+ };
+
/**
* Retrieve formatted credits remaining.
* @param {IVendorCredit} credit
diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts b/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts
index 1102f7bd0..8cd99a9db 100644
--- a/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts
+++ b/packages/server/src/services/Sales/Estimates/SaleEstimateTransformer.ts
@@ -10,6 +10,7 @@ export class SaleEstimateTransfromer extends Transformer {
*/
public includeAttributes = (): string[] => {
return [
+ 'formattedSubtotal',
'formattedAmount',
'formattedEstimateDate',
'formattedExpirationDate',
@@ -76,6 +77,15 @@ export class SaleEstimateTransfromer extends Transformer {
});
};
+ /**
+ * Retrieves the formatted invoice subtotal.
+ * @param {ISaleEstimate} estimate
+ * @returns {string}
+ */
+ protected formattedSubtotal = (estimate: ISaleEstimate): string => {
+ return formatNumber(estimate.amount, { money: false });
+ };
+
/**
* Retrieves the entries of the sale estimate.
* @param {ISaleEstimate} estimate
diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts
index 3f63b27de..f1c7b3cdf 100644
--- a/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts
+++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesApplication.ts
@@ -7,6 +7,8 @@ import {
ISaleEstimate,
ISaleEstimateDTO,
ISalesEstimatesFilter,
+ SaleEstimateMailOptions,
+ SaleEstimateMailOptionsDTO,
} from '@/interfaces';
import { EditSaleEstimate } from './EditSaleEstimate';
import { DeleteSaleEstimate } from './DeleteSaleEstimate';
@@ -17,6 +19,7 @@ import { ApproveSaleEstimate } from './ApproveSaleEstimate';
import { RejectSaleEstimate } from './RejectSaleEstimate';
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
+import { SendSaleEstimateMail } from './SendSaleEstimateMail';
@Service()
export class SaleEstimatesApplication {
@@ -50,6 +53,9 @@ export class SaleEstimatesApplication {
@Inject()
private saleEstimatesPdfService: SaleEstimatesPdf;
+ @Inject()
+ private sendEstimateMailService: SendSaleEstimateMail;
+
/**
* Create a sale estimate.
* @param {number} tenantId - The tenant id.
@@ -198,15 +204,49 @@ export class SaleEstimatesApplication {
};
/**
- *
+ * Retrieve the PDF content of the given sale estimate.
* @param {number} tenantId
- * @param {} saleEstimate
+ * @param {number} saleEstimateId
* @returns
*/
- public getSaleEstimatePdf(tenantId: number, saleEstimate) {
+ public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
return this.saleEstimatesPdfService.getSaleEstimatePdf(
tenantId,
- saleEstimate
+ saleEstimateId
+ );
+ }
+
+ /**
+ * Send the reminder mail of the given sale estimate.
+ * @param {number} tenantId
+ * @param {number} saleEstimateId
+ * @returns {Promise}
+ */
+ public sendSaleEstimateMail(
+ tenantId: number,
+ saleEstimateId: number,
+ saleEstimateMailOpts: SaleEstimateMailOptionsDTO
+ ): Promise {
+ return this.sendEstimateMailService.triggerMail(
+ tenantId,
+ saleEstimateId,
+ saleEstimateMailOpts
+ );
+ }
+
+ /**
+ * Retrieves the default mail options of the given sale estimate.
+ * @param {number} tenantId
+ * @param {number} saleEstimateId
+ * @returns {Promise}
+ */
+ public getSaleEstimateMail(
+ tenantId: number,
+ saleEstimateId: number
+ ): Promise {
+ return this.sendEstimateMailService.getMailOptions(
+ tenantId,
+ saleEstimateId
);
}
}
diff --git a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts
index db19743f7..af1d2098c 100644
--- a/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts
+++ b/packages/server/src/services/Sales/Estimates/SaleEstimatesPdf.ts
@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
+import { GetSaleEstimate } from './GetSaleEstimate';
@Service()
export class SaleEstimatesPdf {
@@ -10,11 +11,19 @@ export class SaleEstimatesPdf {
@Inject()
private templateInjectable: TemplateInjectable;
+ @Inject()
+ private getSaleEstimate: GetSaleEstimate;
+
/**
* Retrieve sale invoice pdf content.
- * @param {} saleInvoice -
+ * @param {number} tenantId -
+ * @param {ISaleInvoice} saleInvoice -
*/
- async getSaleEstimatePdf(tenantId: number, saleEstimate) {
+ public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
+ const saleEstimate = await this.getSaleEstimate.getEstimate(
+ tenantId,
+ saleEstimateId
+ );
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/estimate-regular',
diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts
new file mode 100644
index 000000000..0ac580b90
--- /dev/null
+++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMail.ts
@@ -0,0 +1,159 @@
+import { Inject, Service } from 'typedi';
+import Mail from '@/lib/Mail';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+import {
+ DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
+ DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
+} from './constants';
+import { SaleEstimatesPdf } from './SaleEstimatesPdf';
+import { GetSaleEstimate } from './GetSaleEstimate';
+import {
+ ISaleEstimateMailPresendEvent,
+ SaleEstimateMailOptions,
+ SaleEstimateMailOptionsDTO,
+} from '@/interfaces';
+import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
+import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import events from '@/subscribers/events';
+
+@Service()
+export class SendSaleEstimateMail {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private estimatePdf: SaleEstimatesPdf;
+
+ @Inject()
+ private getSaleEstimateService: GetSaleEstimate;
+
+ @Inject()
+ private contactMailNotification: ContactMailNotification;
+
+ @Inject('agenda')
+ private agenda: any;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ /**
+ * Triggers the reminder mail of the given sale estimate.
+ * @param {number} tenantId -
+ * @param {number} saleEstimateId -
+ * @param {SaleEstimateMailOptionsDTO} messageOptions -
+ * @returns {Promise}
+ */
+ public async triggerMail(
+ tenantId: number,
+ saleEstimateId: number,
+ messageOptions: SaleEstimateMailOptionsDTO
+ ): Promise {
+ const payload = {
+ tenantId,
+ saleEstimateId,
+ messageOptions,
+ };
+ await this.agenda.now('sale-estimate-mail-send', payload);
+
+ // Triggers `onSaleEstimatePreMailSend` event.
+ await this.eventPublisher.emitAsync(events.saleEstimate.onPreMailSend, {
+ tenantId,
+ saleEstimateId,
+ messageOptions,
+ } as ISaleEstimateMailPresendEvent);
+ }
+
+ /**
+ * Formates the text of the mail.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} estimateId - Estimate id.
+ * @returns {Promise>}
+ */
+ public formatterData = async (tenantId: number, estimateId: number) => {
+ const estimate = await this.getSaleEstimateService.getEstimate(
+ tenantId,
+ estimateId
+ );
+ return {
+ CustomerName: estimate.customer.displayName,
+ EstimateNumber: estimate.estimateNumber,
+ EstimateDate: estimate.formattedEstimateDate,
+ EstimateAmount: estimate.formattedAmount,
+ EstimateExpirationDate: estimate.formattedExpirationDate,
+ };
+ };
+
+ /**
+ * Retrieves the mail options.
+ * @param {number} tenantId
+ * @param {number} saleEstimateId
+ * @returns {Promise}
+ */
+ public getMailOptions = async (
+ tenantId: number,
+ saleEstimateId: number
+ ): Promise => {
+ const { SaleEstimate } = this.tenancy.models(tenantId);
+
+ const saleEstimate = await SaleEstimate.query()
+ .findById(saleEstimateId)
+ .throwIfNotFound();
+
+ const formatterData = await this.formatterData(tenantId, saleEstimateId);
+
+ const mailOptions = await this.contactMailNotification.getMailOptions(
+ tenantId,
+ saleEstimate.customerId,
+ DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
+ DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
+ formatterData
+ );
+ return {
+ ...mailOptions,
+ data: formatterData,
+ attachEstimate: true,
+ };
+ };
+
+ /**
+ * Sends the mail notification of the given sale estimate.
+ * @param {number} tenantId
+ * @param {number} saleEstimateId
+ * @param {SaleEstimateMailOptions} messageOptions
+ * @returns {Promise}
+ */
+ public async sendMail(
+ tenantId: number,
+ saleEstimateId: number,
+ messageOptions: SaleEstimateMailOptionsDTO
+ ): Promise {
+ const localMessageOpts = await this.getMailOptions(
+ tenantId,
+ saleEstimateId
+ );
+ // Overrides and validates the given mail options.
+ const messageOpts = parseAndValidateMailOptions(
+ localMessageOpts,
+ messageOptions
+ );
+ const mail = new Mail()
+ .setSubject(messageOpts.subject)
+ .setTo(messageOpts.to)
+ .setContent(messageOpts.body);
+
+ if (messageOpts.attachEstimate) {
+ const estimatePdfBuffer = await this.estimatePdf.getSaleEstimatePdf(
+ tenantId,
+ saleEstimateId
+ );
+ mail.setAttachments([
+ {
+ filename: messageOpts.data?.EstimateNumber || 'estimate.pdf',
+ content: estimatePdfBuffer,
+ },
+ ]);
+ }
+ await mail.send();
+ }
+}
diff --git a/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts
new file mode 100644
index 000000000..b5e8eda39
--- /dev/null
+++ b/packages/server/src/services/Sales/Estimates/SendSaleEstimateMailJob.ts
@@ -0,0 +1,36 @@
+import Container, { Service } from 'typedi';
+import { SendSaleEstimateMail } from './SendSaleEstimateMail';
+
+@Service()
+export class SendSaleEstimateMailJob {
+ /**
+ * Constructor method.
+ */
+ constructor(agenda) {
+ agenda.define(
+ 'sale-estimate-mail-send',
+ { priority: 'high', concurrency: 2 },
+ this.handler
+ );
+ }
+
+ /**
+ * Triggers sending invoice mail.
+ */
+ private handler = async (job, done: Function) => {
+ const { tenantId, saleEstimateId, messageOptions } = job.attrs.data;
+ const sendSaleEstimateMail = Container.get(SendSaleEstimateMail);
+
+ try {
+ await sendSaleEstimateMail.sendMail(
+ tenantId,
+ saleEstimateId,
+ messageOptions
+ );
+ done();
+ } catch (error) {
+ console.log(error);
+ done(error);
+ }
+ };
+}
diff --git a/packages/server/src/services/Sales/Estimates/constants.ts b/packages/server/src/services/Sales/Estimates/constants.ts
index 2b58c74a8..6b689a0e1 100644
--- a/packages/server/src/services/Sales/Estimates/constants.ts
+++ b/packages/server/src/services/Sales/Estimates/constants.ts
@@ -1,3 +1,18 @@
+export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
+ 'Estimate {EstimateNumber} is awaiting your approval';
+export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `Dear {CustomerName}
+Thank you for your business, You can view or print your estimate from attachements.
+
+Estimate #{EstimateNumber}
+Expiration Date : {EstimateExpirationDate}
+Amount : {EstimateAmount}
+
+
+
+Regards
+{CompanyName}
+
+`;
export const ERRORS = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
@@ -8,7 +23,7 @@ export const ERRORS = {
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
- SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED'
+ SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
};
export const DEFAULT_VIEW_COLUMNS = [];
diff --git a/packages/server/src/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent.ts b/packages/server/src/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent.ts
new file mode 100644
index 000000000..99caa3952
--- /dev/null
+++ b/packages/server/src/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent.ts
@@ -0,0 +1,43 @@
+import { Inject, Service } from 'typedi';
+import events from '@/subscribers/events';
+import { ISaleEstimateMailPresendEvent } from '@/interfaces';
+import { DeliverSaleEstimate } from '../DeliverSaleEstimate';
+import { ServiceError } from '@/exceptions';
+import { ERRORS } from '../constants';
+
+@Service()
+export class SaleEstimateMarkApprovedOnMailSent {
+ @Inject()
+ private deliverEstimateService: DeliverSaleEstimate;
+
+ /**
+ * Attaches events.
+ */
+ public attach(bus) {
+ bus.subscribe(events.saleEstimate.onPreMailSend, this.markEstimateApproved);
+ }
+
+ /**
+ * Marks the given estimate approved on submitting mail.
+ * @param {ISaleEstimateMailPresendEvent}
+ */
+ private markEstimateApproved = async ({
+ tenantId,
+ saleEstimateId,
+ }: ISaleEstimateMailPresendEvent) => {
+ try {
+ await this.deliverEstimateService.deliverSaleEstimate(
+ tenantId,
+ saleEstimateId
+ );
+ } catch (error) {
+ if (
+ error instanceof ServiceError &&
+ error.errorType === ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED
+ ) {
+ } else {
+ throw error;
+ }
+ }
+ };
+}
diff --git a/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts
index 30ba635a6..f5500a03a 100644
--- a/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts
+++ b/packages/server/src/services/Sales/Invoices/DeliverSaleInvoice.ts
@@ -4,7 +4,6 @@ import { ServiceError } from '@/exceptions';
import {
ISaleInvoiceDeliveringPayload,
ISaleInvoiceEventDeliveredPayload,
- ISystemUser,
} from '@/interfaces';
import { ERRORS } from './constants';
import { Inject, Service } from 'typedi';
@@ -36,8 +35,7 @@ export class DeliverSaleInvoice {
*/
public async deliverSaleInvoice(
tenantId: number,
- saleInvoiceId: number,
- authorizedUser: ISystemUser
+ saleInvoiceId: number
): Promise {
const { SaleInvoice } = this.tenancy.models(tenantId);
diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts
index f2245afef..b57f86ed9 100644
--- a/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts
+++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoice.ts
@@ -24,8 +24,7 @@ export class GetSaleInvoice {
*/
public async getSaleInvoice(
tenantId: number,
- saleInvoiceId: number,
- authorizedUser: ISystemUser
+ saleInvoiceId: number
): Promise {
const { SaleInvoice } = this.tenancy.models(tenantId);
diff --git a/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts
new file mode 100644
index 000000000..2a65d316e
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/GetSaleInvoiceMailReminder.ts
@@ -0,0 +1,3 @@
+export class GetSaleInvoiceMailReminder {
+ public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
+}
diff --git a/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts b/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts
index ad0d88525..dbaea4862 100644
--- a/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts
+++ b/packages/server/src/services/Sales/Invoices/ItemEntryTransformer.ts
@@ -8,7 +8,16 @@ export class ItemEntryTransformer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
- return ['rateFormatted', 'totalFormatted'];
+ return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
+ };
+
+ /**
+ * Retrieves the formatted quantitty of item entry.
+ * @param {IItemEntry} entry
+ * @returns {string}
+ */
+ protected quantityFormatted = (entry: IItemEntry): string => {
+ return formatNumber(entry.quantity, { money: false });
};
/**
diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts
index 3d17c699c..2bbe7e003 100644
--- a/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts
+++ b/packages/server/src/services/Sales/Invoices/SaleInvoicePdf.ts
@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
-import { ISaleInvoice } from '@/interfaces';
+import { GetSaleInvoice } from './GetSaleInvoice';
@Service()
export class SaleInvoicePdf {
@@ -11,16 +11,23 @@ export class SaleInvoicePdf {
@Inject()
private templateInjectable: TemplateInjectable;
+ @Inject()
+ private getInvoiceService: GetSaleInvoice;
+
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId - Tenant Id.
* @param {ISaleInvoice} saleInvoice -
* @returns {Promise}
*/
- async saleInvoicePdf(
+ public async saleInvoicePdf(
tenantId: number,
- saleInvoice: ISaleInvoice
+ invoiceId: number
): Promise {
+ const saleInvoice = await this.getInvoiceService.getSaleInvoice(
+ tenantId,
+ invoiceId
+ );
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/invoice-regular',
diff --git a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts
index 8a37386f9..bc3f8c24b 100644
--- a/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts
+++ b/packages/server/src/services/Sales/Invoices/SaleInvoicesApplication.ts
@@ -11,6 +11,7 @@ import {
ISystemUser,
ITenantUser,
InvoiceNotificationType,
+ SendInvoiceMailDTO,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { CreateSaleInvoice } from './CreateSaleInvoice';
@@ -24,6 +25,9 @@ import { WriteoffSaleInvoice } from './WriteoffSaleInvoice';
import { SaleInvoicePdf } from './SaleInvoicePdf';
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
+import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
+import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
+import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
@Service()
export class SaleInvoiceApplication {
@@ -60,6 +64,15 @@ export class SaleInvoiceApplication {
@Inject()
private invoiceSms: SaleInvoiceNotifyBySms;
+ @Inject()
+ private sendInvoiceReminderService: SendInvoiceMailReminder;
+
+ @Inject()
+ private sendSaleInvoiceMailService: SendSaleInvoiceMail;
+
+ @Inject()
+ private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
+
/**
* Creates a new sale invoice with associated GL entries.
* @param {number} tenantId
@@ -236,13 +249,13 @@ export class SaleInvoiceApplication {
};
/**
- *
- * @param {number} tenantId ]
- * @param saleInvoice
- * @returns
+ * Retrieves the pdf buffer of the given sale invoice.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} saleInvoice
+ * @returns {Promise}
*/
- public saleInvoicePdf(tenantId: number, saleInvoice) {
- return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice);
+ public saleInvoicePdf(tenantId: number, saleInvoiceId: number) {
+ return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoiceId);
}
/**
@@ -279,4 +292,67 @@ export class SaleInvoiceApplication {
invoiceSmsDetailsDTO
);
};
+
+ /**
+ * Retrieves the metadata of invoice mail reminder.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @returns {}
+ */
+ public getSaleInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {
+ return this.sendInvoiceReminderService.getMailOption(
+ tenantId,
+ saleInvoiceId
+ );
+ }
+
+ /**
+ * Sends reminder of the given invoice to the invoice's customer.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @returns {}
+ */
+ public sendSaleInvoiceMailReminder(
+ tenantId: number,
+ saleInvoiceId: number,
+ messageDTO: SendInvoiceMailDTO
+ ) {
+ return this.sendInvoiceReminderService.triggerMail(
+ tenantId,
+ saleInvoiceId,
+ messageDTO
+ );
+ }
+
+ /**
+ * Sends the invoice mail of the given sale invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @param {SendInvoiceMailDTO} messageDTO
+ * @returns {Promise}
+ */
+ public sendSaleInvoiceMail(
+ tenantId: number,
+ saleInvoiceId: number,
+ messageDTO: SendInvoiceMailDTO
+ ) {
+ return this.sendSaleInvoiceMailService.triggerMail(
+ tenantId,
+ saleInvoiceId,
+ messageDTO
+ );
+ }
+
+ /**
+ * Retrieves the default mail options of the given sale invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceid
+ * @returns {Promise}
+ */
+ public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) {
+ return this.sendSaleInvoiceMailService.getMailOption(
+ tenantId,
+ saleInvoiceid
+ );
+ }
}
diff --git a/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts
new file mode 100644
index 000000000..52ef46a59
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/SendInvoiceInvoiceMailCommon.ts
@@ -0,0 +1,83 @@
+import { Inject, Service } from 'typedi';
+import { SaleInvoiceMailOptions } from '@/interfaces';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+import { GetSaleInvoice } from './GetSaleInvoice';
+import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
+import {
+ DEFAULT_INVOICE_MAIL_CONTENT,
+ DEFAULT_INVOICE_MAIL_SUBJECT,
+} from './constants';
+
+@Service()
+export class SendSaleInvoiceMailCommon {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private getSaleInvoiceService: GetSaleInvoice;
+
+ @Inject()
+ private contactMailNotification: ContactMailNotification;
+
+ /**
+ * Retrieves the mail options.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} invoiceId - Invoice id.
+ * @param {string} defaultSubject - Subject text.
+ * @param {string} defaultBody - Subject body.
+ * @returns {Promise}
+ */
+ public async getMailOption(
+ tenantId: number,
+ invoiceId: number,
+ defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
+ defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT
+ ): Promise {
+ const { SaleInvoice } = this.tenancy.models(tenantId);
+
+ const saleInvoice = await SaleInvoice.query()
+ .findById(invoiceId)
+ .throwIfNotFound();
+
+ const formatterData = await this.formatText(tenantId, invoiceId);
+
+ const mailOptions = await this.contactMailNotification.getMailOptions(
+ tenantId,
+ saleInvoice.customerId,
+ defaultSubject,
+ defaultBody,
+ formatterData
+ );
+ return {
+ ...mailOptions,
+ attachInvoice: true,
+ };
+ }
+
+ /**
+ * Retrieves the formatted text of the given sale invoice.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} invoiceId - Sale invoice id.
+ * @param {string} text - The given text.
+ * @returns {Promise}
+ */
+ public formatText = async (
+ tenantId: number,
+ invoiceId: number
+ ): Promise> => {
+ const invoice = await this.getSaleInvoiceService.getSaleInvoice(
+ tenantId,
+ invoiceId
+ );
+
+ return {
+ CustomerName: invoice.customer.displayName,
+ InvoiceNumber: invoice.invoiceNo,
+ InvoiceDueAmount: invoice.dueAmountFormatted,
+ InvoiceDueDate: invoice.dueDateFormatted,
+ InvoiceDate: invoice.invoiceDateFormatted,
+ InvoiceAmount: invoice.totalFormatted,
+ OverdueDays: invoice.overdueDays,
+ };
+ };
+}
diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts
new file mode 100644
index 000000000..eff8b2603
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMail.ts
@@ -0,0 +1,121 @@
+import { Inject, Service } from 'typedi';
+import Mail from '@/lib/Mail';
+import { ISaleInvoiceMailSend, SendInvoiceMailDTO } from '@/interfaces';
+import { SaleInvoicePdf } from './SaleInvoicePdf';
+import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
+import {
+ DEFAULT_INVOICE_MAIL_CONTENT,
+ DEFAULT_INVOICE_MAIL_SUBJECT,
+} from './constants';
+import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
+import events from '@/subscribers/events';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+
+@Service()
+export class SendSaleInvoiceMail {
+ @Inject()
+ private invoicePdf: SaleInvoicePdf;
+
+ @Inject()
+ private invoiceMail: SendSaleInvoiceMailCommon;
+
+ @Inject('agenda')
+ private agenda: any;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ /**
+ * Sends the invoice mail of the given sale invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @param {SendInvoiceMailDTO} messageDTO
+ */
+ public async triggerMail(
+ tenantId: number,
+ saleInvoiceId: number,
+ messageOptions: SendInvoiceMailDTO
+ ) {
+ const payload = {
+ tenantId,
+ saleInvoiceId,
+ messageOptions,
+ };
+ await this.agenda.now('sale-invoice-mail-send', payload);
+
+ // Triggers the event `onSaleInvoicePreMailSend`.
+ await this.eventPublisher.emitAsync(events.saleInvoice.onPreMailSend, {
+ tenantId,
+ saleInvoiceId,
+ messageOptions,
+ } as ISaleInvoiceMailSend);
+ }
+
+ /**
+ * Retrieves the mail options of the given sale invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @returns {Promise}
+ */
+ public async getMailOption(tenantId: number, saleInvoiceId: number) {
+ return this.invoiceMail.getMailOption(
+ tenantId,
+ saleInvoiceId,
+ DEFAULT_INVOICE_MAIL_SUBJECT,
+ DEFAULT_INVOICE_MAIL_CONTENT
+ );
+ }
+
+ /**
+ * Triggers the mail invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @param {SendInvoiceMailDTO} messageDTO
+ * @returns {Promise}
+ */
+ public async sendMail(
+ tenantId: number,
+ saleInvoiceId: number,
+ messageOptions: SendInvoiceMailDTO
+ ) {
+ const defaultMessageOpts = await this.getMailOption(
+ tenantId,
+ saleInvoiceId
+ );
+ // Merge message opts with default options and validate the incoming options.
+ const messageOpts = parseAndValidateMailOptions(
+ defaultMessageOpts,
+ messageOptions
+ );
+ const mail = new Mail()
+ .setSubject(messageOpts.subject)
+ .setTo(messageOpts.to)
+ .setContent(messageOpts.body);
+
+ if (messageOpts.attachInvoice) {
+ // Retrieves document buffer of the invoice pdf document.
+ const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
+ tenantId,
+ saleInvoiceId
+ );
+ mail.setAttachments([
+ { filename: 'invoice.pdf', content: invoicePdfBuffer },
+ ]);
+ }
+ // Triggers the event `onSaleInvoiceSend`.
+ await this.eventPublisher.emitAsync(events.saleInvoice.onMailSend, {
+ tenantId,
+ saleInvoiceId,
+ messageOptions,
+ } as ISaleInvoiceMailSend);
+
+ await mail.send();
+
+ // Triggers the event `onSaleInvoiceSend`.
+ await this.eventPublisher.emitAsync(events.saleInvoice.onMailSent, {
+ tenantId,
+ saleInvoiceId,
+ messageOptions,
+ } as ISaleInvoiceMailSend);
+ }
+}
diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts
new file mode 100644
index 000000000..9de941f5f
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailJob.ts
@@ -0,0 +1,33 @@
+import Container, { Service } from 'typedi';
+import events from '@/subscribers/events';
+import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
+
+@Service()
+export class SendSaleInvoiceMailJob {
+ /**
+ * Constructor method.
+ */
+ constructor(agenda) {
+ agenda.define(
+ 'sale-invoice-mail-send',
+ { priority: 'high', concurrency: 2 },
+ this.handler
+ );
+ }
+
+ /**
+ * Triggers sending invoice mail.
+ */
+ private handler = async (job, done: Function) => {
+ const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
+ const sendInvoiceMail = Container.get(SendSaleInvoiceMail);
+
+ try {
+ await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
+ done();
+ } catch (error) {
+ console.log(error);
+ done(error);
+ }
+ };
+}
diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts
new file mode 100644
index 000000000..7829a3bf2
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminder.ts
@@ -0,0 +1,112 @@
+import { Inject, Service } from 'typedi';
+import {
+ ISaleInvoiceMailSend,
+ ISaleInvoiceMailSent,
+ SendInvoiceMailDTO,
+} from '@/interfaces';
+import Mail from '@/lib/Mail';
+import { SaleInvoicePdf } from './SaleInvoicePdf';
+import { SendSaleInvoiceMailCommon } from './SendInvoiceInvoiceMailCommon';
+import {
+ DEFAULT_INVOICE_REMINDER_MAIL_CONTENT,
+ DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
+} from './constants';
+import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import events from '@/subscribers/events';
+
+@Service()
+export class SendInvoiceMailReminder {
+ @Inject('agenda')
+ private agenda: any;
+
+ @Inject()
+ private invoicePdf: SaleInvoicePdf;
+
+ @Inject()
+ private invoiceCommonMail: SendSaleInvoiceMailCommon;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ /**
+ * Triggers the reminder mail of the given sale invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ */
+ public async triggerMail(
+ tenantId: number,
+ saleInvoiceId: number,
+ messageOptions: SendInvoiceMailDTO
+ ) {
+ const payload = {
+ tenantId,
+ saleInvoiceId,
+ messageOptions,
+ };
+ await this.agenda.now('sale-invoice-reminder-mail-send', payload);
+ }
+
+ /**
+ * Retrieves the mail options of the given sale invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @returns {Promise}
+ */
+ public async getMailOption(tenantId: number, saleInvoiceId: number) {
+ return this.invoiceCommonMail.getMailOption(
+ tenantId,
+ saleInvoiceId,
+ DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
+ DEFAULT_INVOICE_REMINDER_MAIL_CONTENT
+ );
+ }
+
+ /**
+ * Triggers the mail invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @param {SendInvoiceMailDTO} messageOptions
+ * @returns {Promise}
+ */
+ public async sendMail(
+ tenantId: number,
+ saleInvoiceId: number,
+ messageOptions: SendInvoiceMailDTO
+ ) {
+ const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId);
+
+ const messageOpts = parseAndValidateMailOptions(
+ localMessageOpts,
+ messageOptions
+ );
+ const mail = new Mail()
+ .setSubject(messageOpts.subject)
+ .setTo(messageOpts.to)
+ .setContent(messageOpts.body);
+
+ if (messageOpts.attachInvoice) {
+ // Retrieves document buffer of the invoice pdf document.
+ const invoicePdfBuffer = await this.invoicePdf.saleInvoicePdf(
+ tenantId,
+ saleInvoiceId
+ );
+ mail.setAttachments([
+ { filename: 'invoice.pdf', content: invoicePdfBuffer },
+ ]);
+ }
+ // Triggers the event `onSaleInvoiceSend`.
+ await this.eventPublisher.emitAsync(events.saleInvoice.onMailReminderSend, {
+ saleInvoiceId,
+ messageOptions,
+ } as ISaleInvoiceMailSend);
+
+ await mail.send();
+
+ // Triggers the event `onSaleInvoiceSent`.
+ await this.eventPublisher.emitAsync(events.saleInvoice.onMailReminderSent, {
+ saleInvoiceId,
+ messageOptions,
+ } as ISaleInvoiceMailSent);
+ }
+}
diff --git a/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts
new file mode 100644
index 000000000..6570a153f
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/SendSaleInvoiceMailReminderJob.ts
@@ -0,0 +1,32 @@
+import Container, { Service } from 'typedi';
+import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
+
+@Service()
+export class SendSaleInvoiceReminderMailJob {
+ /**
+ * Constructor method.
+ */
+ constructor(agenda) {
+ agenda.define(
+ 'sale-invoice-reminder-mail-send',
+ { priority: 'high', concurrency: 1 },
+ this.handler
+ );
+ }
+
+ /**
+ * Triggers sending invoice mail.
+ */
+ private handler = async (job, done: Function) => {
+ const { tenantId, saleInvoiceId, messageOptions } = job.attrs.data;
+ const sendInvoiceMail = Container.get(SendInvoiceMailReminder);
+
+ try {
+ await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageOptions);
+ done();
+ } catch (error) {
+ console.log(error);
+ done(error);
+ }
+ };
+}
diff --git a/packages/server/src/services/Sales/Invoices/constants.ts b/packages/server/src/services/Sales/Invoices/constants.ts
index 018dec027..404b7e613 100644
--- a/packages/server/src/services/Sales/Invoices/constants.ts
+++ b/packages/server/src/services/Sales/Invoices/constants.ts
@@ -1,3 +1,35 @@
+export const DEFAULT_INVOICE_MAIL_SUBJECT =
+ 'Invoice {InvoiceNumber} from {CompanyName}';
+export const DEFAULT_INVOICE_MAIL_CONTENT = `
+Dear {CustomerName}
+Thank you for your business, You can view or print your invoice from attachements.
+
+Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}
+
+
+
+Regards
+{CompanyName}
+
+`;
+
+export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =
+ 'Invoice {InvoiceNumber} reminder from {CompanyName}';
+export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
+Dear {CustomerName}
+You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.
+Invoice #{InvoiceNumber}
+Due Date : {InvoiceDueDate}
+Amount : {InvoiceAmount}
+
+
+Regards
+{CompanyName}
+
+`;
+
export const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
@@ -16,6 +48,7 @@ export const ERRORS = {
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_WRITTEN_OFF',
+ NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
};
export const DEFAULT_VIEW_COLUMNS = [];
diff --git a/packages/server/src/services/Sales/Invoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber.ts b/packages/server/src/services/Sales/Invoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber.ts
new file mode 100644
index 000000000..fc4392777
--- /dev/null
+++ b/packages/server/src/services/Sales/Invoices/subscribers/InvoiceChangeStatusOnMailSentSubscriber.ts
@@ -0,0 +1,49 @@
+import { Inject, Service } from 'typedi';
+import events from '@/subscribers/events';
+import { ISaleInvoiceMailSent } from '@/interfaces';
+import { DeliverSaleInvoice } from '../DeliverSaleInvoice';
+import { ServiceError } from '@/exceptions';
+import { ERRORS } from '../constants';
+
+@Service()
+export class InvoiceChangeStatusOnMailSentSubscriber {
+ @Inject()
+ private markInvoiceDelivedService: DeliverSaleInvoice;
+
+ /**
+ * Attaches events.
+ */
+ public attach(bus) {
+ bus.subscribe(events.saleInvoice.onPreMailSend, this.markInvoiceDelivered);
+ bus.subscribe(
+ events.saleInvoice.onMailReminderSent,
+ this.markInvoiceDelivered
+ );
+ }
+
+ /**
+ * Marks the invoice delivered once the invoice mail sent.
+ * @param {ISaleInvoiceMailSent}
+ * @returns {Promise}
+ */
+ private markInvoiceDelivered = async ({
+ tenantId,
+ saleInvoiceId,
+ messageOptions,
+ }: ISaleInvoiceMailSent) => {
+ try {
+ await this.markInvoiceDelivedService.deliverSaleInvoice(
+ tenantId,
+ saleInvoiceId
+ );
+ } catch (error) {
+ if (
+ error instanceof ServiceError &&
+ error.errorType === ERRORS.SALE_INVOICE_ALREADY_DELIVERED
+ ) {
+ } else {
+ throw error;
+ }
+ }
+ };
+}
diff --git a/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts b/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts
index b99413748..635f48946 100644
--- a/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts
+++ b/packages/server/src/services/Sales/PaymentReceives/EditPaymentReceive.ts
@@ -61,7 +61,8 @@ export class EditPaymentReceive {
// Validate the payment receive existance.
const oldPaymentReceive = await PaymentReceive.query()
.withGraphFetched('entries')
- .findById(paymentReceiveId);
+ .findById(paymentReceiveId)
+ .throwIfNotFound();
// Validates the payment existance.
this.validators.validatePaymentExistance(oldPaymentReceive);
diff --git a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts
index e05937f76..e3d3cfb26 100644
--- a/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts
+++ b/packages/server/src/services/Sales/PaymentReceives/GetPaymentReeceivePdf.ts
@@ -1,7 +1,7 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
-import { IPaymentReceive } from '@/interfaces';
+import { GetPaymentReceive } from './GetPaymentReceive';
@Service()
export default class GetPaymentReceivePdf {
@@ -11,6 +11,9 @@ export default class GetPaymentReceivePdf {
@Inject()
private templateInjectable: TemplateInjectable;
+ @Inject()
+ private getPaymentService: GetPaymentReceive;
+
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
@@ -19,8 +22,12 @@ export default class GetPaymentReceivePdf {
*/
async getPaymentReceivePdf(
tenantId: number,
- paymentReceive: IPaymentReceive
+ paymentReceiveId: number
): Promise {
+ const paymentReceive = await this.getPaymentService.getPaymentReceive(
+ tenantId,
+ paymentReceiveId
+ );
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/payment-receive-standard',
diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveEntryTransformer.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveEntryTransformer.ts
index fe9fe9b67..7a7af28c1 100644
--- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveEntryTransformer.ts
+++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveEntryTransformer.ts
@@ -8,7 +8,7 @@ export class PaymentReceiveEntryTransfromer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
- return ['paymentAmountFormatted', 'entry'];
+ return ['paymentAmountFormatted', 'invoice'];
};
/**
diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts
new file mode 100644
index 000000000..bd8d4fa64
--- /dev/null
+++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotification.ts
@@ -0,0 +1,141 @@
+import { Inject, Service } from 'typedi';
+import {
+ PaymentReceiveMailOpts,
+ PaymentReceiveMailOptsDTO,
+ PaymentReceiveMailPresendEvent,
+ SendInvoiceMailDTO,
+} from '@/interfaces';
+import Mail from '@/lib/Mail';
+import HasTenancyService from '@/services/Tenancy/TenancyService';
+import {
+ DEFAULT_PAYMENT_MAIL_CONTENT,
+ DEFAULT_PAYMENT_MAIL_SUBJECT,
+} from './constants';
+import { GetPaymentReceive } from './GetPaymentReceive';
+import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
+import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
+import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
+import events from '@/subscribers/events';
+
+@Service()
+export class SendPaymentReceiveMailNotification {
+ @Inject()
+ private tenancy: HasTenancyService;
+
+ @Inject()
+ private getPaymentService: GetPaymentReceive;
+
+ @Inject()
+ private contactMailNotification: ContactMailNotification;
+
+ @Inject('agenda')
+ private agenda: any;
+
+ @Inject()
+ private eventPublisher: EventPublisher;
+
+ /**
+ * Sends the mail of the given payment receive.
+ * @param {number} tenantId
+ * @param {number} paymentReceiveId
+ * @param {PaymentReceiveMailOptsDTO} messageDTO
+ * @returns {Promise}
+ */
+ public async triggerMail(
+ tenantId: number,
+ paymentReceiveId: number,
+ messageDTO: PaymentReceiveMailOptsDTO
+ ): Promise {
+ const payload = {
+ tenantId,
+ paymentReceiveId,
+ messageDTO,
+ };
+ await this.agenda.now('payment-receive-mail-send', payload);
+
+ // Triggers `onPaymentReceivePreMailSend` event.
+ await this.eventPublisher.emitAsync(events.paymentReceive.onPreMailSend, {
+ tenantId,
+ paymentReceiveId,
+ messageOptions: messageDTO,
+ } as PaymentReceiveMailPresendEvent);
+ }
+
+ /**
+ * Retrieves the default payment mail options.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} paymentReceiveId - Payment receive id.
+ * @returns {Promise}
+ */
+ public getMailOptions = async (
+ tenantId: number,
+ paymentId: number
+ ): Promise => {
+ const { PaymentReceive } = this.tenancy.models(tenantId);
+
+ const paymentReceive = await PaymentReceive.query()
+ .findById(paymentId)
+ .throwIfNotFound();
+
+ const formatterData = await this.textFormatter(tenantId, paymentId);
+
+ return this.contactMailNotification.getMailOptions(
+ tenantId,
+ paymentReceive.customerId,
+ DEFAULT_PAYMENT_MAIL_SUBJECT,
+ DEFAULT_PAYMENT_MAIL_CONTENT,
+ formatterData
+ );
+ };
+
+ /**
+ * Retrieves the formatted text of the given sale invoice.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} invoiceId - Sale invoice id.
+ * @param {string} text - The given text.
+ * @returns {Promise}
+ */
+ public textFormatter = async (
+ tenantId: number,
+ invoiceId: number
+ ): Promise> => {
+ const payment = await this.getPaymentService.getPaymentReceive(
+ tenantId,
+ invoiceId
+ );
+ return {
+ CustomerName: payment.customer.displayName,
+ PaymentNumber: payment.payment_receive_no,
+ PaymentDate: payment.formattedPaymentDate,
+ PaymentAmount: payment.formattedAmount,
+ };
+ };
+
+ /**
+ * Triggers the mail invoice.
+ * @param {number} tenantId
+ * @param {number} saleInvoiceId
+ * @param {SendInvoiceMailDTO} messageDTO
+ * @returns {Promise}
+ */
+ public async sendMail(
+ tenantId: number,
+ paymentReceiveId: number,
+ messageDTO: SendInvoiceMailDTO
+ ): Promise {
+ const defaultMessageOpts = await this.getMailOptions(
+ tenantId,
+ paymentReceiveId
+ );
+ // Parsed message opts with default options.
+ const parsedMessageOpts = parseAndValidateMailOptions(
+ defaultMessageOpts,
+ messageDTO
+ );
+ await new Mail()
+ .setSubject(parsedMessageOpts.subject)
+ .setTo(parsedMessageOpts.to)
+ .setContent(parsedMessageOpts.body)
+ .send();
+ }
+}
diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts
new file mode 100644
index 000000000..b29570d42
--- /dev/null
+++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob.ts
@@ -0,0 +1,32 @@
+import Container, { Service } from 'typedi';
+import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
+
+@Service()
+export class PaymentReceiveMailNotificationJob {
+ /**
+ * Constructor method.
+ */
+ constructor(agenda) {
+ agenda.define(
+ 'payment-receive-mail-send',
+ { priority: 'high', concurrency: 2 },
+ this.handler
+ );
+ }
+
+ /**
+ * Triggers sending payment notification via mail.
+ */
+ private handler = async (job, done: Function) => {
+ const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data;
+ const paymentMail = Container.get(SendPaymentReceiveMailNotification);
+
+ try {
+ await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO);
+ done();
+ } catch (error) {
+ console.log(error);
+ done(error);
+ }
+ };
+}
diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts
index afce4203b..5ca84db07 100644
--- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts
+++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceiveTransformer.ts
@@ -1,7 +1,6 @@
import { IPaymentReceive, IPaymentReceiveEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
-import { SaleInvoiceTransformer } from '../Invoices/SaleInvoiceTransformer';
import { PaymentReceiveEntryTransfromer } from './PaymentReceiveEntryTransformer';
export class PaymentReceiveTransfromer extends Transformer {
@@ -11,6 +10,7 @@ export class PaymentReceiveTransfromer extends Transformer {
*/
public includeAttributes = (): string[] => {
return [
+ 'subtotalFormatted',
'formattedPaymentDate',
'formattedAmount',
'formattedExchangeRate',
@@ -27,6 +27,18 @@ export class PaymentReceiveTransfromer extends Transformer {
return this.formatDate(payment.paymentDate);
};
+ /**
+ * Retrieve the formatted payment subtotal.
+ * @param {IPaymentReceive} payment
+ * @returns {string}
+ */
+ protected subtotalFormatted = (payment: IPaymentReceive): string => {
+ return formatNumber(payment.amount, {
+ currencyCode: payment.currencyCode,
+ money: false,
+ });
+ };
+
/**
* Retrieve formatted payment amount.
* @param {ISaleInvoice} invoice
diff --git a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts
index afeca6010..0d5669bf8 100644
--- a/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts
+++ b/packages/server/src/services/Sales/PaymentReceives/PaymentReceivesApplication.ts
@@ -7,6 +7,7 @@ import {
IPaymentReceiveSmsDetails,
IPaymentReceivesFilter,
ISystemUser,
+ PaymentReceiveMailOptsDTO,
} from '@/interfaces';
import { Inject, Service } from 'typedi';
import { CreatePaymentReceive } from './CreatePaymentReceive';
@@ -17,7 +18,7 @@ import { GetPaymentReceive } from './GetPaymentReceive';
import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices';
import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify';
import GetPaymentReceivePdf from './GetPaymentReeceivePdf';
-import { PaymentReceive } from '@/models';
+import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
@Service()
export class PaymentReceivesApplication {
@@ -42,6 +43,9 @@ export class PaymentReceivesApplication {
@Inject()
private paymentSmsNotify: PaymentReceiveNotifyBySms;
+ @Inject()
+ private paymentMailNotify: SendPaymentReceiveMailNotification;
+
@Inject()
private getPaymentReceivePdfService: GetPaymentReceivePdf;
@@ -176,18 +180,47 @@ export class PaymentReceivesApplication {
};
/**
- * Retrieve PDF content of the given payment receive.
+ * Notify customer via mail about payment receive details.
+ * @param {number} tenantId
+ * @param {number} paymentReceiveId
+ * @param {IPaymentReceiveMailOpts} messageOpts
+ * @returns {Promise}
+ */
+ public notifyPaymentByMail(
+ tenantId: number,
+ paymentReceiveId: number,
+ messageOpts: PaymentReceiveMailOptsDTO
+ ): Promise {
+ return this.paymentMailNotify.triggerMail(
+ tenantId,
+ paymentReceiveId,
+ messageOpts
+ );
+ }
+
+ /**
+ * Retrieves the default mail options of the given payment transaction.
+ * @param {number} tenantId
+ * @param {number} paymentReceiveId
+ * @returns {Promise