mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 14:20:31 +00:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c9810ea4c | ||
|
|
1e4b29f83c | ||
|
|
2b03ac7f16 | ||
|
|
2cb8c2932f | ||
|
|
2814d23f75 | ||
|
|
35fa2389e1 | ||
|
|
c18b450129 | ||
|
|
8db6e9f049 | ||
|
|
b63a2006d3 | ||
|
|
8c1ca503bd | ||
|
|
0ac79e661c | ||
|
|
66ba4d35aa | ||
|
|
46d25dff4c | ||
|
|
5ef99f2cb3 | ||
|
|
7aee76e461 | ||
|
|
79f3f1b63d | ||
|
|
26e104b9f1 | ||
|
|
c71836ec27 | ||
|
|
60b1bc9ed7 | ||
|
|
e6a3daa2c3 | ||
|
|
276ef1c907 | ||
|
|
83e48cce42 | ||
|
|
99ca683d13 | ||
|
|
5062d891e1 | ||
|
|
92d1fc670b | ||
|
|
ab7abfea35 | ||
|
|
0d15c16d40 | ||
|
|
2a85fe2f3c | ||
|
|
dc762567b5 | ||
|
|
a676e09537 | ||
|
|
c46948049c | ||
|
|
de1b7f132c | ||
|
|
90b552d5c9 | ||
|
|
39115b3000 | ||
|
|
3c8c3d8253 | ||
|
|
657400c671 | ||
|
|
6356cb5e63 | ||
|
|
13c6e7a62d | ||
|
|
b6d99b1d4b | ||
|
|
f0e15d43d3 | ||
|
|
50d5ddba8e | ||
|
|
d2c63878ed | ||
|
|
ef52eaf91a | ||
|
|
cd71900bdd | ||
|
|
34e97fc56b | ||
|
|
e6e119682b | ||
|
|
e096db2ab8 | ||
|
|
cfd4540a65 | ||
|
|
9531730d7a |
@@ -69,6 +69,24 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
52
CHANGELOG.md
52
CHANGELOG.md
@@ -2,11 +2,49 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
All notable changes to Bigcapital server-side will be in this file.
|
||||||
|
|
||||||
# [0.10.1] - 25-09-2023
|
## [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
|
* 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: 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
|
* Added: Sales Tax Liability Summary report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||||
@@ -16,7 +54,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 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
|
* 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)
|
* 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
|
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
|
||||||
@@ -30,18 +68,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
|
* 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
|
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
|
* 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
|
* 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
|
* 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): 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.
|
* 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
|
* 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
|
* chore: Move auto-increment components in separate files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/170
|
||||||
@@ -53,7 +91,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 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
|
* 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`
|
`bigcapitalhq/webapp`
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
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)
|
[](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
|
# Resources
|
||||||
|
|
||||||
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
|
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
|
||||||
@@ -107,6 +113,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ import { Router, Request, Response, NextFunction } from 'express';
|
|||||||
import { query, ValidationChain } from 'express-validator';
|
import { query, ValidationChain } from 'express-validator';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService';
|
|
||||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
import { GeneralLedgerApplication } from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GeneralLedgerReportController extends BaseFinancialReportController {
|
export default class GeneralLedgerReportController extends BaseFinancialReportController {
|
||||||
@Inject()
|
@Inject()
|
||||||
generalLedgetService: GeneralLedgerService;
|
private generalLedgerApplication: GeneralLedgerApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
@@ -61,20 +62,43 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
|
|||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
*/
|
*/
|
||||||
async generalLedger(req: Request, res: Response, next: NextFunction) {
|
async generalLedger(req: Request, res: Response, next: NextFunction) {
|
||||||
const { tenantId, settings } = req;
|
const { tenantId } = req;
|
||||||
const filter = this.matchedQueryData(req);
|
const filter = this.matchedQueryData(req);
|
||||||
|
const accept = this.accepts(req);
|
||||||
|
|
||||||
try {
|
const acceptType = accept.types([
|
||||||
const { data, query, meta } =
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||||
|
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||||
|
ACCEPT_TYPE.APPLICATION_CSV,
|
||||||
|
]);
|
||||||
|
// Retrieves the table format.
|
||||||
|
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||||
|
const table = await this.generalLedgerApplication.table(tenantId, filter);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send(table);
|
||||||
meta: this.transfromToResponse(meta),
|
// Retrieves the csv format.
|
||||||
data: this.transfromToResponse(data),
|
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||||
query: this.transfromToResponse(query),
|
const buffer = await this.generalLedgerApplication.csv(tenantId, filter);
|
||||||
});
|
|
||||||
} catch (error) {
|
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||||
next(error);
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the xlsx format.
|
||||||
|
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||||
|
const buffer = await this.generalLedgerApplication.xlsx(tenantId, filter);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
);
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the json format.
|
||||||
|
} else {
|
||||||
|
const sheet = await this.generalLedgerApplication.sheet(tenantId, filter);
|
||||||
|
return res.status(200).send(sheet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,15 @@ import { Request, Response, Router, NextFunction } from 'express';
|
|||||||
import { castArray } from 'lodash';
|
import { castArray } from 'lodash';
|
||||||
import { query, oneOf } from 'express-validator';
|
import { query, oneOf } from 'express-validator';
|
||||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||||
import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService';
|
|
||||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
import { JournalSheetApplication } from '@/services/FinancialStatements/JournalSheet/JournalSheetApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class JournalSheetController extends BaseFinancialReportController {
|
export default class JournalSheetController extends BaseFinancialReportController {
|
||||||
@Inject()
|
@Inject()
|
||||||
journalService: JournalSheetService;
|
private journalSheetApp: JournalSheetApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
@@ -57,28 +58,49 @@ export default class JournalSheetController extends BaseFinancialReportControlle
|
|||||||
* @param {Request} req -
|
* @param {Request} req -
|
||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
*/
|
*/
|
||||||
async journal(req: Request, res: Response, next: NextFunction) {
|
private async journal(req: Request, res: Response, next: NextFunction) {
|
||||||
const { tenantId, settings } = req;
|
const { tenantId } = req;
|
||||||
let filter = this.matchedQueryData(req);
|
let filter = this.matchedQueryData(req);
|
||||||
|
|
||||||
filter = {
|
filter = {
|
||||||
...filter,
|
...filter,
|
||||||
accountsIds: castArray(filter.accountsIds),
|
accountsIds: castArray(filter.accountsIds),
|
||||||
};
|
};
|
||||||
|
const accept = this.accepts(req);
|
||||||
|
const acceptType = accept.types([
|
||||||
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
|
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||||
|
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||||
|
ACCEPT_TYPE.APPLICATION_CSV,
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
// Retrieves the json table format.
|
||||||
const { data, query, meta } = await this.journalService.journalSheet(
|
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
|
||||||
tenantId,
|
const table = await this.journalSheetApp.table(tenantId, filter);
|
||||||
filter
|
return res.status(200).send(table);
|
||||||
|
// Retrieves the csv format.
|
||||||
|
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
|
||||||
|
const buffer = await this.journalSheetApp.csv(tenantId, filter);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
|
||||||
|
res.setHeader('Content-Type', 'text/csv');
|
||||||
|
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the xlsx format.
|
||||||
|
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
|
||||||
|
const buffer = await this.journalSheetApp.xlsx(tenantId, filter);
|
||||||
|
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Type',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
);
|
);
|
||||||
|
return res.send(buffer);
|
||||||
|
// Retrieves the json format.
|
||||||
|
} else {
|
||||||
|
const sheet = await this.journalSheetApp.sheet(tenantId, filter);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send(sheet);
|
||||||
data: this.transfromToResponse(data),
|
|
||||||
query: this.transfromToResponse(query),
|
|
||||||
meta: this.transfromToResponse(meta),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
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 {
|
import {
|
||||||
AbilitySubject,
|
AbilitySubject,
|
||||||
IPaymentReceiveDTO,
|
IPaymentReceiveDTO,
|
||||||
PaymentReceiveAction,
|
PaymentReceiveAction,
|
||||||
|
PaymentReceiveMailOptsDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
@@ -117,6 +118,25 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
asyncMiddleware(this.deletePaymentReceive.bind(this)),
|
asyncMiddleware(this.deletePaymentReceive.bind(this)),
|
||||||
this.handleServiceErrors
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,27 +436,26 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
const { id: paymentReceiveId } = req.params;
|
const { id: paymentReceiveId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const paymentReceive =
|
|
||||||
await this.paymentReceiveApplication.getPaymentReceive(
|
|
||||||
tenantId,
|
|
||||||
paymentReceiveId
|
|
||||||
);
|
|
||||||
|
|
||||||
const ACCEPT_TYPE = {
|
const ACCEPT_TYPE = {
|
||||||
APPLICATION_PDF: 'application/pdf',
|
APPLICATION_PDF: 'application/pdf',
|
||||||
APPLICATION_JSON: 'application/json',
|
APPLICATION_JSON: 'application/json',
|
||||||
};
|
};
|
||||||
res.format({
|
res.format({
|
||||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
||||||
|
const paymentReceive =
|
||||||
|
await this.paymentReceiveApplication.getPaymentReceive(
|
||||||
|
tenantId,
|
||||||
|
paymentReceiveId
|
||||||
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
payment_receive: this.transfromToResponse(paymentReceive),
|
payment_receive: paymentReceive,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||||
const pdfContent =
|
const pdfContent =
|
||||||
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
||||||
tenantId,
|
tenantId,
|
||||||
paymentReceive
|
paymentReceiveId
|
||||||
);
|
);
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/pdf',
|
'Content-Type': 'application/pdf',
|
||||||
@@ -507,6 +526,66 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends mail invoice of the given sale invoice.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {NextFunction} next
|
||||||
|
*/
|
||||||
|
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.
|
* Handles service errors.
|
||||||
* @param error
|
* @param error
|
||||||
@@ -514,7 +593,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
* @param res
|
* @param res
|
||||||
* @param next
|
* @param next
|
||||||
*/
|
*/
|
||||||
handleServiceErrors(
|
private handleServiceErrors(
|
||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
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 { Inject, Service } from 'typedi';
|
||||||
import {
|
import {
|
||||||
AbilitySubject,
|
AbilitySubject,
|
||||||
ISaleEstimateDTO,
|
ISaleEstimateDTO,
|
||||||
SaleEstimateAction,
|
SaleEstimateAction,
|
||||||
|
SaleEstimateMailOptionsDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
@@ -121,6 +122,27 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
this.handleServiceErrors,
|
this.handleServiceErrors,
|
||||||
this.dynamicListService.handlerErrorsToResponse
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,22 +384,22 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
|
||||||
tenantId,
|
|
||||||
estimateId
|
|
||||||
);
|
|
||||||
// Response formatter.
|
// Response formatter.
|
||||||
res.format({
|
res.format({
|
||||||
// JSON content type.
|
// JSON content type.
|
||||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
||||||
return res.status(200).send(this.transfromToResponse({ estimate }));
|
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
||||||
|
tenantId,
|
||||||
|
estimateId
|
||||||
|
);
|
||||||
|
return res.status(200).send({ estimate });
|
||||||
},
|
},
|
||||||
// PDF content type.
|
// PDF content type.
|
||||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
||||||
const pdfContent =
|
const pdfContent =
|
||||||
await this.saleEstimatesApplication.getSaleEstimatePdf(
|
await this.saleEstimatesApplication.getSaleEstimatePdf(
|
||||||
tenantId,
|
tenantId,
|
||||||
estimate
|
estimateId
|
||||||
);
|
);
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/pdf',
|
'Content-Type': 'application/pdf',
|
||||||
@@ -478,6 +500,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.
|
* Handles service errors.
|
||||||
* @param {Error} error
|
* @param {Error} error
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
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 { Service, Inject } from 'typedi';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ISaleInvoiceCreateDTO,
|
ISaleInvoiceCreateDTO,
|
||||||
SaleInvoiceAction,
|
SaleInvoiceAction,
|
||||||
AbilitySubject,
|
AbilitySubject,
|
||||||
|
SendInvoiceMailDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
|
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
|
||||||
@@ -145,6 +146,48 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
this.handleServiceErrors,
|
this.handleServiceErrors,
|
||||||
this.dynamicListService.handlerErrorsToResponse
|
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;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,6 +673,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.
|
* Handles service errors.
|
||||||
* @param {Error} error
|
* @param {Error} error
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
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 { Inject, Service } from 'typedi';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
|
import { ISaleReceiptDTO, SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces/SaleReceipt';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
@@ -46,6 +46,29 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
this.saleReceiptSmsDetails,
|
this.saleReceiptSmsDetails,
|
||||||
this.handleServiceErrors
|
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(
|
router.post(
|
||||||
'/:id',
|
'/:id',
|
||||||
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
|
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
|
||||||
@@ -314,26 +337,24 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
public async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
||||||
const { id: saleReceiptId } = req.params;
|
const { id: saleReceiptId } = req.params;
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
|
||||||
tenantId,
|
|
||||||
saleReceiptId
|
|
||||||
);
|
|
||||||
res.format({
|
res.format({
|
||||||
'application/json': () => {
|
'application/json': async () => {
|
||||||
return res
|
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
||||||
.status(200)
|
tenantId,
|
||||||
.send(this.transfromToResponse({ saleReceipt }));
|
saleReceiptId
|
||||||
|
);
|
||||||
|
return res.status(200).send({ saleReceipt });
|
||||||
},
|
},
|
||||||
'application/pdf': async () => {
|
'application/pdf': async () => {
|
||||||
const pdfContent =
|
const pdfContent =
|
||||||
await this.saleReceiptsApplication.getSaleReceiptPdf(
|
await this.saleReceiptsApplication.getSaleReceiptPdf(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleReceipt
|
saleReceiptId
|
||||||
);
|
);
|
||||||
res.set({
|
res.set({
|
||||||
'Content-Type': 'application/pdf',
|
'Content-Type': 'application/pdf',
|
||||||
@@ -405,6 +426,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.
|
* Handles service errors.
|
||||||
* @param {Error} error
|
* @param {Error} error
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ module.exports = {
|
|||||||
secure: !!parseInt(process.env.MAIL_SECURE, 10),
|
secure: !!parseInt(process.env.MAIL_SECURE, 10),
|
||||||
username: process.env.MAIL_USERNAME,
|
username: process.env.MAIL_USERNAME,
|
||||||
password: process.env.MAIL_PASSWORD,
|
password: process.env.MAIL_PASSWORD,
|
||||||
|
from: process.env.MAIL_FROM_ADDRESS,
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { IFinancialTable } from "./Table";
|
||||||
|
|
||||||
|
|
||||||
export interface IGeneralLedgerSheetQuery {
|
export interface IGeneralLedgerSheetQuery {
|
||||||
@@ -36,6 +37,7 @@ export interface IGeneralLedgerSheetAccountTransaction {
|
|||||||
referenceType?: string,
|
referenceType?: string,
|
||||||
|
|
||||||
date: Date|string,
|
date: Date|string,
|
||||||
|
dateFormatted: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IGeneralLedgerSheetAccountBalance {
|
export interface IGeneralLedgerSheetAccountBalance {
|
||||||
@@ -56,6 +58,8 @@ export interface IGeneralLedgerSheetAccount {
|
|||||||
closingBalance: IGeneralLedgerSheetAccountBalance,
|
closingBalance: IGeneralLedgerSheetAccountBalance,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];
|
||||||
|
|
||||||
export interface IAccountTransaction {
|
export interface IAccountTransaction {
|
||||||
id: number,
|
id: number,
|
||||||
index: number,
|
index: number,
|
||||||
@@ -78,4 +82,11 @@ export interface IGeneralLedgerMeta {
|
|||||||
isCostComputeRunning: boolean,
|
isCostComputeRunning: boolean,
|
||||||
organizationName: string,
|
organizationName: string,
|
||||||
baseCurrency: string,
|
baseCurrency: string,
|
||||||
};
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IGeneralLedgerTableData extends IFinancialTable {
|
||||||
|
meta: IGeneralLedgerMeta;
|
||||||
|
query: IGeneralLedgerSheetQuery;
|
||||||
|
}
|
||||||
@@ -1,36 +1,52 @@
|
|||||||
import { IJournalEntry } from './Journal';
|
import { IJournalEntry } from './Journal';
|
||||||
|
import { IFinancialTable } from './Table';
|
||||||
|
|
||||||
export interface IJournalReportQuery {
|
export interface IJournalReportQuery {
|
||||||
fromDate: Date | string,
|
fromDate: Date | string;
|
||||||
toDate: Date | string,
|
toDate: Date | string;
|
||||||
numberFormat: {
|
numberFormat: {
|
||||||
noCents: boolean,
|
noCents: boolean;
|
||||||
divideOn1000: boolean,
|
divideOn1000: boolean;
|
||||||
},
|
};
|
||||||
transactionType: string,
|
transactionType: string;
|
||||||
transactionId: string,
|
transactionId: string;
|
||||||
|
|
||||||
accountsIds: number | number[],
|
accountsIds: number | number[];
|
||||||
fromRange: number,
|
fromRange: number;
|
||||||
toRange: number,
|
toRange: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IJournalReportEntriesGroup {
|
export interface IJournalReportEntriesGroup {
|
||||||
id: string,
|
id: string;
|
||||||
entries: IJournalEntry[],
|
date: Date;
|
||||||
currencyCode: string,
|
dateFormatted: string;
|
||||||
credit: number,
|
entries: IJournalEntry[];
|
||||||
debit: number,
|
currencyCode: string;
|
||||||
formattedCredit: string,
|
credit: number;
|
||||||
formattedDebit: string,
|
debit: number;
|
||||||
|
formattedCredit: string;
|
||||||
|
formattedDebit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IJournalReport {
|
export interface IJournalReport {
|
||||||
entries: IJournalReportEntriesGroup[],
|
entries: IJournalReportEntriesGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IJournalSheetMeta {
|
export interface IJournalSheetMeta {
|
||||||
isCostComputeRunning: boolean,
|
isCostComputeRunning: boolean;
|
||||||
organizationName: string,
|
organizationName: string;
|
||||||
baseCurrency: string,
|
baseCurrency: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IJournalTable extends IFinancialTable {
|
||||||
|
query: IJournalReportQuery;
|
||||||
|
meta: IJournalSheetMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IJournalTableData = IJournalReportEntriesGroup[];
|
||||||
|
|
||||||
|
export interface IJournalSheet {
|
||||||
|
data: IJournalTableData;
|
||||||
|
query: IJournalReportQuery;
|
||||||
|
meta: IJournalSheetMeta;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
export interface IMailable {
|
||||||
constructor(
|
constructor(view: string, data?: { [key: string]: string | number });
|
||||||
view: string,
|
|
||||||
data?: { [key: string]: string | number },
|
|
||||||
);
|
|
||||||
send(): Promise<any>;
|
send(): Promise<any>;
|
||||||
build(): void;
|
build(): void;
|
||||||
setData(data: { [key: string]: string | number }): IMailable;
|
setData(data: { [key: string]: string | number }): IMailable;
|
||||||
@@ -13,4 +21,27 @@ export interface IMailable {
|
|||||||
setView(view: string): IMailable;
|
setView(view: string): IMailable;
|
||||||
render(data?: { [key: string]: string | number }): string;
|
render(data?: { [key: string]: string | number }): string;
|
||||||
getViewContent(): string;
|
getViewContent(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AddressItem {
|
||||||
|
label: string;
|
||||||
|
mail: string;
|
||||||
|
primary?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonMailOptions {
|
||||||
|
toAddresses: AddressItem[];
|
||||||
|
fromAddresses: AddressItem[];
|
||||||
|
from: string;
|
||||||
|
to: string | string[];
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommonMailOptionsDTO {
|
||||||
|
to?: string | string[];
|
||||||
|
from?: string;
|
||||||
|
subject?: string;
|
||||||
|
body?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ISystemUser } from '@/interfaces';
|
import {
|
||||||
|
CommonMailOptions,
|
||||||
|
CommonMailOptionsDTO,
|
||||||
|
ISystemUser,
|
||||||
|
} from '@/interfaces';
|
||||||
import { ILedgerEntry } from './Ledger';
|
import { ILedgerEntry } from './Ledger';
|
||||||
import { ISaleInvoice } from './SaleInvoice';
|
import { ISaleInvoice } from './SaleInvoice';
|
||||||
|
|
||||||
@@ -19,7 +23,7 @@ export interface IPaymentReceive {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
localAmount?: number;
|
localAmount?: number;
|
||||||
branchId?: number
|
branchId?: number;
|
||||||
}
|
}
|
||||||
export interface IPaymentReceiveCreateDTO {
|
export interface IPaymentReceiveCreateDTO {
|
||||||
customerId: number;
|
customerId: number;
|
||||||
@@ -165,3 +169,7 @@ export type IPaymentReceiveGLCommonEntry = Pick<
|
|||||||
| 'createdAt'
|
| 'createdAt'
|
||||||
| 'branchId'
|
| 'branchId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
|
||||||
|
|
||||||
|
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||||
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||||
|
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||||
|
|
||||||
export interface ISaleEstimate {
|
export interface ISaleEstimate {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -124,3 +125,11 @@ export interface ISaleEstimateApprovedEvent {
|
|||||||
saleEstimate: ISaleEstimate;
|
saleEstimate: ISaleEstimate;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SaleEstimateMailOptions extends CommonMailOptions {
|
||||||
|
attachEstimate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
|
||||||
|
attachEstimate?: boolean;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
|
import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
|
||||||
|
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||||
import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
|
import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
|
||||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||||
|
|
||||||
@@ -186,3 +187,17 @@ export enum SaleInvoiceAction {
|
|||||||
Writeoff = 'Writeoff',
|
Writeoff = 'Writeoff',
|
||||||
NotifyBySms = 'NotifyBySms',
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { IItemEntry } from './ItemEntry';
|
import { IItemEntry } from './ItemEntry';
|
||||||
|
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||||
|
|
||||||
export interface ISaleReceipt {
|
export interface ISaleReceipt {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -134,3 +135,11 @@ export interface ISaleReceiptDeletingPayload {
|
|||||||
oldSaleReceipt: ISaleReceipt;
|
oldSaleReceipt: ISaleReceipt;
|
||||||
trx: Knex.Transaction;
|
trx: Knex.Transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SaleReceiptMailOpts extends CommonMailOptions {
|
||||||
|
attachReceipt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SaleReceiptMailOptsDTO extends CommonMailOptionsDTO {
|
||||||
|
attachReceipt?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ import fs from 'fs';
|
|||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { IMailable } from '@/interfaces';
|
import { IMailAttachment } from '@/interfaces';
|
||||||
|
|
||||||
interface IMailAttachment {
|
|
||||||
filename: string;
|
|
||||||
path: string;
|
|
||||||
cid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Mail {
|
export default class Mail {
|
||||||
view: string;
|
view: string;
|
||||||
subject: string;
|
subject: string = '';
|
||||||
to: string;
|
content: string = '';
|
||||||
|
to: string | string[];
|
||||||
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
|
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
|
||||||
data: { [key: string]: string | number };
|
data: { [key: string]: string | number };
|
||||||
attachments: IMailAttachment[];
|
attachments: IMailAttachment[];
|
||||||
@@ -21,16 +16,24 @@ export default class Mail {
|
|||||||
/**
|
/**
|
||||||
* Mail options.
|
* Mail options.
|
||||||
*/
|
*/
|
||||||
private get mailOptions() {
|
public get mailOptions() {
|
||||||
return {
|
return {
|
||||||
to: this.to,
|
to: this.to,
|
||||||
from: this.from,
|
from: this.from,
|
||||||
subject: this.subject,
|
subject: this.subject,
|
||||||
html: this.render(this.data),
|
html: this.html,
|
||||||
attachments: this.attachments,
|
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.
|
* Sends the given mail to the target address.
|
||||||
*/
|
*/
|
||||||
@@ -52,7 +55,7 @@ export default class Mail {
|
|||||||
* Set send mail to address.
|
* Set send mail to address.
|
||||||
* @param {string} to -
|
* @param {string} to -
|
||||||
*/
|
*/
|
||||||
setTo(to: string) {
|
setTo(to: string | string[]) {
|
||||||
this.to = to;
|
this.to = to;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -62,11 +65,16 @@ export default class Mail {
|
|||||||
* @param {string} from
|
* @param {string} from
|
||||||
* @return {}
|
* @return {}
|
||||||
*/
|
*/
|
||||||
private setFrom(from: string) {
|
setFrom(from: string) {
|
||||||
this.from = from;
|
this.from = from;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set attachments to the mail.
|
||||||
|
* @param {IMailAttachment[]} attachments
|
||||||
|
* @returns {Mail}
|
||||||
|
*/
|
||||||
setAttachments(attachments: IMailAttachment[]) {
|
setAttachments(attachments: IMailAttachment[]) {
|
||||||
this.attachments = attachments;
|
this.attachments = attachments;
|
||||||
return this;
|
return this;
|
||||||
@@ -95,21 +103,26 @@ export default class Mail {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setContent(content: string) {
|
||||||
|
this.content = content;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the view template with the given data.
|
* Renders the view template with the given data.
|
||||||
* @param {object} data
|
* @param {object} data
|
||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
render(data): string {
|
static render(view: string, data: Record<string, any>): string {
|
||||||
const viewContent = this.getViewContent();
|
const viewContent = Mail.getViewContent(view);
|
||||||
return Mustache.render(viewContent, data);
|
return Mustache.render(viewContent, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve view content from the view directory.
|
* Retrieve view content from the view directory.
|
||||||
*/
|
*/
|
||||||
private getViewContent(): string {
|
static getViewContent(view: string): string {
|
||||||
const filePath = path.join(global.__views_dir, `/${this.view}`);
|
const filePath = path.join(global.__views_dir, `/${view}`);
|
||||||
return fs.readFileSync(filePath, 'utf8');
|
return fs.readFileSync(filePath, 'utf8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import RewriteInvoicesJournalEntries from 'jobs/WriteInvoicesJEntries';
|
|||||||
import UserInviteMailJob from 'jobs/UserInviteMail';
|
import UserInviteMailJob from 'jobs/UserInviteMail';
|
||||||
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
import OrganizationSetupJob from 'jobs/OrganizationSetup';
|
||||||
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
import OrganizationUpgrade from 'jobs/OrganizationUpgrade';
|
||||||
|
import { SendSaleInvoiceMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailJob';
|
||||||
|
import { SendSaleInvoiceReminderMailJob } from '@/services/Sales/Invoices/SendSaleInvoiceMailReminderJob';
|
||||||
|
import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEstimateMailJob';
|
||||||
|
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
|
||||||
|
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||||
|
|
||||||
export default ({ agenda }: { agenda: Agenda }) => {
|
export default ({ agenda }: { agenda: Agenda }) => {
|
||||||
new ResetPasswordMailJob(agenda);
|
new ResetPasswordMailJob(agenda);
|
||||||
@@ -13,6 +18,11 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
|||||||
new RewriteInvoicesJournalEntries(agenda);
|
new RewriteInvoicesJournalEntries(agenda);
|
||||||
new OrganizationSetupJob(agenda);
|
new OrganizationSetupJob(agenda);
|
||||||
new OrganizationUpgrade(agenda);
|
new OrganizationUpgrade(agenda);
|
||||||
|
new SendSaleInvoiceMailJob(agenda);
|
||||||
|
new SendSaleInvoiceReminderMailJob(agenda);
|
||||||
|
new SendSaleEstimateMailJob(agenda);
|
||||||
|
new SaleReceiptMailNotificationJob(agenda);
|
||||||
|
new PaymentReceiveMailNotificationJob(agenda);
|
||||||
|
|
||||||
agenda.start();
|
agenda.start();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { Model } from 'objection';
|
|||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
export default class Contact extends TenantModel {
|
export default class Contact extends TenantModel {
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export default class Customer extends mixin(TenantModel, [
|
|||||||
CustomViewBaseModel,
|
CustomViewBaseModel,
|
||||||
ModelSearchable,
|
ModelSearchable,
|
||||||
]) {
|
]) {
|
||||||
|
email: string;
|
||||||
|
displayName: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query builder.
|
* Query builder.
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +79,19 @@ export default class Customer extends mixin(TenantModel, [
|
|||||||
return 'debit';
|
return 'debit';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
get contactAddresses() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
mail: this.email,
|
||||||
|
label: this.displayName,
|
||||||
|
primary: true
|
||||||
|
},
|
||||||
|
].filter((c) => c.mail);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
IContact,
|
IContact,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General ledger sheet.
|
* General ledger sheet.
|
||||||
@@ -88,8 +89,10 @@ export default class GeneralLedgerSheet extends FinancialSheet {
|
|||||||
|
|
||||||
const newEntry = {
|
const newEntry = {
|
||||||
date: entry.date,
|
date: entry.date,
|
||||||
|
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
|
||||||
entryId: entry.id,
|
entryId: entry.id,
|
||||||
|
|
||||||
|
transactionNumber: entry.transactionNumber,
|
||||||
referenceType: entry.referenceType,
|
referenceType: entry.referenceType,
|
||||||
referenceId: entry.referenceId,
|
referenceId: entry.referenceId,
|
||||||
referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted),
|
referenceTypeFormatted: this.i18n.__(entry.referenceTypeFormatted),
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Inject } from 'typedi';
|
||||||
|
import {
|
||||||
|
IGeneralLedgerSheetQuery,
|
||||||
|
IGeneralLedgerTableData,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
|
||||||
|
import { GeneralLedgerExportInjectable } from './GeneralLedgerExport';
|
||||||
|
import { GeneralLedgerService } from './GeneralLedgerService';
|
||||||
|
|
||||||
|
export class GeneralLedgerApplication {
|
||||||
|
@Inject()
|
||||||
|
private GLTable: GeneralLedgerTableInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private GLExport: GeneralLedgerExportInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private GLSheet: GeneralLedgerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the G/L sheet in json format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
|
*/
|
||||||
|
public sheet(tenantId: number, query: IGeneralLedgerSheetQuery) {
|
||||||
|
return this.GLSheet.generalLedger(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the G/L sheet in table format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
|
* @returns {Promise<IGeneralLedgerTableData>}
|
||||||
|
*/
|
||||||
|
public table(
|
||||||
|
tenantId: number,
|
||||||
|
query: IGeneralLedgerSheetQuery
|
||||||
|
): Promise<IGeneralLedgerTableData> {
|
||||||
|
return this.GLTable.table(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the G/L sheet in xlsx format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public xlsx(
|
||||||
|
tenantId: number,
|
||||||
|
query: IGeneralLedgerSheetQuery
|
||||||
|
): Promise<Buffer> {
|
||||||
|
return this.GLExport.xlsx(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the G/L sheet in csv format.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query -
|
||||||
|
*/
|
||||||
|
public csv(
|
||||||
|
tenantId: number,
|
||||||
|
query: IGeneralLedgerSheetQuery
|
||||||
|
): Promise<string> {
|
||||||
|
return this.GLExport.csv(tenantId, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { IGeneralLedgerSheetQuery } from '@/interfaces';
|
||||||
|
import { TableSheet } from '@/lib/Xlsx/TableSheet';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { GeneralLedgerTableInjectable } from './GeneralLedgerTableInjectable';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GeneralLedgerExportInjectable {
|
||||||
|
@Inject()
|
||||||
|
private generalLedgerTable: GeneralLedgerTableInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the general ledger sheet in XLSX format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async xlsx(tenantId: number, query: IGeneralLedgerSheetQuery) {
|
||||||
|
const table = await this.generalLedgerTable.table(tenantId, query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToXLSX();
|
||||||
|
|
||||||
|
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the general ledger sheet in CSV format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async csv(
|
||||||
|
tenantId: number,
|
||||||
|
query: IGeneralLedgerSheetQuery
|
||||||
|
): Promise<string> {
|
||||||
|
const table = await this.generalLedgerTable.table(tenantId, query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToCSV();
|
||||||
|
|
||||||
|
return tableCsv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ const ERRORS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GeneralLedgerService {
|
export class GeneralLedgerService {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
tenancy: TenancyService;
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export default class GeneralLedgerService {
|
|||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @returns {IGeneralLedgerMeta}
|
* @returns {IGeneralLedgerMeta}
|
||||||
*/
|
*/
|
||||||
reportMetadata(tenantId: number): IGeneralLedgerMeta {
|
reportMetadata(tenantId: number, filter): IGeneralLedgerMeta {
|
||||||
const settings = this.tenancy.settings(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
const isCostComputeRunning = this.inventoryService
|
const isCostComputeRunning = this.inventoryService
|
||||||
@@ -78,11 +78,15 @@ export default class GeneralLedgerService {
|
|||||||
group: 'organization',
|
group: 'organization',
|
||||||
key: 'base_currency',
|
key: 'base_currency',
|
||||||
});
|
});
|
||||||
|
const fromDate = moment(filter.fromDate).format('YYYY MMM DD');
|
||||||
|
const toDate = moment(filter.toDate).format('YYYY MMM DD');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
|
||||||
organizationName,
|
organizationName,
|
||||||
baseCurrency
|
baseCurrency,
|
||||||
|
fromDate,
|
||||||
|
toDate
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ export default class GeneralLedgerService {
|
|||||||
return {
|
return {
|
||||||
data: reportData,
|
data: reportData,
|
||||||
query: filter,
|
query: filter,
|
||||||
meta: this.reportMetadata(tenantId),
|
meta: this.reportMetadata(tenantId, filter),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
IGeneralLedgerSheetQuery,
|
||||||
|
IGeneralLedgerTableData,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { GeneralLedgerService } from './GeneralLedgerService';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { GeneralLedgerTable } from './GeneralLedgerTable';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GeneralLedgerTableInjectable {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private GLSheet: GeneralLedgerService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the G/L table.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IGeneralLedgerSheetQuery} query
|
||||||
|
* @returns {Promise<IGeneralLedgerTableData>}
|
||||||
|
*/
|
||||||
|
public async table(
|
||||||
|
tenantId: number,
|
||||||
|
query: IGeneralLedgerSheetQuery
|
||||||
|
): Promise<IGeneralLedgerTableData> {
|
||||||
|
const {
|
||||||
|
data: sheetData,
|
||||||
|
query: sheetQuery,
|
||||||
|
meta: sheetMeta,
|
||||||
|
} = await this.GLSheet.generalLedger(tenantId, query);
|
||||||
|
|
||||||
|
const table = new GeneralLedgerTable(sheetData, sheetQuery, sheetMeta);
|
||||||
|
|
||||||
|
return {
|
||||||
|
table: {
|
||||||
|
columns: table.tableColumns(),
|
||||||
|
rows: table.tableRows(),
|
||||||
|
},
|
||||||
|
query: sheetQuery,
|
||||||
|
meta: sheetMeta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export enum ROW_TYPE {
|
||||||
|
ACCOUNT = 'ACCOUNT',
|
||||||
|
OPENING_BALANCE = 'OPENING_BALANCE',
|
||||||
|
TRANSACTION = 'TRANSACTION',
|
||||||
|
CLOSING_BALANCE = 'CLOSING_BALANCE',
|
||||||
|
}
|
||||||
@@ -6,8 +6,10 @@ import {
|
|||||||
IJournalReportQuery,
|
IJournalReportQuery,
|
||||||
IJournalReport,
|
IJournalReport,
|
||||||
IContact,
|
IContact,
|
||||||
|
IJournalTableData,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
export default class JournalSheet extends FinancialSheet {
|
export default class JournalSheet extends FinancialSheet {
|
||||||
readonly tenantId: number;
|
readonly tenantId: number;
|
||||||
@@ -96,6 +98,8 @@ export default class JournalSheet extends FinancialSheet {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
date: groupEntry.date,
|
date: groupEntry.date,
|
||||||
|
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
|
||||||
|
|
||||||
referenceType: groupEntry.referenceType,
|
referenceType: groupEntry.referenceType,
|
||||||
referenceId: groupEntry.referenceId,
|
referenceId: groupEntry.referenceId,
|
||||||
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
|
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
|
||||||
@@ -131,7 +135,7 @@ export default class JournalSheet extends FinancialSheet {
|
|||||||
* Retrieve journal report.
|
* Retrieve journal report.
|
||||||
* @return {IJournalReport}
|
* @return {IJournalReport}
|
||||||
*/
|
*/
|
||||||
reportData(): IJournalReport {
|
reportData(): IJournalTableData {
|
||||||
return this.entriesWalker(this.journal.entries);
|
return this.entriesWalker(this.journal.entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Inject } from 'typedi';
|
||||||
|
import { JournalSheetService } from './JournalSheetService';
|
||||||
|
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
|
||||||
|
import { IJournalReportQuery, IJournalTable } from '@/interfaces';
|
||||||
|
import { JournalSheetExportInjectable } from './JournalSheetExport';
|
||||||
|
|
||||||
|
export class JournalSheetApplication {
|
||||||
|
@Inject()
|
||||||
|
private journalSheetTable: JournalSheetTableInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private journalSheet: JournalSheetService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private journalExport: JournalSheetExportInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
public sheet(tenantId: number, query: IJournalReportQuery) {
|
||||||
|
return this.journalSheet.journalSheet(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in table format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<IJournalTable>}
|
||||||
|
*/
|
||||||
|
public table(
|
||||||
|
tenantId: number,
|
||||||
|
query: IJournalReportQuery
|
||||||
|
): Promise<IJournalTable> {
|
||||||
|
return this.journalSheetTable.table(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in xlsx format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public xlsx(tenantId: number, query: IJournalReportQuery) {
|
||||||
|
return this.journalExport.xlsx(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in csv format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public csv(tenantId: number, query: IJournalReportQuery) {
|
||||||
|
return this.journalExport.csv(tenantId, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { TableSheet } from '@/lib/Xlsx/TableSheet';
|
||||||
|
import { IJournalReportQuery } from '@/interfaces';
|
||||||
|
import { JournalSheetTableInjectable } from './JournalSheetTableInjectable';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class JournalSheetExportInjectable {
|
||||||
|
@Inject()
|
||||||
|
private journalSheetTable: JournalSheetTableInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial balance sheet in XLSX format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async xlsx(tenantId: number, query: IJournalReportQuery) {
|
||||||
|
const table = await this.journalSheetTable.table(tenantId, query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToXLSX();
|
||||||
|
|
||||||
|
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the trial balance sheet in CSV format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async csv(
|
||||||
|
tenantId: number,
|
||||||
|
query: IJournalReportQuery
|
||||||
|
): Promise<string> {
|
||||||
|
const table = await this.journalSheetTable.table(tenantId, query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToCSV();
|
||||||
|
|
||||||
|
return tableCsv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
|
import {
|
||||||
|
IJournalReportQuery,
|
||||||
|
IJournalSheet,
|
||||||
|
IJournalSheetMeta,
|
||||||
|
IJournalTableData,
|
||||||
|
} from '@/interfaces';
|
||||||
import JournalSheet from './JournalSheet';
|
import JournalSheet from './JournalSheet';
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import Journal from '@/services/Accounting/JournalPoster';
|
import Journal from '@/services/Accounting/JournalPoster';
|
||||||
import InventoryService from '@/services/Inventory/Inventory';
|
import InventoryService from '@/services/Inventory/Inventory';
|
||||||
import { parseBoolean, transformToMap } from 'utils';
|
|
||||||
import { Tenant } from '@/system/models';
|
import { Tenant } from '@/system/models';
|
||||||
|
import { parseBoolean, transformToMap } from 'utils';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class JournalSheetService {
|
export class JournalSheetService {
|
||||||
@Inject()
|
@Inject()
|
||||||
tenancy: TenancyService;
|
private tenancy: TenancyService;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
inventoryService: InventoryService;
|
private inventoryService: InventoryService;
|
||||||
|
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default journal sheet filter queyr.
|
* Default journal sheet filter queyr.
|
||||||
@@ -67,9 +68,13 @@ export default class JournalSheetService {
|
|||||||
/**
|
/**
|
||||||
* Journal sheet.
|
* Journal sheet.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IJournalSheetFilterQuery} query
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<IJournalSheet>}
|
||||||
*/
|
*/
|
||||||
async journalSheet(tenantId: number, query: IJournalReportQuery) {
|
async journalSheet(
|
||||||
|
tenantId: number,
|
||||||
|
query: IJournalReportQuery
|
||||||
|
): Promise<IJournalSheet> {
|
||||||
const i18n = this.tenancy.i18n(tenantId);
|
const i18n = this.tenancy.i18n(tenantId);
|
||||||
const { accountRepository, transactionsRepository, contactRepository } =
|
const { accountRepository, transactionsRepository, contactRepository } =
|
||||||
this.tenancy.repositories(tenantId);
|
this.tenancy.repositories(tenantId);
|
||||||
@@ -80,11 +85,6 @@ export default class JournalSheetService {
|
|||||||
...this.defaultQuery,
|
...this.defaultQuery,
|
||||||
...query,
|
...query,
|
||||||
};
|
};
|
||||||
this.logger.info('[journal] trying to calculate the report.', {
|
|
||||||
tenantId,
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tenant = await Tenant.query()
|
const tenant = await Tenant.query()
|
||||||
.findById(tenantId)
|
.findById(tenantId)
|
||||||
.withGraphFetched('metadata');
|
.withGraphFetched('metadata');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { Inject } from 'typedi';
|
||||||
|
import { JournalSheetService } from './JournalSheetService';
|
||||||
|
import { IJournalReportQuery, IJournalTable } from '@/interfaces';
|
||||||
|
import { JournalSheetTable } from './JournalSheetTable';
|
||||||
|
|
||||||
|
export class JournalSheetTableInjectable {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private journalSheetService: JournalSheetService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the journal sheet in table format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IJournalReportQuery} query
|
||||||
|
* @returns {Promise<IJournalTable>}
|
||||||
|
*/
|
||||||
|
public async table(
|
||||||
|
tenantId: number,
|
||||||
|
query: IJournalReportQuery
|
||||||
|
): Promise<IJournalTable> {
|
||||||
|
const journal = await this.journalSheetService.journalSheet(
|
||||||
|
tenantId,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
const table = new JournalSheetTable(journal.data, journal.query, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
table: {
|
||||||
|
columns: table.tableColumns(),
|
||||||
|
rows: table.tableData(),
|
||||||
|
},
|
||||||
|
query: journal.query,
|
||||||
|
meta: journal.meta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
export enum ROW_TYPE {
|
||||||
|
ENTRY = 'ENTRY',
|
||||||
|
TOTAL = 'TOTAL'
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { CommonMailOptions } from '@/interfaces';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { MailTenancy } from '@/services/MailTenancy/MailTenancy';
|
||||||
|
import { formatSmsMessage } from '@/utils';
|
||||||
|
import { Tenant } from '@/system/models';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ContactMailNotification {
|
||||||
|
@Inject()
|
||||||
|
private mailTenancy: MailTenancy;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the default message options.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {number} invoiceId -
|
||||||
|
* @param {string} subject -
|
||||||
|
* @param {string} body -
|
||||||
|
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||||
|
*/
|
||||||
|
public async getDefaultMailOptions(
|
||||||
|
tenantId: number,
|
||||||
|
contactId: number,
|
||||||
|
subject: string = '',
|
||||||
|
body: string = ''
|
||||||
|
): Promise<CommonMailOptions> {
|
||||||
|
const { Customer } = this.tenancy.models(tenantId);
|
||||||
|
const contact = await Customer.query()
|
||||||
|
.findById(contactId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const toAddresses = contact.contactAddresses;
|
||||||
|
const fromAddresses = await this.mailTenancy.senders(tenantId);
|
||||||
|
|
||||||
|
const toAddress = toAddresses.find((a) => a.primary);
|
||||||
|
const fromAddress = fromAddresses.find((a) => a.primary);
|
||||||
|
|
||||||
|
const to = toAddress?.mail || '';
|
||||||
|
const from = fromAddress?.mail || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
subject,
|
||||||
|
body,
|
||||||
|
to,
|
||||||
|
from,
|
||||||
|
fromAddresses,
|
||||||
|
toAddresses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the mail options of the given contact.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} invoiceId - Invoice id.
|
||||||
|
* @param {string} defaultSubject - Default subject text.
|
||||||
|
* @param {string} defaultBody - Default body text.
|
||||||
|
* @returns {Promise<CommonMailOptions>}
|
||||||
|
*/
|
||||||
|
public async getMailOptions(
|
||||||
|
tenantId: number,
|
||||||
|
contactId: number,
|
||||||
|
defaultSubject?: string,
|
||||||
|
defaultBody?: string,
|
||||||
|
formatterData?: Record<string, any>
|
||||||
|
): Promise<CommonMailOptions> {
|
||||||
|
const mailOpts = await this.getDefaultMailOptions(
|
||||||
|
tenantId,
|
||||||
|
contactId,
|
||||||
|
defaultSubject,
|
||||||
|
defaultBody
|
||||||
|
);
|
||||||
|
const commonFormatArgs = await this.getCommonFormatArgs(tenantId);
|
||||||
|
const formatArgs = {
|
||||||
|
...commonFormatArgs,
|
||||||
|
...formatterData,
|
||||||
|
};
|
||||||
|
const subject = formatSmsMessage(mailOpts.subject, formatArgs);
|
||||||
|
const body = formatSmsMessage(mailOpts.body, formatArgs);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mailOpts,
|
||||||
|
subject,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the common format args.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @returns {Promise<Record<string, string>>}
|
||||||
|
*/
|
||||||
|
public async getCommonFormatArgs(
|
||||||
|
tenantId: number
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
|
const organization = await Tenant.query()
|
||||||
|
.findById(tenantId)
|
||||||
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
|
return {
|
||||||
|
CompanyName: organization.metadata.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
33
packages/server/src/services/MailNotification/utils.ts
Normal file
33
packages/server/src/services/MailNotification/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { CommonMailOptions, CommonMailOptionsDTO } from '@/interfaces';
|
||||||
|
import { ERRORS } from './constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges the mail options with incoming options.
|
||||||
|
* @param {Partial<SaleInvoiceMailOptions>} mailOptions
|
||||||
|
* @param {Partial<SendInvoiceMailDTO>} overridedOptions
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
export function parseAndValidateMailOptions(
|
||||||
|
mailOptions: Partial<CommonMailOptions>,
|
||||||
|
overridedOptions: Partial<CommonMailOptionsDTO>
|
||||||
|
) {
|
||||||
|
const mergedMessageOptions = {
|
||||||
|
...mailOptions,
|
||||||
|
...overridedOptions,
|
||||||
|
};
|
||||||
|
if (isEmpty(mergedMessageOptions.from)) {
|
||||||
|
throw new ServiceError(ERRORS.MAIL_FROM_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (isEmpty(mergedMessageOptions.to)) {
|
||||||
|
throw new ServiceError(ERRORS.MAIL_TO_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (isEmpty(mergedMessageOptions.subject)) {
|
||||||
|
throw new ServiceError(ERRORS.MAIL_SUBJECT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (isEmpty(mergedMessageOptions.body)) {
|
||||||
|
throw new ServiceError(ERRORS.MAIL_BODY_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return mergedMessageOptions;
|
||||||
|
}
|
||||||
25
packages/server/src/services/MailTenancy/MailTenancy.ts
Normal file
25
packages/server/src/services/MailTenancy/MailTenancy.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import config from '@/config';
|
||||||
|
import { Tenant } from "@/system/models";
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class MailTenancy {
|
||||||
|
/**
|
||||||
|
* Retrieves the senders mails of the given tenant.
|
||||||
|
* @param {number} tenantId
|
||||||
|
*/
|
||||||
|
public async senders(tenantId: number) {
|
||||||
|
const tenant = await Tenant.query()
|
||||||
|
.findById(tenantId)
|
||||||
|
.withGraphFetched('metadata');
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
mail: config.mail.from,
|
||||||
|
label: tenant.metadata.name,
|
||||||
|
primary: true,
|
||||||
|
}
|
||||||
|
].filter((item) => item.mail)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
ISaleEstimate,
|
ISaleEstimate,
|
||||||
ISaleEstimateDTO,
|
ISaleEstimateDTO,
|
||||||
ISalesEstimatesFilter,
|
ISalesEstimatesFilter,
|
||||||
|
SaleEstimateMailOptions,
|
||||||
|
SaleEstimateMailOptionsDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { EditSaleEstimate } from './EditSaleEstimate';
|
import { EditSaleEstimate } from './EditSaleEstimate';
|
||||||
import { DeleteSaleEstimate } from './DeleteSaleEstimate';
|
import { DeleteSaleEstimate } from './DeleteSaleEstimate';
|
||||||
@@ -17,6 +19,7 @@ import { ApproveSaleEstimate } from './ApproveSaleEstimate';
|
|||||||
import { RejectSaleEstimate } from './RejectSaleEstimate';
|
import { RejectSaleEstimate } from './RejectSaleEstimate';
|
||||||
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
|
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
|
||||||
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
|
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
|
||||||
|
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleEstimatesApplication {
|
export class SaleEstimatesApplication {
|
||||||
@@ -50,6 +53,9 @@ export class SaleEstimatesApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private saleEstimatesPdfService: SaleEstimatesPdf;
|
private saleEstimatesPdfService: SaleEstimatesPdf;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private sendEstimateMailService: SendSaleEstimateMail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a sale estimate.
|
* Create a sale estimate.
|
||||||
* @param {number} tenantId - The tenant id.
|
* @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 {number} tenantId
|
||||||
* @param {} saleEstimate
|
* @param {number} saleEstimateId
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public getSaleEstimatePdf(tenantId: number, saleEstimate) {
|
public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
|
||||||
return this.saleEstimatesPdfService.getSaleEstimatePdf(
|
return this.saleEstimatesPdfService.getSaleEstimatePdf(
|
||||||
tenantId,
|
tenantId,
|
||||||
saleEstimate
|
saleEstimateId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the reminder mail of the given sale estimate.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleEstimateId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public sendSaleEstimateMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleEstimateId: number,
|
||||||
|
saleEstimateMailOpts: SaleEstimateMailOptionsDTO
|
||||||
|
): Promise<void> {
|
||||||
|
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<SaleEstimateMailOptions>}
|
||||||
|
*/
|
||||||
|
public getSaleEstimateMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleEstimateId: number
|
||||||
|
): Promise<SaleEstimateMailOptions> {
|
||||||
|
return this.sendEstimateMailService.getMailOptions(
|
||||||
|
tenantId,
|
||||||
|
saleEstimateId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
|
import { GetSaleEstimate } from './GetSaleEstimate';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleEstimatesPdf {
|
export class SaleEstimatesPdf {
|
||||||
@@ -10,11 +11,19 @@ export class SaleEstimatesPdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private templateInjectable: TemplateInjectable;
|
private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getSaleEstimate: GetSaleEstimate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* 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(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/estimate-regular',
|
'modules/estimate-regular',
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
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 {
|
||||||
|
SaleEstimateMailOptions,
|
||||||
|
SaleEstimateMailOptionsDTO,
|
||||||
|
} from '@/interfaces';
|
||||||
|
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||||
|
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the reminder mail of the given sale estimate.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {number} saleEstimateId -
|
||||||
|
* @param {SaleEstimateMailOptionsDTO} messageOptions -
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async triggerMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleEstimateId: number,
|
||||||
|
messageOptions: SaleEstimateMailOptionsDTO
|
||||||
|
): Promise<void> {
|
||||||
|
const payload = {
|
||||||
|
tenantId,
|
||||||
|
saleEstimateId,
|
||||||
|
messageOptions,
|
||||||
|
};
|
||||||
|
await this.agenda.now('sale-estimate-mail-send', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formates the text of the mail.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} estimateId - Estimate id.
|
||||||
|
* @returns {Promise<Record<string, any>>}
|
||||||
|
*/
|
||||||
|
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<SaleEstimateMailOptions>}
|
||||||
|
*/
|
||||||
|
public getMailOptions = async (
|
||||||
|
tenantId: number,
|
||||||
|
saleEstimateId: number
|
||||||
|
): Promise<SaleEstimateMailOptions> => {
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
public async sendMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleEstimateId: number,
|
||||||
|
messageOptions: SaleEstimateMailOptionsDTO
|
||||||
|
): Promise<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,18 @@
|
|||||||
|
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
|
||||||
|
'Estimate {EstimateNumber} is awaiting your approval';
|
||||||
|
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {CustomerName}</p>
|
||||||
|
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
|
||||||
|
<p>
|
||||||
|
Estimate <strong>#{EstimateNumber}</strong><br />
|
||||||
|
Expiration Date : <strong>{EstimateExpirationDate}</strong><br />
|
||||||
|
Amount : <strong>{EstimateAmount}</strong></br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>Regards</i><br />
|
||||||
|
<i>{CompanyName}</i>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||||
@@ -8,7 +23,7 @@ export const ERRORS = {
|
|||||||
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
|
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
|
||||||
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
|
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
|
||||||
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
|
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 = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export class GetSaleInvoice {
|
|||||||
*/
|
*/
|
||||||
public async getSaleInvoice(
|
public async getSaleInvoice(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
saleInvoiceId: number,
|
saleInvoiceId: number
|
||||||
authorizedUser: ISystemUser
|
|
||||||
): Promise<ISaleInvoice> {
|
): Promise<ISaleInvoice> {
|
||||||
const { SaleInvoice } = this.tenancy.models(tenantId);
|
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export class GetSaleInvoiceMailReminder {
|
||||||
|
public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { ISaleInvoice } from '@/interfaces';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoicePdf {
|
export class SaleInvoicePdf {
|
||||||
@@ -11,16 +12,34 @@ export class SaleInvoicePdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private templateInjectable: TemplateInjectable;
|
private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private validators: CommandSaleInvoiceValidators;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId - Tenant Id.
|
* @param {number} tenantId - Tenant Id.
|
||||||
* @param {ISaleInvoice} saleInvoice -
|
* @param {ISaleInvoice} saleInvoice -
|
||||||
* @returns {Promise<Buffer>}
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
async saleInvoicePdf(
|
public async saleInvoicePdf(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
saleInvoice: ISaleInvoice
|
invoiceId: number
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
|
const { SaleInvoice } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const saleInvoice = await SaleInvoice.query()
|
||||||
|
.findById(invoiceId)
|
||||||
|
.withGraphFetched('entries.item')
|
||||||
|
.withGraphFetched('entries.tax')
|
||||||
|
.withGraphFetched('customer')
|
||||||
|
.withGraphFetched('taxes.taxRate');
|
||||||
|
|
||||||
|
// Validates the given sale invoice existance.
|
||||||
|
this.validators.validateInvoiceExistance(saleInvoice);
|
||||||
|
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/invoice-regular',
|
'modules/invoice-regular',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ISystemUser,
|
ISystemUser,
|
||||||
ITenantUser,
|
ITenantUser,
|
||||||
InvoiceNotificationType,
|
InvoiceNotificationType,
|
||||||
|
SendInvoiceMailDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { CreateSaleInvoice } from './CreateSaleInvoice';
|
import { CreateSaleInvoice } from './CreateSaleInvoice';
|
||||||
@@ -24,6 +25,9 @@ import { WriteoffSaleInvoice } from './WriteoffSaleInvoice';
|
|||||||
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
import { SaleInvoicePdf } from './SaleInvoicePdf';
|
||||||
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
|
import { GetInvoicePaymentsService } from './GetInvoicePaymentsService';
|
||||||
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
|
import { SaleInvoiceNotifyBySms } from './SaleInvoiceNotifyBySms';
|
||||||
|
import { SendInvoiceMailReminder } from './SendSaleInvoiceMailReminder';
|
||||||
|
import { SendSaleInvoiceMail } from './SendSaleInvoiceMail';
|
||||||
|
import { GetSaleInvoiceMailReminder } from './GetSaleInvoiceMailReminder';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoiceApplication {
|
export class SaleInvoiceApplication {
|
||||||
@@ -60,6 +64,15 @@ export class SaleInvoiceApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private invoiceSms: SaleInvoiceNotifyBySms;
|
private invoiceSms: SaleInvoiceNotifyBySms;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private sendInvoiceReminderService: SendInvoiceMailReminder;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private sendSaleInvoiceMailService: SendSaleInvoiceMail;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getSaleInvoiceReminderService: GetSaleInvoiceMailReminder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale invoice with associated GL entries.
|
* Creates a new sale invoice with associated GL entries.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -236,13 +249,13 @@ export class SaleInvoiceApplication {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Retrieves the pdf buffer of the given sale invoice.
|
||||||
* @param {number} tenantId ]
|
* @param {number} tenantId - Tenant id.
|
||||||
* @param saleInvoice
|
* @param {number} saleInvoice
|
||||||
* @returns
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
public saleInvoicePdf(tenantId: number, saleInvoice) {
|
public saleInvoicePdf(tenantId: number, saleInvoiceId: number) {
|
||||||
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoice);
|
return this.pdfSaleInvoiceService.saleInvoicePdf(tenantId, saleInvoiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -279,4 +292,67 @@ export class SaleInvoiceApplication {
|
|||||||
invoiceSmsDetailsDTO
|
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<void>}
|
||||||
|
*/
|
||||||
|
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<SendInvoiceMailDTO>}
|
||||||
|
*/
|
||||||
|
public getSaleInvoiceMail(tenantId: number, saleInvoiceid: number) {
|
||||||
|
return this.sendSaleInvoiceMailService.getMailOption(
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceid
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<SaleInvoiceMailOptions>}
|
||||||
|
*/
|
||||||
|
public async getMailOption(
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number,
|
||||||
|
defaultSubject: string = DEFAULT_INVOICE_MAIL_SUBJECT,
|
||||||
|
defaultBody: string = DEFAULT_INVOICE_MAIL_CONTENT
|
||||||
|
): Promise<SaleInvoiceMailOptions> {
|
||||||
|
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<string>}
|
||||||
|
*/
|
||||||
|
public formatText = async (
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number
|
||||||
|
): Promise<Record<string, string | number>> => {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import Mail from '@/lib/Mail';
|
||||||
|
import { 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';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SendSaleInvoiceMail {
|
||||||
|
@Inject()
|
||||||
|
private invoicePdf: SaleInvoicePdf;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private invoiceMail: SendSaleInvoiceMailCommon;
|
||||||
|
|
||||||
|
@Inject('agenda')
|
||||||
|
private agenda: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
messageDTO: SendInvoiceMailDTO
|
||||||
|
) {
|
||||||
|
const payload = {
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceId,
|
||||||
|
messageDTO,
|
||||||
|
};
|
||||||
|
await this.agenda.now('sale-invoice-mail-send', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the mail options of the given sale invoice.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleInvoiceId
|
||||||
|
* @returns {Promise<SaleInvoiceMailOptions>}
|
||||||
|
*/
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
public async sendMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleInvoiceId: number,
|
||||||
|
messageDTO: SendInvoiceMailDTO
|
||||||
|
) {
|
||||||
|
const defaultMessageOpts = await this.getMailOption(
|
||||||
|
tenantId,
|
||||||
|
saleInvoiceId
|
||||||
|
);
|
||||||
|
// Merge message opts with default options and validate the incoming options.
|
||||||
|
const messageOpts = parseAndValidateMailOptions(
|
||||||
|
defaultMessageOpts,
|
||||||
|
messageDTO
|
||||||
|
);
|
||||||
|
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 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await mail.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, messageDTO } = job.attrs.data;
|
||||||
|
const sendInvoiceMail = Container.get(SendSaleInvoiceMail);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendInvoiceMail.sendMail(tenantId, saleInvoiceId, messageDTO);
|
||||||
|
done();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
done(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { 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';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SendInvoiceMailReminder {
|
||||||
|
@Inject('agenda')
|
||||||
|
private agenda: any;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private invoicePdf: SaleInvoicePdf;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private invoiceCommonMail: SendSaleInvoiceMailCommon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<SaleInvoiceMailOptions>}
|
||||||
|
*/
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
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 },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await mail.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,35 @@
|
|||||||
|
export const DEFAULT_INVOICE_MAIL_SUBJECT =
|
||||||
|
'Invoice {InvoiceNumber} from {CompanyName}';
|
||||||
|
export const DEFAULT_INVOICE_MAIL_CONTENT = `
|
||||||
|
<p>Dear {CustomerName}</p>
|
||||||
|
<p>Thank you for your business, You can view or print your invoice from attachements.</p>
|
||||||
|
<p>
|
||||||
|
Invoice <strong>#{InvoiceNumber}</strong><br />
|
||||||
|
Due Date : <strong>{InvoiceDueDate}</strong><br />
|
||||||
|
Amount : <strong>{InvoiceAmount}</strong></br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>Regards</i><br />
|
||||||
|
<i>{CompanyName}</i>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT =
|
||||||
|
'Invoice {InvoiceNumber} reminder from {CompanyName}';
|
||||||
|
export const DEFAULT_INVOICE_REMINDER_MAIL_CONTENT = `
|
||||||
|
<p>Dear {CustomerName}</p>
|
||||||
|
<p>You might have missed the payment date and the invoice is now overdue by {OverdueDays} days.</p>
|
||||||
|
<p>Invoice <strong>#{InvoiceNumber}</strong><br />
|
||||||
|
Due Date : <strong>{InvoiceDueDate}</strong><br />
|
||||||
|
Amount : <strong>{InvoiceAmount}</strong></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>Regards</i><br />
|
||||||
|
<i>{CompanyName}</i>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
|
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
|
||||||
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
|
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
|
||||||
@@ -16,6 +48,7 @@ export const ERRORS = {
|
|||||||
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
||||||
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
|
SALE_INVOICE_ALREADY_WRITTEN_OFF: 'SALE_INVOICE_ALREADY_WRITTEN_OFF',
|
||||||
SALE_INVOICE_NOT_WRITTEN_OFF: 'SALE_INVOICE_NOT_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 = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { IPaymentReceive } from '@/interfaces';
|
import { GetPaymentReceive } from './GetPaymentReceive';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GetPaymentReceivePdf {
|
export default class GetPaymentReceivePdf {
|
||||||
@@ -11,6 +11,9 @@ export default class GetPaymentReceivePdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private templateInjectable: TemplateInjectable;
|
private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getPaymentService: GetPaymentReceive;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -19,8 +22,12 @@ export default class GetPaymentReceivePdf {
|
|||||||
*/
|
*/
|
||||||
async getPaymentReceivePdf(
|
async getPaymentReceivePdf(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
paymentReceive: IPaymentReceive
|
paymentReceiveId: number
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
|
const paymentReceive = await this.getPaymentService.getPaymentReceive(
|
||||||
|
tenantId,
|
||||||
|
paymentReceiveId
|
||||||
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/payment-receive-standard',
|
'modules/payment-receive-standard',
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import {
|
||||||
|
PaymentReceiveMailOpts,
|
||||||
|
PaymentReceiveMailOptsDTO,
|
||||||
|
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';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SendPaymentReceiveMailNotification {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getPaymentService: GetPaymentReceive;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private contactMailNotification: ContactMailNotification;
|
||||||
|
|
||||||
|
@Inject('agenda')
|
||||||
|
private agenda: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the mail of the given payment receive.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} paymentReceiveId
|
||||||
|
* @param {PaymentReceiveMailOptsDTO} messageDTO
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async triggerMail(
|
||||||
|
tenantId: number,
|
||||||
|
paymentReceiveId: number,
|
||||||
|
messageDTO: PaymentReceiveMailOptsDTO
|
||||||
|
): Promise<void> {
|
||||||
|
const payload = {
|
||||||
|
tenantId,
|
||||||
|
paymentReceiveId,
|
||||||
|
messageDTO,
|
||||||
|
};
|
||||||
|
await this.agenda.now('payment-receive-mail-send', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the default payment mail options.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} paymentReceiveId - Payment receive id.
|
||||||
|
* @returns {Promise<PaymentReceiveMailOpts>}
|
||||||
|
*/
|
||||||
|
public getMailOptions = async (
|
||||||
|
tenantId: number,
|
||||||
|
paymentId: number
|
||||||
|
): Promise<PaymentReceiveMailOpts> => {
|
||||||
|
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<string>}
|
||||||
|
*/
|
||||||
|
public textFormatter = async (
|
||||||
|
tenantId: number,
|
||||||
|
invoiceId: number
|
||||||
|
): Promise<Record<string, string>> => {
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
public async sendMail(
|
||||||
|
tenantId: number,
|
||||||
|
paymentReceiveId: number,
|
||||||
|
messageDTO: SendInvoiceMailDTO
|
||||||
|
): Promise<void> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
IPaymentReceiveSmsDetails,
|
IPaymentReceiveSmsDetails,
|
||||||
IPaymentReceivesFilter,
|
IPaymentReceivesFilter,
|
||||||
ISystemUser,
|
ISystemUser,
|
||||||
|
PaymentReceiveMailOptsDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { CreatePaymentReceive } from './CreatePaymentReceive';
|
import { CreatePaymentReceive } from './CreatePaymentReceive';
|
||||||
@@ -17,7 +18,7 @@ import { GetPaymentReceive } from './GetPaymentReceive';
|
|||||||
import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices';
|
import { GetPaymentReceiveInvoices } from './GetPaymentReceiveInvoices';
|
||||||
import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify';
|
import { PaymentReceiveNotifyBySms } from './PaymentReceiveSmsNotify';
|
||||||
import GetPaymentReceivePdf from './GetPaymentReeceivePdf';
|
import GetPaymentReceivePdf from './GetPaymentReeceivePdf';
|
||||||
import { PaymentReceive } from '@/models';
|
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class PaymentReceivesApplication {
|
export class PaymentReceivesApplication {
|
||||||
@@ -42,6 +43,9 @@ export class PaymentReceivesApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private paymentSmsNotify: PaymentReceiveNotifyBySms;
|
private paymentSmsNotify: PaymentReceiveNotifyBySms;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private paymentMailNotify: SendPaymentReceiveMailNotification;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private getPaymentReceivePdfService: GetPaymentReceivePdf;
|
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<void>}
|
||||||
|
*/
|
||||||
|
public notifyPaymentByMail(
|
||||||
|
tenantId: number,
|
||||||
|
paymentReceiveId: number,
|
||||||
|
messageOpts: PaymentReceiveMailOptsDTO
|
||||||
|
): Promise<void> {
|
||||||
|
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<void>}
|
||||||
|
*/
|
||||||
|
public getPaymentMailOptions(tenantId: number, paymentReceiveId: number) {
|
||||||
|
return this.paymentMailNotify.getMailOptions(tenantId, paymentReceiveId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve pdf content of the given payment receive.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {PaymentReceive} paymentReceive
|
* @param {PaymentReceive} paymentReceive
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
public getPaymentReceivePdf = (
|
public getPaymentReceivePdf = (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
paymentReceive: PaymentReceive
|
paymentReceiveId: number
|
||||||
) => {
|
) => {
|
||||||
return this.getPaymentReceivePdfService.getPaymentReceivePdf(
|
return this.getPaymentReceivePdfService.getPaymentReceivePdf(
|
||||||
tenantId,
|
tenantId,
|
||||||
paymentReceive
|
paymentReceiveId
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
export const DEFAULT_PAYMENT_MAIL_SUBJECT = 'Payment Received by {CompanyName}';
|
||||||
|
export const DEFAULT_PAYMENT_MAIL_CONTENT = `
|
||||||
|
<p>Dear {CustomerName}</p>
|
||||||
|
<p>Thank you for your payment. It was a pleasure doing business with you. We look forward to work together again!</p>
|
||||||
|
<p>
|
||||||
|
Payment Date : <strong>{PaymentDate}</strong><br />
|
||||||
|
Amount : <strong>{PaymentAmount}</strong></br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>Regards</i><br />
|
||||||
|
<i>{CompanyName}</i>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
|
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
|
||||||
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
|
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
|
||||||
@@ -12,6 +27,7 @@ export const ERRORS = {
|
|||||||
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
|
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
|
||||||
CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES',
|
CUSTOMER_HAS_PAYMENT_RECEIVES: 'CUSTOMER_HAS_PAYMENT_RECEIVES',
|
||||||
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
PAYMENT_ACCOUNT_CURRENCY_INVALID: 'PAYMENT_ACCOUNT_CURRENCY_INVALID',
|
||||||
|
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VIEWS = [];
|
export const DEFAULT_VIEWS = [];
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
IPaginationMeta,
|
IPaginationMeta,
|
||||||
ISaleReceipt,
|
ISaleReceipt,
|
||||||
ISalesReceiptsFilter,
|
ISalesReceiptsFilter,
|
||||||
|
SaleReceiptMailOpts,
|
||||||
|
SaleReceiptMailOptsDTO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { EditSaleReceipt } from './EditSaleReceipt';
|
import { EditSaleReceipt } from './EditSaleReceipt';
|
||||||
import { GetSaleReceipt } from './GetSaleReceipt';
|
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||||
@@ -13,6 +15,7 @@ import { GetSaleReceipts } from './GetSaleReceipts';
|
|||||||
import { CloseSaleReceipt } from './CloseSaleReceipt';
|
import { CloseSaleReceipt } from './CloseSaleReceipt';
|
||||||
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||||
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
import { SaleReceiptNotifyBySms } from './SaleReceiptNotifyBySms';
|
||||||
|
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptApplication {
|
export class SaleReceiptApplication {
|
||||||
@@ -40,6 +43,9 @@ export class SaleReceiptApplication {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms;
|
private saleReceiptNotifyBySmsService: SaleReceiptNotifyBySms;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private saleReceiptNotifyByMailService: SaleReceiptMailNotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new sale receipt with associated entries.
|
* Creates a new sale receipt with associated entries.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -166,4 +172,39 @@ export class SaleReceiptApplication {
|
|||||||
saleReceiptId
|
saleReceiptId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the receipt mail of the given sale receipt.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleReceiptId
|
||||||
|
* @param {SaleReceiptMailOptsDTO} messageOpts
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public sendSaleReceiptMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleReceiptId: number,
|
||||||
|
messageOpts: SaleReceiptMailOptsDTO
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleReceiptNotifyByMailService.triggerMail(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId,
|
||||||
|
messageOpts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the default mail options of the given sale receipt.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleReceiptId
|
||||||
|
* @returns {Promise<SaleReceiptMailOpts>}
|
||||||
|
*/
|
||||||
|
public getSaleReceiptMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleReceiptId: number
|
||||||
|
): Promise<SaleReceiptMailOpts> {
|
||||||
|
return this.saleReceiptNotifyByMailService.getMailOptions(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import Mail from '@/lib/Mail';
|
||||||
|
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||||
|
import { SaleReceiptsPdf } from './SaleReceiptsPdfService';
|
||||||
|
import {
|
||||||
|
DEFAULT_RECEIPT_MAIL_CONTENT,
|
||||||
|
DEFAULT_RECEIPT_MAIL_SUBJECT,
|
||||||
|
} from './constants';
|
||||||
|
import { SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces';
|
||||||
|
import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||||
|
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleReceiptMailNotification {
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getSaleReceiptService: GetSaleReceipt;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private receiptPdfService: SaleReceiptsPdf;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private contactMailNotification: ContactMailNotification;
|
||||||
|
|
||||||
|
@Inject('agenda')
|
||||||
|
private agenda: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the receipt mail of the given sale receipt.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleReceiptId
|
||||||
|
* @param {SaleReceiptMailOptsDTO} messageDTO
|
||||||
|
*/
|
||||||
|
public async triggerMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleReceiptId: number,
|
||||||
|
messageOpts: SaleReceiptMailOptsDTO
|
||||||
|
) {
|
||||||
|
const payload = {
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId,
|
||||||
|
messageOpts,
|
||||||
|
};
|
||||||
|
await this.agenda.now('sale-receipt-mail-send', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the mail options of the given sale receipt.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {number} saleReceiptId
|
||||||
|
* @returns {Promise<SaleReceiptMailOptsDTO>}
|
||||||
|
*/
|
||||||
|
public async getMailOptions(
|
||||||
|
tenantId: number,
|
||||||
|
saleReceiptId: number
|
||||||
|
): Promise<SaleReceiptMailOpts> {
|
||||||
|
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
const saleReceipt = await SaleReceipt.query()
|
||||||
|
.findById(saleReceiptId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
const formattedData = await this.textFormatter(tenantId, saleReceiptId);
|
||||||
|
|
||||||
|
const mailOpts = await this.contactMailNotification.getMailOptions(
|
||||||
|
tenantId,
|
||||||
|
saleReceipt.customerId,
|
||||||
|
DEFAULT_RECEIPT_MAIL_SUBJECT,
|
||||||
|
DEFAULT_RECEIPT_MAIL_CONTENT,
|
||||||
|
formattedData
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...mailOpts,
|
||||||
|
attachReceipt: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted text of the given sale receipt.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} receiptId - Sale receipt id.
|
||||||
|
* @param {string} text - The given text.
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
public textFormatter = async (
|
||||||
|
tenantId: number,
|
||||||
|
receiptId: number
|
||||||
|
): Promise<Record<string, string>> => {
|
||||||
|
const receipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||||
|
tenantId,
|
||||||
|
receiptId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
CustomerName: receipt.customer.displayName,
|
||||||
|
ReceiptNumber: receipt.receiptNumber,
|
||||||
|
ReceiptDate: receipt.formattedReceiptDate,
|
||||||
|
ReceiptAmount: receipt.formattedAmount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the mail notification of the given sale receipt.
|
||||||
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} saleReceiptId - Sale receipt id.
|
||||||
|
* @param {SaleReceiptMailOpts} messageDTO - Overrided message options.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async sendMail(
|
||||||
|
tenantId: number,
|
||||||
|
saleReceiptId: number,
|
||||||
|
messageOpts: SaleReceiptMailOptsDTO
|
||||||
|
) {
|
||||||
|
const defaultMessageOpts = await this.getMailOptions(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId
|
||||||
|
);
|
||||||
|
// Merges message opts with default options.
|
||||||
|
const parsedMessageOpts = parseAndValidateMailOptions(
|
||||||
|
defaultMessageOpts,
|
||||||
|
messageOpts
|
||||||
|
);
|
||||||
|
const mail = new Mail()
|
||||||
|
.setSubject(parsedMessageOpts.subject)
|
||||||
|
.setTo(parsedMessageOpts.to)
|
||||||
|
.setContent(parsedMessageOpts.body);
|
||||||
|
|
||||||
|
if (parsedMessageOpts.attachReceipt) {
|
||||||
|
// Retrieves document buffer of the receipt pdf document.
|
||||||
|
const receiptPdfBuffer = await this.receiptPdfService.saleReceiptPdf(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId
|
||||||
|
);
|
||||||
|
mail.setAttachments([
|
||||||
|
{ filename: 'receipt.pdf', content: receiptPdfBuffer },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
await mail.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import Container, { Service } from 'typedi';
|
||||||
|
import { SaleReceiptMailNotification } from './SaleReceiptMailNotification';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class SaleReceiptMailNotificationJob {
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
*/
|
||||||
|
constructor(agenda) {
|
||||||
|
agenda.define(
|
||||||
|
'sale-receipt-mail-send',
|
||||||
|
{ priority: 'high', concurrency: 2 },
|
||||||
|
this.handler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers sending invoice mail.
|
||||||
|
*/
|
||||||
|
private handler = async (job, done: Function) => {
|
||||||
|
const { tenantId, saleReceiptId, messageOpts } = job.attrs.data;
|
||||||
|
const receiveMailNotification = Container.get(SaleReceiptMailNotification);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await receiveMailNotification.sendMail(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId,
|
||||||
|
messageOpts
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
done(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
|
import { GetSaleReceipt } from './GetSaleReceipt';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptsPdf {
|
export class SaleReceiptsPdf {
|
||||||
@@ -10,11 +11,20 @@ export class SaleReceiptsPdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private templateInjectable: TemplateInjectable;
|
private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getSaleReceiptService: GetSaleReceipt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieves sale invoice pdf content.
|
||||||
* @param {} saleInvoice -
|
* @param {number} tenantId -
|
||||||
|
* @param {number} saleInvoiceId -
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
*/
|
*/
|
||||||
public async saleReceiptPdf(tenantId: number, saleReceipt) {
|
public async saleReceiptPdf(tenantId: number, saleReceiptId: number) {
|
||||||
|
const saleReceipt = await this.getSaleReceiptService.getSaleReceipt(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId
|
||||||
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/receipt-regular',
|
'modules/receipt-regular',
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
export const DEFAULT_RECEIPT_MAIL_SUBJECT =
|
||||||
|
'Receipt {ReceiptNumber} from {CompanyName}';
|
||||||
|
export const DEFAULT_RECEIPT_MAIL_CONTENT = `
|
||||||
|
<p>Dear {CustomerName}</p>
|
||||||
|
<p>Thank you for your business, You can view or print your receipt from attachements.</p>
|
||||||
|
<p>
|
||||||
|
Receipt <strong>#{ReceiptNumber}</strong><br />
|
||||||
|
Amount : <strong>{ReceiptAmount}</strong></br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>Regards</i><br />
|
||||||
|
<i>{CompanyName}</i>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
export const ERRORS = {
|
export const ERRORS = {
|
||||||
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
|
||||||
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
|
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
|
||||||
@@ -6,6 +22,7 @@ export const ERRORS = {
|
|||||||
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
|
||||||
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
|
||||||
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
CUSTOMER_HAS_SALES_INVOICES: 'CUSTOMER_HAS_SALES_INVOICES',
|
||||||
|
NO_INVOICE_CUSTOMER_EMAIL_ADDR: 'NO_INVOICE_CUSTOMER_EMAIL_ADDR'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VIEW_COLUMNS = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ export default {
|
|||||||
|
|
||||||
onNotifySms: 'onSaleInvoiceNotifySms',
|
onNotifySms: 'onSaleInvoiceNotifySms',
|
||||||
onNotifiedSms: 'onSaleInvoiceNotifiedSms',
|
onNotifiedSms: 'onSaleInvoiceNotifiedSms',
|
||||||
|
|
||||||
|
onNotifyMail: 'onSaleInvoiceNotifyMail',
|
||||||
|
onNotifyReminderMail: 'onSaleInvoiceNotifyReminderMail'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,6 +163,8 @@ export default {
|
|||||||
|
|
||||||
onRejecting: 'onSaleEstimateRejecting',
|
onRejecting: 'onSaleEstimateRejecting',
|
||||||
onRejected: 'onSaleEstimateRejected',
|
onRejected: 'onSaleEstimateRejected',
|
||||||
|
|
||||||
|
onNotifyMail: 'onSaleEstimateNotifyMail'
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,6 +20,13 @@
|
|||||||
"@testing-library/jest-dom": "^4.2.4",
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
"@testing-library/react": "^9.4.0",
|
"@testing-library/react": "^9.4.0",
|
||||||
"@testing-library/user-event": "^7.2.1",
|
"@testing-library/user-event": "^7.2.1",
|
||||||
|
"@tiptap/extension-color": "latest",
|
||||||
|
"@tiptap/extension-text-style": "2.1.13",
|
||||||
|
"@tiptap/core": "2.1.13",
|
||||||
|
"@tiptap/pm": "2.1.13",
|
||||||
|
"@tiptap/extension-list-item": "2.1.13",
|
||||||
|
"@tiptap/react": "2.1.13",
|
||||||
|
"@tiptap/starter-kit": "2.1.13",
|
||||||
"@types/jest": "^26.0.15",
|
"@types/jest": "^26.0.15",
|
||||||
"@types/js-money": "^0.6.1",
|
"@types/js-money": "^0.6.1",
|
||||||
"@types/lodash": "^4.14.172",
|
"@types/lodash": "^4.14.172",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function CustomerSelectRoot({
|
|||||||
<FSelect
|
<FSelect
|
||||||
items={items}
|
items={items}
|
||||||
textAccessor={'display_name'}
|
textAccessor={'display_name'}
|
||||||
labelAccessor={'code'}
|
labelAccessor={'currency_code'}
|
||||||
valueAccessor={'id'}
|
valueAccessor={'id'}
|
||||||
popoverProps={{ minimal: true, usePortal: true, inline: false }}
|
popoverProps={{ minimal: true, usePortal: true, inline: false }}
|
||||||
createNewItemRenderer={maybeCreateNewItemRenderer}
|
createNewItemRenderer={maybeCreateNewItemRenderer}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export default function Dashboard() {
|
|||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
<DashboardUniversalSearch />
|
<DashboardUniversalSearch />
|
||||||
<DialogsContainer />
|
|
||||||
<GlobalHotkeys />
|
<GlobalHotkeys />
|
||||||
|
<DialogsContainer />
|
||||||
<DrawersContainer />
|
<DrawersContainer />
|
||||||
<AlertsContainer />
|
<AlertsContainer />
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import InviteUserDialog from '@/containers/Dialogs/InviteUserDialog';
|
|||||||
import UserFormDialog from '@/containers/Dialogs/UserFormDialog';
|
import UserFormDialog from '@/containers/Dialogs/UserFormDialog';
|
||||||
import ItemCategoryDialog from '@/containers/Dialogs/ItemCategoryDialog';
|
import ItemCategoryDialog from '@/containers/Dialogs/ItemCategoryDialog';
|
||||||
import CurrencyFormDialog from '@/containers/Dialogs/CurrencyFormDialog';
|
import CurrencyFormDialog from '@/containers/Dialogs/CurrencyFormDialog';
|
||||||
import ExchangeRateFormDialog from '@/containers/Dialogs/ExchangeRateFormDialog';
|
|
||||||
|
|
||||||
import InventoryAdjustmentDialog from '@/containers/Dialogs/InventoryAdjustmentFormDialog';
|
import InventoryAdjustmentDialog from '@/containers/Dialogs/InventoryAdjustmentFormDialog';
|
||||||
import PaymentViaVoucherDialog from '@/containers/Dialogs/PaymentViaVoucherDialog';
|
import PaymentViaVoucherDialog from '@/containers/Dialogs/PaymentViaVoucherDialog';
|
||||||
import KeyboardShortcutsDialog from '@/containers/Dialogs/keyboardShortcutsDialog';
|
import KeyboardShortcutsDialog from '@/containers/Dialogs/keyboardShortcutsDialog';
|
||||||
@@ -47,6 +45,11 @@ import ProjectInvoicingFormDialog from '@/containers/Projects/containers/Project
|
|||||||
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
|
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
|
||||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||||
import { DialogsName } from '@/constants/dialogs';
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
|
||||||
|
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
|
||||||
|
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
|
||||||
|
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
|
||||||
|
import PaymentMailDialog from '@/containers/Sales/PaymentReceives/PaymentMailDialog/PaymentMailDialog';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialogs container.
|
* Dialogs container.
|
||||||
@@ -58,7 +61,6 @@ export default function DialogsContainer() {
|
|||||||
<CurrencyFormDialog dialogName={DialogsName.CurrencyForm} />
|
<CurrencyFormDialog dialogName={DialogsName.CurrencyForm} />
|
||||||
<InviteUserDialog dialogName={DialogsName.InviteForm} />
|
<InviteUserDialog dialogName={DialogsName.InviteForm} />
|
||||||
<UserFormDialog dialogName={DialogsName.UserForm} />
|
<UserFormDialog dialogName={DialogsName.UserForm} />
|
||||||
<ExchangeRateFormDialog dialogName={DialogsName.ExchangeRateForm} />
|
|
||||||
<ItemCategoryDialog dialogName={DialogsName.ItemCategoryForm} />
|
<ItemCategoryDialog dialogName={DialogsName.ItemCategoryForm} />
|
||||||
<InventoryAdjustmentDialog
|
<InventoryAdjustmentDialog
|
||||||
dialogName={DialogsName.InventoryAdjustmentForm}
|
dialogName={DialogsName.InventoryAdjustmentForm}
|
||||||
@@ -137,6 +139,13 @@ export default function DialogsContainer() {
|
|||||||
dialogName={DialogsName.ProjectBillableEntriesForm}
|
dialogName={DialogsName.ProjectBillableEntriesForm}
|
||||||
/>
|
/>
|
||||||
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
|
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
|
||||||
|
<InvoiceExchangeRateChangeDialog
|
||||||
|
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
|
||||||
|
/>
|
||||||
|
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
|
||||||
|
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
|
||||||
|
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />
|
||||||
|
<PaymentMailDialog dialogName={DialogsName.PaymentMail} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,159 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import { useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { ControlGroup } from '@blueprintjs/core';
|
import { useFormikContext } from 'formik';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Classes,
|
||||||
|
ControlGroup,
|
||||||
|
Intent,
|
||||||
|
Popover,
|
||||||
|
Position,
|
||||||
|
Spinner,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
import { FlagIcon } from '../Tags';
|
import { FlagIcon } from '../Tags';
|
||||||
import { FMoneyInputGroup, FFormGroup } from '../Forms';
|
import { FMoneyInputGroup, FFormGroup } from '../Forms';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
|
||||||
|
interface ExchangeRateValuesBag {
|
||||||
|
oldExchangeRate: string;
|
||||||
|
exchangeRate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExchangeRateInputGroupProps {
|
||||||
|
name: string;
|
||||||
|
fromCurrency: string;
|
||||||
|
toCurrency: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
|
||||||
|
inputGroupProps?: any;
|
||||||
|
formGroupProps?: any;
|
||||||
|
|
||||||
|
withPopoverRecalcConfirm?: boolean;
|
||||||
|
|
||||||
|
onRecalcConfirm: (bag: ExchangeRateValuesBag) => void;
|
||||||
|
onCancel: (bag: ExchangeRateValuesBag) => void;
|
||||||
|
|
||||||
|
isConfirmPopoverOpen?: boolean;
|
||||||
|
initialConfirmPopoverOpen?: boolean;
|
||||||
|
onConfirmPopoverOpen?: (isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function ExchangeRateInputGroup({
|
export function ExchangeRateInputGroup({
|
||||||
|
name,
|
||||||
fromCurrency,
|
fromCurrency,
|
||||||
toCurrency,
|
toCurrency,
|
||||||
|
isLoading,
|
||||||
|
|
||||||
inputGroupProps,
|
inputGroupProps,
|
||||||
formGroupProps,
|
formGroupProps,
|
||||||
name,
|
|
||||||
}) {
|
withPopoverRecalcConfirm = false,
|
||||||
|
|
||||||
|
onRecalcConfirm,
|
||||||
|
onCancel,
|
||||||
|
|
||||||
|
isConfirmPopoverOpen,
|
||||||
|
initialConfirmPopoverOpen,
|
||||||
|
onConfirmPopoverOpen,
|
||||||
|
}: ExchangeRateInputGroupProps) {
|
||||||
|
const [isOpen, handlePopoverOpen] = useUncontrolled<boolean>({
|
||||||
|
value: isConfirmPopoverOpen,
|
||||||
|
initialValue: initialConfirmPopoverOpen,
|
||||||
|
finalValue: false,
|
||||||
|
onChange: onConfirmPopoverOpen,
|
||||||
|
});
|
||||||
|
const { values, setFieldValue } = useFormikContext();
|
||||||
|
const [oldExchangeRate, setOldExchangeRate] = useState<string>('');
|
||||||
|
|
||||||
|
const exchangeRate = values[name];
|
||||||
|
const exchangeRateValuesBag: ExchangeRateValuesBag = {
|
||||||
|
exchangeRate,
|
||||||
|
oldExchangeRate,
|
||||||
|
};
|
||||||
|
// Handle re-calc confirm button click.
|
||||||
|
const handleRecalcConfirmBtn = () => {
|
||||||
|
handlePopoverOpen(false);
|
||||||
|
onRecalcConfirm && onRecalcConfirm(exchangeRateValuesBag);
|
||||||
|
};
|
||||||
|
// Handle cancel button click.
|
||||||
|
const handleCancelBtn = () => {
|
||||||
|
handlePopoverOpen(false);
|
||||||
|
onCancel && onCancel(exchangeRateValuesBag);
|
||||||
|
};
|
||||||
|
// Handle exchange rate field blur.
|
||||||
|
const handleExchangeRateFieldBlur = (value: string) => {
|
||||||
|
if (value !== values[name]) {
|
||||||
|
handlePopoverOpen(true);
|
||||||
|
setFieldValue(name, value);
|
||||||
|
setOldExchangeRate(values[name]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeRateField = (
|
||||||
|
<ExchangeRateField
|
||||||
|
allowDecimals={true}
|
||||||
|
allowNegativeValue={true}
|
||||||
|
asyncControl={true}
|
||||||
|
onChange={() => null}
|
||||||
|
onBlur={handleExchangeRateFieldBlur}
|
||||||
|
rightElement={isLoading && <Spinner size={16} />}
|
||||||
|
decimalsLimit={5}
|
||||||
|
{...inputGroupProps}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const popoverConfirmContent = (
|
||||||
|
<PopoverContent>
|
||||||
|
<p>
|
||||||
|
Are you want to re-calculate item prices based on this exchange rate.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: 15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
intent={Intent.WARNING}
|
||||||
|
className={Classes.POPOVER_DISMISS}
|
||||||
|
onClick={handleRecalcConfirmBtn}
|
||||||
|
small
|
||||||
|
>
|
||||||
|
Calculate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={Classes.POPOVER_DISMISS}
|
||||||
|
style={{ marginRight: 10 }}
|
||||||
|
onClick={handleCancelBtn}
|
||||||
|
small
|
||||||
|
minimal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FFormGroup inline={true} {...formGroupProps} name={name}>
|
<FFormGroup inline={true} {...formGroupProps} name={name}>
|
||||||
<ControlGroup>
|
<ControlGroup>
|
||||||
<ExchangeRatePrepend>
|
<ExchangeRatePrepend>
|
||||||
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
|
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
|
||||||
</ExchangeRatePrepend>
|
</ExchangeRatePrepend>
|
||||||
<ExchangeRateField
|
|
||||||
allowDecimals={true}
|
{withPopoverRecalcConfirm ? (
|
||||||
allowNegativeValue={true}
|
<Popover
|
||||||
{...inputGroupProps}
|
isOpen={isOpen}
|
||||||
name={name}
|
content={popoverConfirmContent}
|
||||||
/>
|
position={Position.RIGHT}
|
||||||
|
>
|
||||||
|
{exchangeRateField}
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
exchangeRateField
|
||||||
|
)}
|
||||||
<ExchangeRateAppend>
|
<ExchangeRateAppend>
|
||||||
<ExchangeFlagIcon currencyCode={toCurrency} /> {toCurrency}
|
<ExchangeFlagIcon currencyCode={toCurrency} /> {toCurrency}
|
||||||
</ExchangeRateAppend>
|
</ExchangeRateAppend>
|
||||||
@@ -34,7 +163,7 @@ export function ExchangeRateInputGroup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ExchangeRateField = styled(FMoneyInputGroup)`
|
const ExchangeRateField = styled(FMoneyInputGroup)`
|
||||||
max-width: 75px;
|
max-width: 85px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ExchangeRateSideIcon = styled.div`
|
const ExchangeRateSideIcon = styled.div`
|
||||||
@@ -57,3 +186,8 @@ const ExchangeFlagIcon = styled(FlagIcon)`
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const PopoverContent = styled('div')`
|
||||||
|
padding: 20px;
|
||||||
|
width: 300px;
|
||||||
|
`;
|
||||||
|
|||||||
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
52
packages/webapp/src/components/Forms/FRichEditor.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FieldConfig, FieldProps } from 'formik';
|
||||||
|
import { Field } from '@blueprintjs-formik/core';
|
||||||
|
import { RichEditor, RichEditorProps } from '../../components/RichEditor';
|
||||||
|
|
||||||
|
export interface FRichEditorProps
|
||||||
|
extends Omit<FieldConfig, 'children' | 'component' | 'as'>,
|
||||||
|
RichEditorProps {
|
||||||
|
name: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldToRichEditorProps
|
||||||
|
extends FieldProps,
|
||||||
|
Omit<RichEditorProps, 'form'> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes the field props to `RichEditor` props.
|
||||||
|
* @param {FieldToRichEditorProps}
|
||||||
|
* @returns {HTMLSelectProps}
|
||||||
|
*/
|
||||||
|
function fieldToRichEditor({
|
||||||
|
field: { onBlur: onFieldBlur, ...field },
|
||||||
|
form: { touched, errors, ...form },
|
||||||
|
...props
|
||||||
|
}: FieldToRichEditorProps): RichEditorProps {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
...props,
|
||||||
|
onChange: (value: string) => {
|
||||||
|
form.setFieldValue(field.name, value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformes field props to `RichEditor` props.
|
||||||
|
* @param {FieldToRichEditorProps}
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
function FieldToRichEditor({ ...props }: FieldToRichEditorProps): JSX.Element {
|
||||||
|
return <RichEditor {...fieldToRichEditor(props)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rich editor wrapper to bind with Formik.
|
||||||
|
* @param {FRichEditorProps} props -
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export function FRichEditor({ ...props }: FRichEditorProps): JSX.Element {
|
||||||
|
return <Field {...props} component={FieldToRichEditor} />;
|
||||||
|
}
|
||||||
@@ -4,4 +4,5 @@ export * from './FMoneyInputGroup';
|
|||||||
export * from './BlueprintFormik';
|
export * from './BlueprintFormik';
|
||||||
export * from './InputPrependText';
|
export * from './InputPrependText';
|
||||||
export * from './InputPrependButton';
|
export * from './InputPrependButton';
|
||||||
export * from './MoneyInputGroup';
|
export * from './MoneyInputGroup';
|
||||||
|
export * from './FRichEditor';
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* Basic editor styles */
|
||||||
|
.tiptap {
|
||||||
|
color: #222;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
>*+* {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: rgba(#ffffff, 0.1);
|
||||||
|
color: rgba(#ffffff, 0.6);
|
||||||
|
border: 1px solid rgba(#ffffff, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: rgba(#ffffff, 0.1);
|
||||||
|
font-family: "JetBrainsMono", monospace;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid rgba(#ffffff, 0.4);
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid rgba(#ffffff, 0.1);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
58
packages/webapp/src/components/RichEditor/RichEditor.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { Color } from '@tiptap/extension-color';
|
||||||
|
import ListItem from '@tiptap/extension-list-item';
|
||||||
|
import TextStyle from '@tiptap/extension-text-style';
|
||||||
|
import { EditorProvider } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import { useUncontrolled } from '@/hooks/useUncontrolled';
|
||||||
|
import { Box } from '../Layout/Box';
|
||||||
|
import './RichEditor.style.scss';
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||||
|
TextStyle.configure({ types: [ListItem.name] }),
|
||||||
|
StarterKit.configure({
|
||||||
|
bulletList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
orderedList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface RichEditorProps {
|
||||||
|
value?: string;
|
||||||
|
initialValue?: string;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
export const RichEditor = ({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
className,
|
||||||
|
}: RichEditorProps) => {
|
||||||
|
const [content, handleChange] = useUncontrolled({
|
||||||
|
value,
|
||||||
|
initialValue,
|
||||||
|
onChange,
|
||||||
|
finalValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleBlur = ({ editor }) => {
|
||||||
|
handleChange(editor.getHTML());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className={className}>
|
||||||
|
<EditorProvider
|
||||||
|
extensions={extensions}
|
||||||
|
content={content}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
1
packages/webapp/src/components/RichEditor/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './RichEditor';
|
||||||
@@ -48,4 +48,9 @@ export enum DialogsName {
|
|||||||
ProjectBillableEntriesForm = 'project-billable-entries',
|
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||||
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
||||||
TaxRateForm = 'tax-rate-form',
|
TaxRateForm = 'tax-rate-form',
|
||||||
|
InvoiceExchangeRateChangeNotice = 'InvoiceExchangeRateChangeNotice',
|
||||||
|
InvoiceMail = 'invoice-mail',
|
||||||
|
EstimateMail = 'estimate-mail',
|
||||||
|
ReceiptMail = 'receipt-mail',
|
||||||
|
PaymentMail = 'payment-mail',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function AccountsDataTable({
|
|||||||
// #withAlertsDialog
|
// #withAlertsDialog
|
||||||
openAlert,
|
openAlert,
|
||||||
|
|
||||||
// #withDial
|
// #withDialog
|
||||||
openDialog,
|
openDialog,
|
||||||
|
|
||||||
// #withDrawerActions
|
// #withDrawerActions
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import PaymentMadesAlerts from '@/containers/Purchases/PaymentMades/PaymentMades
|
|||||||
import CustomersAlerts from '@/containers/Customers/CustomersAlerts';
|
import CustomersAlerts from '@/containers/Customers/CustomersAlerts';
|
||||||
import VendorsAlerts from '@/containers/Vendors/VendorsAlerts';
|
import VendorsAlerts from '@/containers/Vendors/VendorsAlerts';
|
||||||
import ManualJournalsAlerts from '@/containers/Accounting/JournalsLanding/ManualJournalsAlerts';
|
import ManualJournalsAlerts from '@/containers/Accounting/JournalsLanding/ManualJournalsAlerts';
|
||||||
import ExchangeRatesAlerts from '@/containers/ExchangeRates/ExchangeRatesAlerts';
|
|
||||||
import ExpensesAlerts from '@/containers/Expenses/ExpensesAlerts';
|
import ExpensesAlerts from '@/containers/Expenses/ExpensesAlerts';
|
||||||
import AccountTransactionsAlerts from '@/containers/CashFlow/AccountTransactions/AccountTransactionsAlerts';
|
import AccountTransactionsAlerts from '@/containers/CashFlow/AccountTransactions/AccountTransactionsAlerts';
|
||||||
import UsersAlerts from '@/containers/Preferences/Users/UsersAlerts';
|
import UsersAlerts from '@/containers/Preferences/Users/UsersAlerts';
|
||||||
@@ -41,7 +40,6 @@ export default [
|
|||||||
...CustomersAlerts,
|
...CustomersAlerts,
|
||||||
...VendorsAlerts,
|
...VendorsAlerts,
|
||||||
...ManualJournalsAlerts,
|
...ManualJournalsAlerts,
|
||||||
...ExchangeRatesAlerts,
|
|
||||||
...ExpensesAlerts,
|
...ExpensesAlerts,
|
||||||
...AccountTransactionsAlerts,
|
...AccountTransactionsAlerts,
|
||||||
...UsersAlerts,
|
...UsersAlerts,
|
||||||
@@ -54,5 +52,5 @@ export default [
|
|||||||
...WarehousesTransfersAlerts,
|
...WarehousesTransfersAlerts,
|
||||||
...BranchesAlerts,
|
...BranchesAlerts,
|
||||||
...ProjectAlerts,
|
...ProjectAlerts,
|
||||||
...TaxRatesAlerts
|
...TaxRatesAlerts,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
|||||||
import authenticationRoutes from '@/routes/authentication';
|
import authenticationRoutes from '@/routes/authentication';
|
||||||
import { Icon, FormattedMessage as T } from '@/components';
|
import { Icon, FormattedMessage as T } from '@/components';
|
||||||
import { useIsAuthenticated } from '@/hooks/state';
|
import { useIsAuthenticated } from '@/hooks/state';
|
||||||
|
import { AuthMetaBootProvider } from './AuthMetaBoot';
|
||||||
|
|
||||||
import '@/style/pages/Authentication/Auth.scss';
|
import '@/style/pages/Authentication/Auth.scss';
|
||||||
import { AuthMetaBootProvider } from './AuthMetaBoot';
|
|
||||||
|
|
||||||
export function Authentication() {
|
export function Authentication() {
|
||||||
const to = { pathname: '/' };
|
const to = { pathname: '/' };
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const Schema = Yup.object().shape({
|
|||||||
billing_address_2: Yup.string().trim(),
|
billing_address_2: Yup.string().trim(),
|
||||||
billing_address_city: Yup.string().trim(),
|
billing_address_city: Yup.string().trim(),
|
||||||
billing_address_state: Yup.string().trim(),
|
billing_address_state: Yup.string().trim(),
|
||||||
billing_address_postcode: Yup.number().nullable(),
|
billing_address_postcode: Yup.string().nullable(),
|
||||||
billing_address_phone: Yup.number(),
|
billing_address_phone: Yup.number(),
|
||||||
|
|
||||||
shipping_address_country: Yup.string().trim(),
|
shipping_address_country: Yup.string().trim(),
|
||||||
@@ -37,7 +37,7 @@ const Schema = Yup.object().shape({
|
|||||||
shipping_address_2: Yup.string().trim(),
|
shipping_address_2: Yup.string().trim(),
|
||||||
shipping_address_city: Yup.string().trim(),
|
shipping_address_city: Yup.string().trim(),
|
||||||
shipping_address_state: Yup.string().trim(),
|
shipping_address_state: Yup.string().trim(),
|
||||||
shipping_address_postcode: Yup.number().nullable(),
|
shipping_address_postcode: Yup.string().nullable(),
|
||||||
shipping_address_phone: Yup.number(),
|
shipping_address_phone: Yup.number(),
|
||||||
|
|
||||||
opening_balance: Yup.number().nullable(),
|
opening_balance: Yup.number().nullable(),
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import * as Yup from 'yup';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
|
|
||||||
|
|
||||||
const Schema = Yup.object().shape({
|
|
||||||
exchange_rate: Yup.number()
|
|
||||||
.required()
|
|
||||||
.label(intl.get('exchange_rate_')),
|
|
||||||
currency_code: Yup.string()
|
|
||||||
.max(3)
|
|
||||||
.required(intl.get('currency_code_')),
|
|
||||||
date: Yup.date()
|
|
||||||
.required()
|
|
||||||
.label(intl.get('date')),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CreateExchangeRateFormSchema = Schema;
|
|
||||||
export const EditExchangeRateFormSchema = Schema;
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { useMemo } from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { Intent } from '@blueprintjs/core';
|
|
||||||
import { Formik } from 'formik';
|
|
||||||
import { AppToaster } from '@/components';
|
|
||||||
import {
|
|
||||||
CreateExchangeRateFormSchema,
|
|
||||||
EditExchangeRateFormSchema,
|
|
||||||
} from './ExchangeRateForm.schema';
|
|
||||||
import ExchangeRateFormContent from './ExchangeRateFormContent';
|
|
||||||
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
|
|
||||||
import { compose, transformToForm } from '@/utils';
|
|
||||||
|
|
||||||
const defaultInitialValues = {
|
|
||||||
exchange_rate: '',
|
|
||||||
currency_code: '',
|
|
||||||
date: moment(new Date()).format('YYYY-MM-DD'),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange rate form.
|
|
||||||
*/
|
|
||||||
function ExchangeRateForm({
|
|
||||||
// #withDialogActions
|
|
||||||
closeDialog,
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
createExchangeRateMutate,
|
|
||||||
editExchangeRateMutate,
|
|
||||||
isNewMode,
|
|
||||||
dialogName,
|
|
||||||
exchangeRate,
|
|
||||||
} = useExchangeRateFromContext();
|
|
||||||
|
|
||||||
// Form validation schema in create and edit mode.
|
|
||||||
const validationSchema = isNewMode
|
|
||||||
? CreateExchangeRateFormSchema
|
|
||||||
: EditExchangeRateFormSchema;
|
|
||||||
const initialValues = useMemo(
|
|
||||||
() => ({
|
|
||||||
...defaultInitialValues,
|
|
||||||
...transformToForm(exchangeRate, defaultInitialValues),
|
|
||||||
}),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Transformers response errors.
|
|
||||||
const transformErrors = (errors, { setErrors }) => {
|
|
||||||
if (
|
|
||||||
errors.find((error) => error.type === 'EXCHANGE.RATE.DATE.PERIOD.DEFINED')
|
|
||||||
) {
|
|
||||||
setErrors({
|
|
||||||
exchange_rate: intl.get(
|
|
||||||
'there_is_exchange_rate_in_this_date_with_the_same_currency',
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle the form submit.
|
|
||||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
|
||||||
setSubmitting(true);
|
|
||||||
|
|
||||||
// Handle close the dialog after success response.
|
|
||||||
const afterSubmit = () => {
|
|
||||||
closeDialog(dialogName);
|
|
||||||
};
|
|
||||||
const onSuccess = ({ response }) => {
|
|
||||||
AppToaster.show({
|
|
||||||
message: intl.get(
|
|
||||||
!isNewMode
|
|
||||||
? 'the_exchange_rate_has_been_edited_successfully'
|
|
||||||
: 'the_exchange_rate_has_been_created_successfully',
|
|
||||||
),
|
|
||||||
intent: Intent.SUCCESS,
|
|
||||||
});
|
|
||||||
afterSubmit(response);
|
|
||||||
};
|
|
||||||
// Handle the response error.
|
|
||||||
const onError = (error) => {
|
|
||||||
const {
|
|
||||||
response: {
|
|
||||||
data: { errors },
|
|
||||||
},
|
|
||||||
} = error;
|
|
||||||
|
|
||||||
transformErrors(errors, { setErrors });
|
|
||||||
setSubmitting(false);
|
|
||||||
};
|
|
||||||
if (isNewMode) {
|
|
||||||
createExchangeRateMutate(values).then(onSuccess).catch(onError);
|
|
||||||
} else {
|
|
||||||
editExchangeRateMutate([exchangeRate.id, values])
|
|
||||||
.then(onSuccess)
|
|
||||||
.catch(onError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
validationSchema={validationSchema}
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={handleFormSubmit}
|
|
||||||
>
|
|
||||||
<ExchangeRateFormContent />
|
|
||||||
</Formik>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(withDialogActions)(ExchangeRateForm);
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { Form } from 'formik';
|
|
||||||
import ExchangeRateFormFields from './ExchangeRateFormFields';
|
|
||||||
import ExchangeRateFormFooter from './ExchangeRateFormFooter';
|
|
||||||
|
|
||||||
export default function ExchangeRateFormContent() {
|
|
||||||
return (
|
|
||||||
<Form>
|
|
||||||
<ExchangeRateFormFields />
|
|
||||||
<ExchangeRateFormFooter />
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import ExchangeRateForm from './ExchangeRateForm';
|
|
||||||
import { ExchangeRateFormProvider } from './ExchangeRateFormProvider';
|
|
||||||
|
|
||||||
import '@/style/pages/ExchangeRate/ExchangeRateDialog.scss';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange rate form content.
|
|
||||||
*/
|
|
||||||
export default function ExchangeRateFormDialogContent({
|
|
||||||
// #ownProp
|
|
||||||
action,
|
|
||||||
exchangeRateId,
|
|
||||||
dialogName,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ExchangeRateFormProvider
|
|
||||||
dialogName={dialogName}
|
|
||||||
exchangeRate={exchangeRateId}
|
|
||||||
action={action}
|
|
||||||
>
|
|
||||||
<ExchangeRateForm />
|
|
||||||
</ExchangeRateFormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { Classes, FormGroup, InputGroup, Position } from '@blueprintjs/core';
|
|
||||||
import { FastField } from 'formik';
|
|
||||||
import { DateInput } from '@blueprintjs/datetime';
|
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
momentFormatter,
|
|
||||||
tansformDateValue,
|
|
||||||
handleDateChange,
|
|
||||||
inputIntent,
|
|
||||||
} from '@/utils';
|
|
||||||
import {
|
|
||||||
ErrorMessage,
|
|
||||||
FieldRequiredHint,
|
|
||||||
CurrencySelectList,
|
|
||||||
} from '@/components';
|
|
||||||
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
|
|
||||||
|
|
||||||
|
|
||||||
export default function ExchangeRateFormFields() {
|
|
||||||
const { action, currencies } = useExchangeRateFromContext();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={Classes.DIALOG_BODY}>
|
|
||||||
{/* ----------- Date ----------- */}
|
|
||||||
<FastField name={'date'}>
|
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'date'} />}
|
|
||||||
labelInfo={FieldRequiredHint}
|
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
helperText={<ErrorMessage name="date" />}
|
|
||||||
inline={true}
|
|
||||||
>
|
|
||||||
<DateInput
|
|
||||||
{...momentFormatter('YYYY/MM/DD')}
|
|
||||||
value={tansformDateValue(value)}
|
|
||||||
onChange={handleDateChange((formattedDate) => {
|
|
||||||
form.setFieldValue('date', formattedDate);
|
|
||||||
})}
|
|
||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
|
||||||
disabled={action === 'edit'}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</FastField>
|
|
||||||
{/* ----------- Currency Code ----------- */}
|
|
||||||
<FastField name={'currency_code'}>
|
|
||||||
{({ form, field: { value }, meta: { error, touched } }) => (
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'currency_code'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
className={classNames('form-group--currency', Classes.FILL)}
|
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
helperText={<ErrorMessage name="currency_code" />}
|
|
||||||
inline={true}
|
|
||||||
>
|
|
||||||
<CurrencySelectList
|
|
||||||
currenciesList={currencies}
|
|
||||||
selectedCurrencyCode={value}
|
|
||||||
onCurrencySelected={({ currency_code }) => {
|
|
||||||
form.setFieldValue('currency_code', currency_code);
|
|
||||||
}}
|
|
||||||
disabled={action === 'edit'}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</FastField>
|
|
||||||
|
|
||||||
{/*------------ Exchange Rate -----------*/}
|
|
||||||
<FastField name={'exchange_rate'}>
|
|
||||||
{({ form, field, meta: { error, touched } }) => (
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'exchange_rate'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
intent={inputIntent({ error, touched })}
|
|
||||||
helperText={<ErrorMessage name="exchange_rate" />}
|
|
||||||
inline={true}
|
|
||||||
>
|
|
||||||
<InputGroup intent={inputIntent({ error, touched })} {...field} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
</FastField>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
import { useFormikContext } from 'formik';
|
|
||||||
|
|
||||||
import { Button, Classes, Intent } from '@blueprintjs/core';
|
|
||||||
import { FormattedMessage as T } from '@/components';
|
|
||||||
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
function ExchangeRateFormFooter({
|
|
||||||
// #withDialogActions
|
|
||||||
closeDialog,
|
|
||||||
}) {
|
|
||||||
const { isSubmitting } = useFormikContext();
|
|
||||||
const { dialogName, action } = useExchangeRateFromContext();
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
closeDialog(dialogName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={Classes.DIALOG_FOOTER}>
|
|
||||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
|
||||||
<Button onClick={handleClose}>
|
|
||||||
<T id={'close'} />
|
|
||||||
</Button>
|
|
||||||
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
|
|
||||||
{action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(withDialogActions)(ExchangeRateFormFooter);
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { createContext, useContext } from 'react';
|
|
||||||
import {
|
|
||||||
useCreateExchangeRate,
|
|
||||||
useEdiExchangeRate,
|
|
||||||
useCurrencies,
|
|
||||||
useExchangeRates,
|
|
||||||
} from '@/hooks/query';
|
|
||||||
import { DialogContent } from '@/components';
|
|
||||||
|
|
||||||
const ExchangeRateFormContext = createContext();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange rate Form page provider.
|
|
||||||
*/
|
|
||||||
function ExchangeRateFormProvider({
|
|
||||||
exchangeRate,
|
|
||||||
action,
|
|
||||||
dialogName,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
// Create and edit exchange rate mutations.
|
|
||||||
const { mutateAsync: createExchangeRateMutate } = useCreateExchangeRate();
|
|
||||||
const { mutateAsync: editExchangeRateMutate } = useEdiExchangeRate();
|
|
||||||
|
|
||||||
// Load Currencies list.
|
|
||||||
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
|
|
||||||
const { isFetching: isExchangeRatesLoading } = useExchangeRates();
|
|
||||||
|
|
||||||
const isNewMode = !exchangeRate;
|
|
||||||
|
|
||||||
// Provider state.
|
|
||||||
const provider = {
|
|
||||||
createExchangeRateMutate,
|
|
||||||
editExchangeRateMutate,
|
|
||||||
dialogName,
|
|
||||||
exchangeRate,
|
|
||||||
action,
|
|
||||||
currencies,
|
|
||||||
isExchangeRatesLoading,
|
|
||||||
isNewMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogContent isLoading={isCurrenciesLoading} name={'exchange-rate-form'}>
|
|
||||||
<ExchangeRateFormContext.Provider value={provider} {...props} />
|
|
||||||
</DialogContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const useExchangeRateFromContext = () => useContext(ExchangeRateFormContext);
|
|
||||||
|
|
||||||
export { ExchangeRateFormProvider, useExchangeRateFromContext };
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { lazy } from 'react';
|
|
||||||
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
|
|
||||||
import withDialogRedux from '@/components/DialogReduxConnect';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
const ExchangeRateFormDialogContent = lazy(
|
|
||||||
() => import('./ExchangeRateFormDialogContent'),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange rate form dialog.
|
|
||||||
*/
|
|
||||||
function ExchangeRateFormDialog({
|
|
||||||
dialogName,
|
|
||||||
payload = { action: '', id: null, exchangeRate: '' },
|
|
||||||
isOpen,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
name={dialogName}
|
|
||||||
title={
|
|
||||||
payload.action === 'edit' ? (
|
|
||||||
<T id={'edit_exchange_rate'} />
|
|
||||||
) : (
|
|
||||||
<T id={'new_exchange_rate'} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={'dialog--exchangeRate-form'}
|
|
||||||
isOpen={isOpen}
|
|
||||||
autoFocus={true}
|
|
||||||
canEscapeKeyClose={true}
|
|
||||||
>
|
|
||||||
<DialogSuspense>
|
|
||||||
<ExchangeRateFormDialogContent
|
|
||||||
dialogName={dialogName}
|
|
||||||
action={payload.action}
|
|
||||||
exchangeRateId={payload.exchangeRate}
|
|
||||||
/>
|
|
||||||
</DialogSuspense>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(withDialogRedux())(ExchangeRateFormDialog);
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -22,10 +21,12 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
Can,
|
Can,
|
||||||
|
Choose,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate read-only details actions bar of the drawer.
|
* Estimate read-only details actions bar of the drawer.
|
||||||
@@ -51,7 +52,6 @@ function EstimateDetailActionsBar({
|
|||||||
history.push(`/estimates/${estimateId}/edit`);
|
history.push(`/estimates/${estimateId}/edit`);
|
||||||
closeDrawer(DRAWERS.ESTIMATE_DETAILS);
|
closeDrawer(DRAWERS.ESTIMATE_DETAILS);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete sale estimate.
|
// Handle delete sale estimate.
|
||||||
const handleDeleteEstimate = () => {
|
const handleDeleteEstimate = () => {
|
||||||
openAlert('estimate-delete', { estimateId });
|
openAlert('estimate-delete', { estimateId });
|
||||||
@@ -65,6 +65,10 @@ function EstimateDetailActionsBar({
|
|||||||
const handleNotifyViaSMS = () => {
|
const handleNotifyViaSMS = () => {
|
||||||
openDialog('notify-estimate-via-sms', { estimateId });
|
openDialog('notify-estimate-via-sms', { estimateId });
|
||||||
};
|
};
|
||||||
|
// Handles the estimate mail dialog.
|
||||||
|
const handleMailEstimate = () => {
|
||||||
|
openDialog(DialogsName.EstimateMail, { estimateId });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerActionsBar>
|
<DrawerActionsBar>
|
||||||
@@ -78,13 +82,21 @@ function EstimateDetailActionsBar({
|
|||||||
/>
|
/>
|
||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
|
|
||||||
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
|
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="envelope" />}
|
||||||
|
text={'Send Mail'}
|
||||||
|
onClick={handleMailEstimate}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="print-16" />}
|
icon={<Icon icon="print-16" />}
|
||||||
text={<T id={'print'} />}
|
text={<T id={'print'} />}
|
||||||
onClick={handlePrintEstimate}
|
onClick={handlePrintEstimate}
|
||||||
/>
|
/>
|
||||||
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
<Can I={SaleEstimateAction.Delete} a={AbilitySubject.Estimate}>
|
<Can I={SaleEstimateAction.Delete} a={AbilitySubject.Estimate}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import {
|
import {
|
||||||
Intent,
|
Intent,
|
||||||
Button,
|
Button,
|
||||||
@@ -9,9 +8,15 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Menu,
|
Menu,
|
||||||
Tag,
|
Tag,
|
||||||
|
MenuDivider,
|
||||||
|
Classes,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
|
||||||
import { Icon, T, Choose } from '@/components';
|
import { Icon, T, Choose, Can } from '@/components';
|
||||||
|
import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption';
|
||||||
|
import withAlertsActions from '@/containers/Alert/withAlertActions';
|
||||||
|
import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate details status.
|
* Estimate details status.
|
||||||
@@ -49,25 +54,85 @@ export function EstimateDetailsStatus({ estimate }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EstimateMoreMenuItems({ payload: { onNotifyViaSMS } }) {
|
export const EstimateMoreMenuItems = R.compose(withAlertsActions)(
|
||||||
return (
|
({
|
||||||
<Popover
|
// # withAlertsActions,
|
||||||
minimal={true}
|
openAlert,
|
||||||
content={
|
|
||||||
<Menu>
|
// # rest
|
||||||
<MenuItem
|
payload: { onNotifyViaSMS },
|
||||||
onClick={onNotifyViaSMS}
|
}) => {
|
||||||
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
|
const { estimateId, estimate } = useEstimateDetailDrawerContext();
|
||||||
/>
|
|
||||||
</Menu>
|
// Handle cancel/confirm estimate approve.
|
||||||
}
|
const handleApproveEstimate = () => {
|
||||||
interactionKind={PopoverInteractionKind.CLICK}
|
openAlert('estimate-Approve', { estimateId });
|
||||||
position={Position.BOTTOM_LEFT}
|
};
|
||||||
modifiers={{
|
// Handle cancel/confirm estimate reject.
|
||||||
offset: { offset: '0, 4' },
|
const handleRejectEstimate = () => {
|
||||||
}}
|
openAlert('estimate-reject', { estimateId });
|
||||||
>
|
};
|
||||||
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
|
|
||||||
</Popover>
|
return (
|
||||||
);
|
<Popover
|
||||||
}
|
minimal={true}
|
||||||
|
content={
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
onClick={onNotifyViaSMS}
|
||||||
|
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
|
||||||
|
/>
|
||||||
|
<MenuDivider />
|
||||||
|
<Choose>
|
||||||
|
<Choose.When
|
||||||
|
condition={estimate.is_delivered && estimate.is_rejected}
|
||||||
|
>
|
||||||
|
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
|
||||||
|
<MenuItem
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={<T id={'mark_as_approved'} />}
|
||||||
|
onClick={handleApproveEstimate}
|
||||||
|
/>
|
||||||
|
</Can>
|
||||||
|
</Choose.When>
|
||||||
|
<Choose.When
|
||||||
|
condition={estimate.is_delivered && estimate.is_approved}
|
||||||
|
>
|
||||||
|
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
|
||||||
|
<MenuItem
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={<T id={'mark_as_rejected'} />}
|
||||||
|
onClick={handleRejectEstimate}
|
||||||
|
/>
|
||||||
|
</Can>
|
||||||
|
</Choose.When>
|
||||||
|
<Choose.When condition={estimate.is_delivered}>
|
||||||
|
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
|
||||||
|
<MenuItem
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={<T id={'mark_as_approved'} />}
|
||||||
|
onClick={handleApproveEstimate}
|
||||||
|
/>
|
||||||
|
</Can>
|
||||||
|
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
|
||||||
|
<MenuItem
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={<T id={'mark_as_rejected'} />}
|
||||||
|
onClick={handleRejectEstimate}
|
||||||
|
/>
|
||||||
|
</Can>
|
||||||
|
</Choose.When>
|
||||||
|
</Choose>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
|
position={Position.BOTTOM_LEFT}
|
||||||
|
modifiers={{
|
||||||
|
offset: { offset: '0, 4' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { BadDebtMenuItem } from './utils';
|
import { BadDebtMenuItem } from './utils';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoice details action bar.
|
* Invoice details action bar.
|
||||||
@@ -93,6 +94,10 @@ function InvoiceDetailActionsBar({
|
|||||||
openAlert('cancel-bad-debt', { invoiceId });
|
openAlert('cancel-bad-debt', { invoiceId });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMailInvoice = () => {
|
||||||
|
openDialog(DialogsName.InvoiceMail, { invoiceId });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerActionsBar>
|
<DrawerActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -117,12 +122,19 @@ function InvoiceDetailActionsBar({
|
|||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
<Can I={SaleInvoiceAction.View} a={AbilitySubject.Invoice}>
|
<Can I={SaleInvoiceAction.View} a={AbilitySubject.Invoice}>
|
||||||
|
<Button
|
||||||
|
text={'Send Mail'}
|
||||||
|
icon={<Icon icon="envelope" />}
|
||||||
|
onClick={handleMailInvoice}
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="print-16" />}
|
icon={<Icon icon="print-16" />}
|
||||||
text={<T id={'print'} />}
|
text={<T id={'print'} />}
|
||||||
onClick={handlePrintInvoice}
|
onClick={handlePrintInvoice}
|
||||||
/>
|
/>
|
||||||
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
<Can I={SaleInvoiceAction.Delete} a={AbilitySubject.Invoice}>
|
<Can I={SaleInvoiceAction.Delete} a={AbilitySubject.Invoice}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payment receive actions bar.
|
* Payment receive actions bar.
|
||||||
@@ -68,6 +69,10 @@ function PaymentReceiveActionsBar({
|
|||||||
openDialog('payment-pdf-preview', { paymentReceiveId });
|
openDialog('payment-pdf-preview', { paymentReceiveId });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMailPaymentReceive = () => {
|
||||||
|
openDialog(DialogsName.PaymentMail, { paymentReceiveId });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerActionsBar>
|
<DrawerActionsBar>
|
||||||
<NavbarGroup>
|
<NavbarGroup>
|
||||||
@@ -81,12 +86,19 @@ function PaymentReceiveActionsBar({
|
|||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
<Can I={PaymentReceiveAction.View} a={AbilitySubject.PaymentReceive}>
|
<Can I={PaymentReceiveAction.View} a={AbilitySubject.PaymentReceive}>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={'Send Mail'}
|
||||||
|
icon={<Icon icon="envelope" />}
|
||||||
|
onClick={handleMailPaymentReceive}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="print-16" />}
|
icon={<Icon icon="print-16" />}
|
||||||
text={<T id={'print'} />}
|
text={<T id={'print'} />}
|
||||||
onClick={handlePrintPaymentReceive}
|
onClick={handlePrintPaymentReceive}
|
||||||
/>
|
/>
|
||||||
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
<Can I={PaymentReceiveAction.Delete} a={AbilitySubject.PaymentReceive}>
|
<Can I={PaymentReceiveAction.Delete} a={AbilitySubject.PaymentReceive}>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useReceiptDetailDrawerContext } from './ReceiptDetailDrawerProvider';
|
|||||||
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
import { SaleReceiptAction, AbilitySubject } from '@/constants/abilityOption';
|
||||||
import { safeCallback, compose } from '@/utils';
|
import { safeCallback, compose } from '@/utils';
|
||||||
import { DRAWERS } from '@/constants/drawers';
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
|
import { DialogsName } from '@/constants/dialogs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Receipt details actions bar.
|
* Receipt details actions bar.
|
||||||
@@ -60,6 +61,9 @@ function ReceiptDetailActionBar({
|
|||||||
const handleNotifyViaSMS = () => {
|
const handleNotifyViaSMS = () => {
|
||||||
openDialog('notify-receipt-via-sms', { receiptId });
|
openDialog('notify-receipt-via-sms', { receiptId });
|
||||||
};
|
};
|
||||||
|
const handleReceiptMail = () => {
|
||||||
|
openDialog(DialogsName.ReceiptMail, { receiptId });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DrawerActionsBar>
|
<DrawerActionsBar>
|
||||||
@@ -74,6 +78,12 @@ function ReceiptDetailActionBar({
|
|||||||
<NavbarDivider />
|
<NavbarDivider />
|
||||||
</Can>
|
</Can>
|
||||||
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
|
<Can I={SaleReceiptAction.View} a={AbilitySubject.Receipt}>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
text={'Send Mail'}
|
||||||
|
icon={<Icon icon="envelope" />}
|
||||||
|
onClick={handleReceiptMail}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="print-16" />}
|
icon={<Icon icon="print-16" />}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useExchangeRate } from '@/hooks/query';
|
||||||
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface AutoExchangeRateProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoExchangeRateProviderValue {
|
||||||
|
autoExRateCurrency: string;
|
||||||
|
isAutoExchangeRateLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoExchangeRateContext = React.createContext(
|
||||||
|
{} as AutoExchangeRateProviderValue,
|
||||||
|
);
|
||||||
|
|
||||||
|
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
|
||||||
|
const [autoExRateCurrency, setAutoExRateCurrency] =
|
||||||
|
React.useState<string>('');
|
||||||
|
const currentOrganization = useCurrentOrganization();
|
||||||
|
|
||||||
|
// Retrieves the exchange rate.
|
||||||
|
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
|
||||||
|
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
|
||||||
|
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
staleTime: 0,
|
||||||
|
cacheTime: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
autoExRateCurrency,
|
||||||
|
setAutoExRateCurrency,
|
||||||
|
isAutoExchangeRateLoading,
|
||||||
|
autoExchangeRate,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoExchangeRateContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AutoExchangeRateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useAutoExRateContext = () => React.useContext(AutoExchangeRateContext);
|
||||||
|
|
||||||
|
export {
|
||||||
|
useAutoExRateContext,
|
||||||
|
AutoExchangeRateContext,
|
||||||
|
AutoExchangeRateProvider,
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from 'react';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { round } from 'lodash';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { updateItemsEntriesTotal } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the given rate to the local currency.
|
||||||
|
* @param {number} rate
|
||||||
|
* @param {number} exchangeRate
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const convertToForeignCurrency = (
|
||||||
|
rate: number,
|
||||||
|
exchangeRate: number,
|
||||||
|
) => {
|
||||||
|
return rate * exchangeRate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given rate to the base currency.
|
||||||
|
* @param {number} rate
|
||||||
|
* @param {number} exchangeRate
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
|
||||||
|
return rate / exchangeRate;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts the given rate from the old exchange rate and covert it to the new
|
||||||
|
* currency based on the given new exchange rate.
|
||||||
|
* @param {number} rate -
|
||||||
|
* @param {number} oldExchangeRate - Old exchange rate.
|
||||||
|
* @param {number} newExchangeRate - New exchange rate.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
const revertAndConvertExchangeRate = (
|
||||||
|
rate: number,
|
||||||
|
oldExchangeRate: number,
|
||||||
|
newExchangeRate: number,
|
||||||
|
) => {
|
||||||
|
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
|
||||||
|
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
|
||||||
|
|
||||||
|
return round(newValue, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign the new item entry rate after converting to the new exchange rate.
|
||||||
|
* @params {number} oldExchangeRate -
|
||||||
|
* @params {number} newExchangeRate -
|
||||||
|
* @params {IItemEntry} entries -
|
||||||
|
*/
|
||||||
|
const assignRateRevertAndCovertExchangeRate = R.curry(
|
||||||
|
(oldExchangeRate: number, newExchangeRate: number, entries: IITemEntry[]) => {
|
||||||
|
return entries.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
rate: revertAndConvertExchangeRate(
|
||||||
|
entry.rate,
|
||||||
|
oldExchangeRate,
|
||||||
|
newExchangeRate,
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates items entries on exchange rate change.
|
||||||
|
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
|
||||||
|
*/
|
||||||
|
export const useUpdateEntriesOnExchangeRateChange = () => {
|
||||||
|
const {
|
||||||
|
values: { entries },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
return React.useMemo(() => {
|
||||||
|
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
|
||||||
|
return R.compose(
|
||||||
|
// Updates entries total.
|
||||||
|
updateItemsEntriesTotal,
|
||||||
|
// Assign a new rate of the given new exchange rate from the old exchange rate.
|
||||||
|
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
|
||||||
|
)(entries);
|
||||||
|
});
|
||||||
|
}, [entries]);
|
||||||
|
};
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { useUpdateEntriesOnExchangeRateChange } from './useUpdateEntriesOnExchangeRateChange';
|
||||||
|
import { useAutoExRateContext } from './AutoExchangeProvider';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useCurrentOrganization } from '@/hooks/state';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-calculate the item entries prices based on the old exchange rate.
|
||||||
|
* @param {InvoiceExchangeRateInputFieldRoot} Component
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export const withExchangeRateItemEntriesPriceRecalc =
|
||||||
|
(Component) => (props) => {
|
||||||
|
const { setFieldValue } = useFormikContext();
|
||||||
|
const updateChangeExRate = useUpdateEntriesOnExchangeRateChange();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
|
||||||
|
setFieldValue(
|
||||||
|
'entries',
|
||||||
|
updateChangeExRate(oldExchangeRate, exchangeRate),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the loading props to the exchange rate field.
|
||||||
|
* @param Component
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
export const withExchangeRateFetchingLoading = (Component) => (props) => {
|
||||||
|
const { isAutoExchangeRateLoading } = useAutoExRateContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
isLoading={isAutoExchangeRateLoading}
|
||||||
|
inputGroupProps={{
|
||||||
|
disabled: isAutoExchangeRateLoading,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the customer currency code and exchange rate once you update the customer
|
||||||
|
* then change the state to fetch the realtime exchange rate of the new selected currency.
|
||||||
|
*/
|
||||||
|
export const useCustomerUpdateExRate = () => {
|
||||||
|
const { setFieldValue, values } = useFormikContext();
|
||||||
|
const { setAutoExRateCurrency } = useAutoExRateContext();
|
||||||
|
|
||||||
|
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||||
|
const currentCompany = useCurrentOrganization();
|
||||||
|
|
||||||
|
const DEFAULT_EX_RATE = 1;
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(customer) => {
|
||||||
|
// Reset the auto exchange rate currency cycle.
|
||||||
|
setAutoExRateCurrency(null);
|
||||||
|
|
||||||
|
// If the customer's currency code equals the same base currency.
|
||||||
|
if (customer.currency_code === currentCompany.base_currency) {
|
||||||
|
setFieldValue('exchange_rate', DEFAULT_EX_RATE + '');
|
||||||
|
setFieldValue(
|
||||||
|
'entries',
|
||||||
|
updateEntriesOnExChange(values.exchange_rate, DEFAULT_EX_RATE),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Sets the currency code to fetch exchange rate of the given currency code.
|
||||||
|
setAutoExRateCurrency(customer?.currency_code);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
currentCompany.base_currency,
|
||||||
|
setAutoExRateCurrency,
|
||||||
|
setFieldValue,
|
||||||
|
updateEntriesOnExChange,
|
||||||
|
values.exchange_rate,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UseSyncExRateToFormProps {
|
||||||
|
onSynced?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the realtime exchange rate to the Formik form and then re-calculates
|
||||||
|
* the entries rate based on the given new and old ex. rate.
|
||||||
|
* @param {UseSyncExRateToFormProps} props -
|
||||||
|
* @returns {React.ReactNode}
|
||||||
|
*/
|
||||||
|
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
|
||||||
|
const { setFieldValue, values } = useFormikContext();
|
||||||
|
const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext();
|
||||||
|
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||||
|
|
||||||
|
// Sync the fetched real-time exchanage rate to the form.
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
|
||||||
|
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
|
||||||
|
setFieldValue(
|
||||||
|
'entries',
|
||||||
|
updateEntriesOnExChange(
|
||||||
|
values.exchange_rate,
|
||||||
|
autoExchangeRate?.exchange_rate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
onSynced?.();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { useCallback, useState, useMemo } from 'react';
|
|
||||||
import intl from 'react-intl-universal';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import {
|
|
||||||
NavbarGroup,
|
|
||||||
NavbarDivider,
|
|
||||||
Button,
|
|
||||||
Classes,
|
|
||||||
Intent,
|
|
||||||
Popover,
|
|
||||||
Position,
|
|
||||||
PopoverInteractionKind,
|
|
||||||
Alignment,
|
|
||||||
} from '@blueprintjs/core';
|
|
||||||
import {
|
|
||||||
Icon,
|
|
||||||
If,
|
|
||||||
DashboardActionsBar,
|
|
||||||
FormattedMessage as T,
|
|
||||||
} from '@/components';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { useRefreshExchangeRate } from '@/hooks/query/exchangeRates';
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
import withResourceDetail from '@/containers/Resources/withResourceDetails';
|
|
||||||
import withExchangeRatesActions from './withExchangeRatesActions';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange rate actions bar.
|
|
||||||
*/
|
|
||||||
function ExchangeRateActionsBar({
|
|
||||||
// #withDialogActions.
|
|
||||||
openDialog,
|
|
||||||
|
|
||||||
// #withResourceDetail
|
|
||||||
resourceFields,
|
|
||||||
|
|
||||||
//#withExchangeRatesActions
|
|
||||||
addExchangeRatesTableQueries,
|
|
||||||
|
|
||||||
// #ownProps
|
|
||||||
selectedRows = [],
|
|
||||||
onDeleteExchangeRate,
|
|
||||||
onFilterChanged,
|
|
||||||
onBulkDelete,
|
|
||||||
}) {
|
|
||||||
const [filterCount, setFilterCount] = useState(0);
|
|
||||||
|
|
||||||
const onClickNewExchangeRate = () => {
|
|
||||||
openDialog('exchangeRate-form', {});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Exchange rates refresh action.
|
|
||||||
const { refresh } = useRefreshExchangeRate();
|
|
||||||
|
|
||||||
// Handle click a refresh sale estimates
|
|
||||||
const handleRefreshBtnClick = () => {
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasSelectedRows = useMemo(
|
|
||||||
() => selectedRows.length > 0,
|
|
||||||
[selectedRows],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handelBulkDelete = useCallback(() => {
|
|
||||||
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
|
|
||||||
}, [onBulkDelete, selectedRows]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardActionsBar>
|
|
||||||
<NavbarGroup>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="plus" />}
|
|
||||||
text={<T id={'new_exchange_rate'} />}
|
|
||||||
onClick={onClickNewExchangeRate}
|
|
||||||
/>
|
|
||||||
<NavbarDivider />
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
minimal={true}
|
|
||||||
// content={filterDropdown}
|
|
||||||
interactionKind={PopoverInteractionKind.CLICK}
|
|
||||||
position={Position.BOTTOM_LEFT}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={classNames(Classes.MINIMAL, 'button--filter')}
|
|
||||||
text={
|
|
||||||
filterCount <= 0 ? (
|
|
||||||
<T id={'filter'} />
|
|
||||||
) : (
|
|
||||||
`${filterCount} ${intl.get('filters_applied')}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
icon={<Icon icon="filter-16" iconSize={16} />}
|
|
||||||
/>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<If condition={hasSelectedRows}>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="trash-16" iconSize={16} />}
|
|
||||||
text={<T id={'delete'} />}
|
|
||||||
intent={Intent.DANGER}
|
|
||||||
onClick={handelBulkDelete}
|
|
||||||
/>
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
|
||||||
text={<T id={'import'} />}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
|
||||||
text={<T id={'export'} />}
|
|
||||||
/>
|
|
||||||
</NavbarGroup>
|
|
||||||
<NavbarGroup align={Alignment.RIGHT}>
|
|
||||||
<Button
|
|
||||||
className={Classes.MINIMAL}
|
|
||||||
icon={<Icon icon="refresh-16" iconSize={14} />}
|
|
||||||
onClick={handleRefreshBtnClick}
|
|
||||||
/>
|
|
||||||
</NavbarGroup>
|
|
||||||
</DashboardActionsBar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
resourceName: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const withExchangeRateActionBar = connect(mapStateToProps);
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withExchangeRateActionBar,
|
|
||||||
withDialogActions,
|
|
||||||
withResourceDetail(({ resourceFields }) => ({
|
|
||||||
resourceFields,
|
|
||||||
})),
|
|
||||||
withExchangeRatesActions,
|
|
||||||
)(ExchangeRateActionsBar);
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DataTable,
|
|
||||||
TableSkeletonRows,
|
|
||||||
TableSkeletonHeader,
|
|
||||||
} from '@/components';
|
|
||||||
|
|
||||||
import { useExchangeRatesContext } from './ExchangeRatesProvider';
|
|
||||||
import { useExchangeRatesTableColumns, ActionMenuList } from './components';
|
|
||||||
|
|
||||||
import withExchangeRates from './withExchangeRates';
|
|
||||||
import withExchangeRatesActions from './withExchangeRatesActions';
|
|
||||||
|
|
||||||
import withDialogActions from '@/containers/Dialog/withDialogActions';
|
|
||||||
import withAlertActions from '@/containers/Alert/withAlertActions';
|
|
||||||
import { compose } from '@/utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange rates table.
|
|
||||||
*/
|
|
||||||
function ExchangeRateTable({
|
|
||||||
// #ownProps
|
|
||||||
tableProps,
|
|
||||||
|
|
||||||
// #withDialogActions.
|
|
||||||
openDialog,
|
|
||||||
|
|
||||||
// #withAlertActions
|
|
||||||
openAlert,
|
|
||||||
|
|
||||||
// #withExchangeRatesActions
|
|
||||||
setExchangeRateTableState,
|
|
||||||
|
|
||||||
// #withExchangeRates
|
|
||||||
exchangeRatesTableState,
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
isExchangeRatesFetching,
|
|
||||||
isExchangeRatesLoading,
|
|
||||||
|
|
||||||
exchangesRates,
|
|
||||||
pagination,
|
|
||||||
} = useExchangeRatesContext();
|
|
||||||
|
|
||||||
// Table columns.
|
|
||||||
const columns = useExchangeRatesTableColumns();
|
|
||||||
|
|
||||||
// Handle delete exchange rate.
|
|
||||||
const handleDeleteExchangeRate = ({ id }) => {
|
|
||||||
openAlert('exchange-rate-delete', { exchangeRateId: id });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle Edit exchange rate.
|
|
||||||
const handelEditExchangeRate = (exchangeRate) => {
|
|
||||||
openDialog('exchangeRate-form', {
|
|
||||||
action: 'edit',
|
|
||||||
exchangeRate: exchangeRate,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFetchData = useCallback(
|
|
||||||
({ pageSize, pageIndex, sortBy }) => {
|
|
||||||
setExchangeRateTableState({
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
sortBy,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[setExchangeRateTableState],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataTable
|
|
||||||
noInitialFetch={true}
|
|
||||||
columns={columns}
|
|
||||||
data={exchangesRates}
|
|
||||||
initialState={exchangeRatesTableState}
|
|
||||||
loading={isExchangeRatesLoading}
|
|
||||||
headerLoading={isExchangeRatesLoading}
|
|
||||||
progressBarLoading={isExchangeRatesFetching}
|
|
||||||
selectionColumn={true}
|
|
||||||
expandable={true}
|
|
||||||
sticky={true}
|
|
||||||
manualSortBy={true}
|
|
||||||
onFetchData={handleFetchData}
|
|
||||||
pagination={true}
|
|
||||||
manualPagination={true}
|
|
||||||
pagesCount={pagination.pagesCount}
|
|
||||||
TableLoadingRenderer={TableSkeletonRows}
|
|
||||||
TableHeaderSkeletonRenderer={TableSkeletonHeader}
|
|
||||||
ContextMenu={ActionMenuList}
|
|
||||||
payload={{
|
|
||||||
onDeleteExchangeRate: handleDeleteExchangeRate,
|
|
||||||
onEditExchangeRate: handelEditExchangeRate,
|
|
||||||
}}
|
|
||||||
{...tableProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withDialogActions,
|
|
||||||
withAlertActions,
|
|
||||||
withExchangeRates(({ exchangeRatesTableState }) => ({
|
|
||||||
exchangeRatesTableState,
|
|
||||||
})),
|
|
||||||
withExchangeRatesActions,
|
|
||||||
)(ExchangeRateTable);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const ExchangeRateDeleteAlert = React.lazy(
|
|
||||||
() => import('@/containers/Alerts/ExchangeRates/ExchangeRateDeleteAlert'),
|
|
||||||
);
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{ name: 'exchange-rate-delete', component: ExchangeRateDeleteAlert },
|
|
||||||
];
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
// @ts-nocheck
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { DashboardContentTable, DashboardPageContent } from '@/components';
|
|
||||||
|
|
||||||
import ExchangeRateTable from './ExchangeRateTable';
|
|
||||||
import ExchangeRateActionsBar from './ExchangeRateActionsBar';
|
|
||||||
|
|
||||||
import { ExchangeRatesProvider } from './ExchangeRatesProvider';
|
|
||||||
import { transformTableStateToQuery, compose } from '@/utils';
|
|
||||||
import withExchangeRates from './withExchangeRates';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange Rates list.
|
|
||||||
*/
|
|
||||||
function ExchangeRatesList({
|
|
||||||
// #withExchangeRates
|
|
||||||
exchangeRatesTableState,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ExchangeRatesProvider
|
|
||||||
query={transformTableStateToQuery(exchangeRatesTableState)}
|
|
||||||
>
|
|
||||||
<ExchangeRateActionsBar />
|
|
||||||
|
|
||||||
<DashboardPageContent>
|
|
||||||
<DashboardContentTable>
|
|
||||||
<ExchangeRateTable />
|
|
||||||
</DashboardContentTable>
|
|
||||||
</DashboardPageContent>
|
|
||||||
</ExchangeRatesProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default compose(
|
|
||||||
withExchangeRates(({ exchangeRatesTableState }) => ({
|
|
||||||
exchangeRatesTableState,
|
|
||||||
})),
|
|
||||||
)(ExchangeRatesList);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user