Compare commits

..

2 Commits

Author SHA1 Message Date
allcontributors[bot]
d793e79475 docs: update .all-contributorsrc [skip ci] 2023-12-26 13:45:13 +00:00
allcontributors[bot]
855462a185 docs: update README.md [skip ci] 2023-12-26 13:45:12 +00:00
276 changed files with 2849 additions and 8988 deletions

View File

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

View File

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

View File

@@ -2,32 +2,6 @@
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.13.3] - 22-01-2024
* hotfix(server): Unhandled thrown errors of services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/329
## [0.13.2] - 21-01-2024
* feat: show customer / vendor balance. by @asenawritescode in https://github.com/bigcapitalhq/bigcapital/pull/311
* feat: inventory valuation csv and xlsx export by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/308
* feat: sales by items export csv & xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/310
* fix(server): the invoice and payment receipt printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/315
* fix: get cashflow transaction broken cause transaction type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/318
* fix: `AccountActivateAlert` import by @xprnio in https://github.com/bigcapitalhq/bigcapital/pull/322
## [0.13.1] - 15-01-2024
* feat(webapp): add approve/reject to action bar of estimate details dr… by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/304
* docs: add ANasouf as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/305
* feat: Export general ledger & Journal to CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/303
* feat: Auto re-calculate the items rate once changing the invoice exchange rate. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/270
## [0.13.0] - 31-12-2023
* feat: Send an invoice mail the customer email by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/292
* fix: Allow non-numeric postal codes by @cschuijt in https://github.com/bigcapitalhq/bigcapital/pull/294
* docs: add cschuijt as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/295
## [0.12.1] - 17-11-2023 ## [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 * feat: Add default customer message and terms conditions to the transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/291

View File

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

View File

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

View File

@@ -2,16 +2,15 @@ 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()
private generalLedgerApplication: GeneralLedgerApplication; generalLedgetService: GeneralLedgerService;
/** /**
* Router constructor. * Router constructor.
@@ -62,43 +61,20 @@ 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 } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const accept = this.accepts(req);
const acceptType = accept.types([ try {
ACCEPT_TYPE.APPLICATION_JSON, const { data, query, meta } =
ACCEPT_TYPE.APPLICATION_JSON_TABLE, await this.generalLedgetService.generalLedger(tenantId, filter);
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(table); return res.status(200).send({
// Retrieves the csv format. meta: this.transfromToResponse(meta),
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { data: this.transfromToResponse(data),
const buffer = await this.generalLedgerApplication.csv(tenantId, filter); query: this.transfromToResponse(query),
});
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); } catch (error) {
res.setHeader('Content-Type', 'text/csv'); next(error);
return res.send(buffer);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.generalLedgerApplication.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.generalLedgerApplication.sheet(tenantId, filter);
return res.status(200).send(sheet);
} }
} }
} }

View File

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

View File

@@ -3,15 +3,14 @@ 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()
private journalSheetApp: JournalSheetApplication; journalService: JournalSheetService;
/** /**
* Router constructor. * Router constructor.
@@ -58,49 +57,28 @@ export default class JournalSheetController extends BaseFinancialReportControlle
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
private async journal(req: Request, res: Response, next: NextFunction) { async journal(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId, settings } = 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,
]);
// Retrieves the json table format. try {
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) { const { data, query, meta } = await this.journalService.journalSheet(
const table = await this.journalSheetApp.table(tenantId, filter); tenantId,
return res.status(200).send(table); filter
// 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(sheet); return res.status(200).send({
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
} catch (error) {
next(error);
} }
} }
} }

View File

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

View File

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

View File

@@ -303,7 +303,7 @@ export default class BillsController extends BaseController {
try { try {
const bill = await this.billsApplication.getBill(tenantId, billId); const bill = await this.billsApplication.getBill(tenantId, billId);
return res.status(200).send({ bill }); return res.status(200).send(this.transfromToResponse({ bill }));
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -348,11 +348,14 @@ export default class BillsController extends BaseController {
}; };
try { try {
const billsWithPagination = await this.billsApplication.getBills( const { bills, pagination, filterMeta } =
tenantId, await this.billsApplication.getBills(tenantId, filter);
filter
); return res.status(200).send({
return res.status(200).send(billsWithPagination); bills: this.transfromToResponse(bills),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,10 @@
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 { body, check, param, query, ValidationChain } from 'express-validator'; import { 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';
@@ -14,7 +13,6 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication'; import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service() @Service()
export default class PaymentReceivesController extends BaseController { export default class PaymentReceivesController extends BaseController {
@@ -119,25 +117,6 @@ 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;
} }
@@ -349,12 +328,17 @@ export default class PaymentReceivesController extends BaseController {
}; };
try { try {
const paymentsReceivedWithPagination = const { paymentReceives, pagination, filterMeta } =
await this.paymentReceiveApplication.getPaymentReceives( await this.paymentReceiveApplication.getPaymentReceives(
tenantId, tenantId,
filter filter
); );
return res.status(200).send(paymentsReceivedWithPagination);
return res.status(200).send({
payment_receives: this.transfromToResponse(paymentReceives),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -431,34 +415,38 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
const accept = this.accepts(req); try {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Response in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceiveId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Response in json format.
} else {
const paymentReceive = const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive( await this.paymentReceiveApplication.getPaymentReceive(
tenantId, tenantId,
paymentReceiveId paymentReceiveId
); );
return res.status(200).send({
payment_receive: paymentReceive, const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send({
payment_receive: this.transfromToResponse(paymentReceive),
});
},
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceive
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
}); });
} catch (error) {
next(error);
} }
} }
@@ -492,7 +480,7 @@ export default class PaymentReceivesController extends BaseController {
}; };
/** /**
* Retrieves the sms details of the given payment receive. *
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
@@ -519,74 +507,14 @@ 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} error * @param error
* @param {Request} req * @param req
* @param {Response} res * @param res
* @param {NextFunction} next * @param next
*/ */
private handleServiceErrors( handleServiceErrors(
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,

View File

@@ -1,11 +1,10 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { body, check, param, query } from 'express-validator'; import { 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';
@@ -122,27 +121,6 @@ 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;
} }
@@ -334,6 +312,7 @@ export default class SalesEstimatesController extends BaseController {
tenantId, tenantId,
estimateId estimateId
); );
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
message: 'The sale estimate has been approved successfully.', message: 'The sale estimate has been approved successfully.',
@@ -362,6 +341,7 @@ export default class SalesEstimatesController extends BaseController {
tenantId, tenantId,
estimateId estimateId
); );
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
message: 'The sale estimate has been rejected successfully.', message: 'The sale estimate has been rejected successfully.',
@@ -381,30 +361,33 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params; const { id: estimateId } = req.params;
const { tenantId } = req; const { tenantId } = req;
const accept = this.accepts(req); try {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimateId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves estimates in json format.
} else {
const estimate = await this.saleEstimatesApplication.getSaleEstimate( const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId, tenantId,
estimateId estimateId
); );
return res.status(200).send({ estimate }); // Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(this.transfromToResponse({ estimate }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent =
await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimate
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
} }
} }
@@ -422,11 +405,22 @@ export default class SalesEstimatesController extends BaseController {
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
try { try {
const salesEstimatesWithPagination = const { salesEstimates, pagination, filterMeta } =
await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter); await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter);
return res.status(200).send(salesEstimatesWithPagination); res.format({
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res.status(200).send(
this.transfromToResponse({
salesEstimates,
pagination,
filterMeta,
})
);
},
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -484,65 +478,6 @@ 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

View File

@@ -1,5 +1,5 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { body, check, param, query } from 'express-validator'; import { 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,12 +10,14 @@ 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';
import { ACCEPT_TYPE } from '@/interfaces/Http';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service() @Service()
export default class SaleInvoicesController extends BaseController { export default class SaleInvoicesController extends BaseController {
@Inject() @Inject()
@@ -143,48 +145,6 @@ 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;
} }
@@ -400,6 +360,7 @@ export default class SaleInvoicesController extends BaseController {
saleInvoiceId, saleInvoiceId,
user user
); );
return res.status(200).send({ return res.status(200).send({
id: saleInvoiceId, id: saleInvoiceId,
message: 'The sale invoice has been deleted successfully.', message: 'The sale invoice has been deleted successfully.',
@@ -414,35 +375,43 @@ export default class SaleInvoicesController extends BaseController {
* @param {Request} req - Request object. * @param {Request} req - Request object.
* @param {Response} res - Response object. * @param {Response} res - Response object.
*/ */
private async getSaleInvoice(req: Request, res: Response) { private async getSaleInvoice(
req: Request,
res: Response,
next: NextFunction
) {
const { id: saleInvoiceId } = req.params; const { id: saleInvoiceId } = req.params;
const { tenantId, user } = req; const { tenantId, user } = req;
const accept = this.accepts(req); try {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoiceId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves invoice in json format.
} else {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice( const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId, tenantId,
saleInvoiceId, saleInvoiceId,
user user
); );
return res.status(200).send({ saleInvoice }); // Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res
.status(200)
.send(this.transfromToResponse({ saleInvoice }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoice
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
} }
} }
/** /**
@@ -465,10 +434,14 @@ export default class SaleInvoicesController extends BaseController {
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
try { try {
const salesInvoicesWithPagination = const { salesInvoices, filterMeta, pagination } =
await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter); await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter);
return res.status(200).send(salesInvoicesWithPagination); return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -495,7 +468,9 @@ export default class SaleInvoicesController extends BaseController {
tenantId, tenantId,
customerId customerId
); );
return res.status(200).send({ salesInvoices }); return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -523,6 +498,7 @@ export default class SaleInvoicesController extends BaseController {
invoiceId, invoiceId,
writeoffDTO writeoffDTO
); );
return res.status(200).send({ return res.status(200).send({
id: saleInvoice.id, id: saleInvoice.id,
message: 'The given sale invoice has been written-off successfully.', message: 'The given sale invoice has been written-off successfully.',
@@ -654,119 +630,6 @@ 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

View File

@@ -1,19 +1,14 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { body, check, param, query } from 'express-validator'; import { 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 { import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
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';
import { AbilitySubject, SaleReceiptAction } from '@/interfaces'; import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication'; import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service() @Service()
export default class SalesReceiptsController extends BaseController { export default class SalesReceiptsController extends BaseController {
@@ -51,27 +46,6 @@ 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),
@@ -231,6 +205,7 @@ export default class SalesReceiptsController extends BaseController {
tenantId, tenantId,
saleReceiptId saleReceiptId
); );
return res.status(200).send({ return res.status(200).send({
id: saleReceiptId, id: saleReceiptId,
message: 'Sale receipt has been deleted successfully.', message: 'Sale receipt has been deleted successfully.',
@@ -319,10 +294,15 @@ export default class SalesReceiptsController extends BaseController {
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
try { try {
const salesReceiptsWithPagination = const { data, pagination, filterMeta } =
await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter); await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter);
return res.status(200).send(salesReceiptsWithPagination); const response = this.transfromToResponse({
data,
pagination,
filterMeta,
});
return res.status(200).send(response);
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -334,34 +314,36 @@ export default class SalesReceiptsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public async getSaleReceipt(req: Request, res: Response) { async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
const { tenantId } = req; const { tenantId } = req;
const accept = this.accepts(req); try {
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceiptId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves receipt in json format.
} else {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt( const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId, tenantId,
saleReceiptId saleReceiptId
); );
return res.status(200).send({ saleReceipt }); res.format({
'application/json': () => {
return res
.status(200)
.send(this.transfromToResponse({ saleReceipt }));
},
'application/pdf': async () => {
const pdfContent =
await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceipt
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
} }
} }
@@ -423,64 +405,6 @@ 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

View File

@@ -58,7 +58,6 @@ 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,
}, },
/** /**
@@ -169,14 +168,4 @@ module.exports = {
* to application detarmines to upgrade. * to application detarmines to upgrade.
*/ */
databaseBatch: 4, databaseBatch: 4,
/**
* Exchange rate.
*/
exchangeRate: {
service: 'open-exchange-rate',
openExchangeRate: {
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
}
}
}; };

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { IFinancialTable } from "./Table";
export interface IGeneralLedgerSheetQuery { export interface IGeneralLedgerSheetQuery {
@@ -37,7 +36,6 @@ export interface IGeneralLedgerSheetAccountTransaction {
referenceType?: string, referenceType?: string,
date: Date|string, date: Date|string,
dateFormatted: string;
}; };
export interface IGeneralLedgerSheetAccountBalance { export interface IGeneralLedgerSheetAccountBalance {
@@ -58,8 +56,6 @@ 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,
@@ -82,11 +78,4 @@ 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;
}

View File

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

View File

@@ -1,52 +1,36 @@
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,
date: Date; entries: IJournalEntry[],
dateFormatted: string; currencyCode: string,
entries: IJournalEntry[]; credit: number,
currencyCode: string; debit: number,
credit: number; formattedCredit: string,
debit: number; formattedDebit: string,
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;
}

View File

@@ -1,17 +1,9 @@
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(view: string, data?: { [key: string]: string | number }); constructor(
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;
@@ -21,27 +13,4 @@ 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;
}

View File

@@ -1,9 +1,5 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { import { ISystemUser } from '@/interfaces';
CommonMailOptions,
CommonMailOptionsDTO,
ISystemUser,
} from '@/interfaces';
import { ILedgerEntry } from './Ledger'; import { ILedgerEntry } from './Ledger';
import { ISaleInvoice } from './SaleInvoice'; import { ISaleInvoice } from './SaleInvoice';
@@ -23,7 +19,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;
@@ -169,7 +165,3 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'createdAt' | 'createdAt'
| 'branchId' | 'branchId'
>; >;
export interface PaymentReceiveMailOpts extends CommonMailOptions {}
export interface PaymentReceiveMailOptsDTO extends CommonMailOptionsDTO {}

View File

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

View File

@@ -1,7 +1,6 @@
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;
@@ -125,11 +124,3 @@ 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;
}

View File

@@ -1,6 +1,5 @@
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';
@@ -187,17 +186,3 @@ 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;
}

View File

@@ -1,6 +1,5 @@
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;
@@ -135,11 +134,3 @@ 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;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,13 +2,18 @@ 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 { IMailAttachment } from '@/interfaces'; import { IMailable } 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;
content: string = ''; to: 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[];
@@ -16,24 +21,16 @@ export default class Mail {
/** /**
* Mail options. * Mail options.
*/ */
public get mailOptions() { private get mailOptions() {
return { return {
to: this.to, to: this.to,
from: this.from, from: this.from,
subject: this.subject, subject: this.subject,
html: this.html, html: this.render(this.data),
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.
*/ */
@@ -55,7 +52,7 @@ export default class Mail {
* Set send mail to address. * Set send mail to address.
* @param {string} to - * @param {string} to -
*/ */
setTo(to: string | string[]) { setTo(to: string) {
this.to = to; this.to = to;
return this; return this;
} }
@@ -65,16 +62,11 @@ export default class Mail {
* @param {string} from * @param {string} from
* @return {} * @return {}
*/ */
setFrom(from: string) { private 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;
@@ -103,26 +95,21 @@ 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}
*/ */
static render(view: string, data: Record<string, any>): string { render(data): string {
const viewContent = Mail.getViewContent(view); const viewContent = this.getViewContent();
return Mustache.render(viewContent, data); return Mustache.render(viewContent, data);
} }
/** /**
* Retrieve view content from the view directory. * Retrieve view content from the view directory.
*/ */
static getViewContent(view: string): string { private getViewContent(): string {
const filePath = path.join(global.__views_dir, `/${view}`); const filePath = path.join(global.__views_dir, `/${this.view}`);
return fs.readFileSync(filePath, 'utf8'); return fs.readFileSync(filePath, 'utf8');
} }
} }

View File

@@ -5,11 +5,6 @@ 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);
@@ -18,11 +13,6 @@ 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();
}; };

View File

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

View File

@@ -2,9 +2,6 @@ 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
*/ */

View File

@@ -24,9 +24,6 @@ export default class Customer extends mixin(TenantModel, [
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, ModelSearchable,
]) { ]) {
email: string;
displayName: string;
/** /**
* Query builder. * Query builder.
*/ */
@@ -79,19 +76,6 @@ 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.
*/ */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ 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.
@@ -89,10 +88,8 @@ 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),

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ const ERRORS = {
}; };
@Service() @Service()
export class GeneralLedgerService { export default class GeneralLedgerService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -64,7 +64,7 @@ export class GeneralLedgerService {
* @param {number} tenantId - * @param {number} tenantId -
* @returns {IGeneralLedgerMeta} * @returns {IGeneralLedgerMeta}
*/ */
reportMetadata(tenantId: number, filter): IGeneralLedgerMeta { reportMetadata(tenantId: number): IGeneralLedgerMeta {
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService const isCostComputeRunning = this.inventoryService
@@ -78,15 +78,11 @@ export 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
}; };
} }
@@ -170,7 +166,7 @@ export class GeneralLedgerService {
return { return {
data: reportData, data: reportData,
query: filter, query: filter,
meta: this.reportMetadata(tenantId, filter), meta: this.reportMetadata(tenantId),
}; };
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,10 +6,8 @@ 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;
@@ -98,8 +96,6 @@ 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),
@@ -135,7 +131,7 @@ export default class JournalSheet extends FinancialSheet {
* Retrieve journal report. * Retrieve journal report.
* @return {IJournalReport} * @return {IJournalReport}
*/ */
reportData(): IJournalTableData { reportData(): IJournalReport {
return this.entriesWalker(this.journal.entries); return this.entriesWalker(this.journal.entries);
} }
} }

View File

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

View File

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

View File

@@ -1,25 +1,24 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
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 { Tenant } from '@/system/models';
import { parseBoolean, transformToMap } from 'utils'; import { parseBoolean, transformToMap } from 'utils';
import { Tenant } from '@/system/models';
@Service() @Service()
export class JournalSheetService { export default class JournalSheetService {
@Inject() @Inject()
private tenancy: TenancyService; tenancy: TenancyService;
@Inject() @Inject()
private inventoryService: InventoryService; inventoryService: InventoryService;
@Inject('logger')
logger: any;
/** /**
* Default journal sheet filter queyr. * Default journal sheet filter queyr.
@@ -68,13 +67,9 @@ export class JournalSheetService {
/** /**
* Journal sheet. * Journal sheet.
* @param {number} tenantId * @param {number} tenantId
* @param {IJournalReportQuery} query * @param {IJournalSheetFilterQuery} query
* @returns {Promise<IJournalSheet>}
*/ */
async journalSheet( async journalSheet(tenantId: number, query: IJournalReportQuery) {
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);
@@ -85,6 +80,11 @@ export 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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ export class PurchaseInvoiceTransformer extends Transformer {
return [ return [
'formattedBillDate', 'formattedBillDate',
'formattedDueDate', 'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount', 'formattedPaymentAmount',
'formattedBalance', 'formattedBalance',
'formattedDueAmount', 'formattedDueAmount',

View File

@@ -9,7 +9,6 @@ import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export default class EditVendorCredit extends BaseVendorCredit { export default class EditVendorCredit extends BaseVendorCredit {
@@ -22,9 +21,6 @@ export default class EditVendorCredit extends BaseVendorCredit {
@Inject() @Inject()
private itemsEntriesService: ItemsEntriesService; private itemsEntriesService: ItemsEntriesService;
@Inject()
private tenancy: HasTenancyService;
/** /**
* Deletes the given vendor credit. * Deletes the given vendor credit.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
@@ -35,7 +31,7 @@ export default class EditVendorCredit extends BaseVendorCredit {
vendorCreditId: number, vendorCreditId: number,
vendorCreditDTO: IVendorCreditEditDTO vendorCreditDTO: IVendorCreditEditDTO
) => { ) => {
const { VendorCredit, Contact } = this.tenancy.models(tenantId); const { VendorCredit } = this.tenancy.models(tenantId);
// Retrieve the vendor credit or throw not found service error. // Retrieve the vendor credit or throw not found service error.
const oldVendorCredit = await this.getVendorCreditOrThrowError( const oldVendorCredit = await this.getVendorCreditOrThrowError(

View File

@@ -7,8 +7,6 @@ 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';
@@ -19,7 +17,6 @@ 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 {
@@ -53,9 +50,6 @@ 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.
@@ -204,49 +198,15 @@ export class SaleEstimatesApplication {
}; };
/** /**
* Retrieve the PDF content of the given sale estimate. *
* @param {number} tenantId * @param {number} tenantId
* @param {number} saleEstimateId * @param {} saleEstimate
* @returns * @returns
*/ */
public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { public getSaleEstimatePdf(tenantId: number, saleEstimate) {
return this.saleEstimatesPdfService.getSaleEstimatePdf( return this.saleEstimatesPdfService.getSaleEstimatePdf(
tenantId, tenantId,
saleEstimateId saleEstimate
);
}
/**
* 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
); );
} }
} }

View File

@@ -1,7 +1,6 @@
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 {
@@ -11,19 +10,11 @@ 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 {number} tenantId - * @param {} saleInvoice -
* @param {ISaleInvoice} saleInvoice -
*/ */
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) { async getSaleEstimatePdf(tenantId: number, saleEstimate) {
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',

View File

@@ -1,146 +0,0 @@
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();
}
}

View File

@@ -1,36 +0,0 @@
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);
}
};
}

View File

@@ -1,18 +1,3 @@
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',
@@ -23,7 +8,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 = [];

View File

@@ -24,7 +24,8 @@ 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);

View File

@@ -1,3 +0,0 @@
export class GetSaleInvoiceMailReminder {
public getInvoiceMailReminder(tenantId: number, saleInvoiceId: number) {}
}

View File

@@ -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 { GetSaleInvoice } from './GetSaleInvoice'; import { ISaleInvoice } from '@/interfaces';
@Service() @Service()
export class SaleInvoicePdf { export class SaleInvoicePdf {
@@ -11,23 +11,16 @@ export class SaleInvoicePdf {
@Inject() @Inject()
private templateInjectable: TemplateInjectable; private templateInjectable: TemplateInjectable;
@Inject()
private getInvoiceService: GetSaleInvoice;
/** /**
* 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>}
*/ */
public async saleInvoicePdf( async saleInvoicePdf(
tenantId: number, tenantId: number,
invoiceId: number saleInvoice: ISaleInvoice
): Promise<Buffer> { ): Promise<Buffer> {
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
tenantId,
invoiceId
);
const htmlContent = await this.templateInjectable.render( const htmlContent = await this.templateInjectable.render(
tenantId, tenantId,
'modules/invoice-regular', 'modules/invoice-regular',

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