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": [
"bug"
]
},
{
"login": "ANasouf",
"name": "ANasouf",
"avatar_url": "https://avatars.githubusercontent.com/u/19536487?v=4",
"profile": "https://github.com/ANasouf",
"contributions": [
"code"
]
},
{
"login": "xprnio",
"name": "Ragnar Laud",
"avatar_url": "https://avatars.githubusercontent.com/u/3042904?v=4",
"profile": "https://ragnarlaud.dev",
"contributions": [
"bug"
]
},
{
"login": "asenawritescode",
"name": "Asena",
"avatar_url": "https://avatars.githubusercontent.com/u/67445192?v=4",
"profile": "https://github.com/asenawritescode",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

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

View File

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

View File

@@ -1,16 +1,19 @@
import { Service, Inject } from 'typedi';
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 BaseController from './BaseController';
import { ServiceError } from '@/exceptions';
import { EchangeRateErrors } from '@/lib/ExchangeRate/types';
import { ExchangeRateApplication } from '@/services/ExchangeRates/ExchangeRateApplication';
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
@Service()
export default class ExchangeRatesController extends BaseController {
@Inject()
private exchangeRatesApp: ExchangeRateApplication;
exchangeRatesService: ExchangeRatesService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Constructor method.
@@ -19,40 +22,164 @@ export default class ExchangeRatesController extends BaseController {
const router = Router();
router.get(
'/latest',
[
oneOf([
query('to_currency').exists().isString().isISO4217(),
query('from_currency').exists().isString().isISO4217(),
]),
],
'/',
[...this.exchangeRatesListSchema],
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
);
return router;
}
get exchangeRatesListSchema() {
return [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
];
}
get exchangeRateDTOSchema() {
return [
check('exchange_rate').exists().isNumeric().toFloat(),
check('currency_code').exists().trim().escape(),
check('date').exists().isISO8601(),
];
}
get exchangeRateEditDTOSchema() {
return [check('exchange_rate').exists().isNumeric().toFloat()];
}
get exchangeRateIdSchema() {
return [param('id').isNumeric().toInt()];
}
get exchangeRatesIdsSchema() {
return [
query('ids').isArray({ min: 2 }),
query('ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve exchange rates.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async latestExchangeRate(
req: Request,
res: Response,
next: NextFunction
) {
async exchangeRates(req: Request, res: Response, next: NextFunction) {
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 {
const exchangeRate = await this.exchangeRatesApp.latest(
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
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) {
next(error);
}
@@ -65,56 +192,26 @@ export default class ExchangeRatesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
private handleServiceError(
handleServiceError(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY === error.errorType) {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
code: 100,
message: 'The given base currency is invalid.',
},
],
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
return res.status(404).send({
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
});
} else if (
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED === error.errorType
) {
}
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
code: 200,
message: 'The service is not allowed',
},
],
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
});
} else if (
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED === error.errorType
) {
}
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
code: 300,
message: 'The API key is required',
},
],
});
} else if (EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED === error.errorType) {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED,
code: 400,
message: 'The API rate limit has been exceeded',
},
],
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -158,11 +158,15 @@ export default class BillsPayments extends BaseController {
const { tenantId } = req;
const { vendorId } = this.matchedQueryData(req);
const entries = await this.billPaymentsPages.getNewPageEntries(
tenantId,
vendorId
);
return res.status(200).send({ entries });
try {
const entries = await this.billPaymentsPages.getNewPageEntries(
tenantId,
vendorId
);
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;
try {
const billPaymentsWithEditEntries =
const { billPayment, entries } =
await this.billPaymentsPages.getBillPaymentEditPage(
tenantId,
paymentReceiveId
);
return res.status(200).send(billPaymentsWithEditEntries);
return res.status(200).send({
bill_payment: this.transfromToResponse(billPayment),
entries: this.transfromToResponse(entries),
});
} catch (error) {
next(error);
}
@@ -296,7 +304,9 @@ export default class BillsPayments extends BaseController {
tenantId,
billPaymentId
);
return res.status(200).send({ billPayment });
return res.status(200).send({
bill_payment: this.transfromToResponse(billPayment),
});
} catch (error) {
next(error);
}
@@ -349,12 +359,17 @@ export default class BillsPayments extends BaseController {
};
try {
const billPaymentsWithPagination =
const { billPayments, pagination, filterMeta } =
await this.billPaymentsApplication.getBillPayments(
tenantId,
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) {
next(error);
}

View File

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

View File

@@ -26,7 +26,6 @@ import GetCreditNoteAssociatedInvoicesToApply from '@/services/CreditNotes/GetCr
import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCreditNoteAssociatedAppliedInvoices';
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
import { ACCEPT_TYPE } from '@/interfaces/Http';
/**
* Credit notes controller.
* @service
@@ -294,7 +293,7 @@ export default class PaymentReceivesController extends BaseController {
return [
check('from_account_id').exists().isNumeric().toInt(),
check('description').optional(),
check('amount').exists().isNumeric().toFloat(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
@@ -439,7 +438,7 @@ export default class PaymentReceivesController extends BaseController {
};
/**
* Retrieve the credit note details.
* Retrieve the payment receive details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
@@ -452,28 +451,38 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req;
const { id: creditNoteId } = req.params;
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
tenantId,
creditNoteId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} else {
try {
const creditNote = await this.getCreditNoteService.getCreditNote(
tenantId,
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 { Router, Request, Response, NextFunction } from 'express';
import { body, check, param, query, ValidationChain } from 'express-validator';
import { check, param, query, ValidationChain } from 'express-validator';
import {
AbilitySubject,
IPaymentReceiveDTO,
PaymentReceiveAction,
PaymentReceiveMailOptsDTO,
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -14,7 +13,6 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ServiceError } from '@/exceptions';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class PaymentReceivesController extends BaseController {
@@ -119,25 +117,6 @@ export default class PaymentReceivesController extends BaseController {
asyncMiddleware(this.deletePaymentReceive.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.paymentReceiveValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.sendPaymentReceiveByMail.bind(this),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.paymentReceiveValidation],
asyncMiddleware(this.getPaymentDefaultMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -349,12 +328,17 @@ export default class PaymentReceivesController extends BaseController {
};
try {
const paymentsReceivedWithPagination =
const { paymentReceives, pagination, filterMeta } =
await this.paymentReceiveApplication.getPaymentReceives(
tenantId,
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) {
next(error);
}
@@ -431,34 +415,38 @@ export default class PaymentReceivesController extends BaseController {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Response in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent =
await this.paymentReceiveApplication.getPaymentReceivePdf(
tenantId,
paymentReceiveId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Response in json format.
} else {
try {
const paymentReceive =
await this.paymentReceiveApplication.getPaymentReceive(
tenantId,
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 {Response} res
* @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.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param error
* @param req
* @param res
* @param next
*/
private handleServiceErrors(
handleServiceErrors(
error: Error,
req: Request,
res: Response,

View File

@@ -1,11 +1,10 @@
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 {
AbilitySubject,
ISaleEstimateDTO,
SaleEstimateAction,
SaleEstimateMailOptionsDTO,
} from '@/interfaces';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -122,27 +121,6 @@ export default class SalesEstimatesController extends BaseController {
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse
);
router.post(
'/:id/mail',
[
...this.validateSpecificEstimateSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleEstimateMail.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.getSaleEstimateMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -334,6 +312,7 @@ export default class SalesEstimatesController extends BaseController {
tenantId,
estimateId
);
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been approved successfully.',
@@ -362,6 +341,7 @@ export default class SalesEstimatesController extends BaseController {
tenantId,
estimateId
);
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been rejected successfully.',
@@ -381,30 +361,33 @@ export default class SalesEstimatesController extends BaseController {
const { id: estimateId } = req.params;
const { tenantId } = req;
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves estimate in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf(
tenantId,
estimateId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves estimates in json format.
} else {
try {
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
tenantId,
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,
...this.matchedQueryData(req),
};
try {
const salesEstimatesWithPagination =
const { salesEstimates, pagination, filterMeta } =
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) {
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.
* @param {Error} error

View File

@@ -1,5 +1,5 @@
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 BaseController from '../BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
@@ -10,12 +10,14 @@ import {
ISaleInvoiceCreateDTO,
SaleInvoiceAction,
AbilitySubject,
SendInvoiceMailDTO,
} from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
const ACCEPT_TYPE = {
APPLICATION_PDF: 'application/pdf',
APPLICATION_JSON: 'application/json',
};
@Service()
export default class SaleInvoicesController extends BaseController {
@Inject()
@@ -143,48 +145,6 @@ export default class SaleInvoicesController extends BaseController {
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse
);
router.get(
'/:id/mail-reminder',
this.specificSaleInvoiceValidation,
this.validationResult,
asyncMiddleware(this.getSaleInvoiceMailReminder.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail-reminder',
[
...this.specificSaleInvoiceValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleInvoiceMailReminder.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.specificSaleInvoiceValidation,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_invoice').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleInvoiceMail.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.specificSaleInvoiceValidation],
this.validationResult,
asyncMiddleware(this.getSaleInvoiceMail.bind(this)),
this.handleServiceErrors
);
return router;
}
@@ -400,6 +360,7 @@ export default class SaleInvoicesController extends BaseController {
saleInvoiceId,
user
);
return res.status(200).send({
id: saleInvoiceId,
message: 'The sale invoice has been deleted successfully.',
@@ -414,35 +375,43 @@ export default class SaleInvoicesController extends BaseController {
* @param {Request} req - Request 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 { tenantId, user } = req;
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves invoice in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoiceId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves invoice in json format.
} else {
try {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId,
saleInvoiceId,
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),
};
try {
const salesInvoicesWithPagination =
const { salesInvoices, filterMeta, pagination } =
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) {
next(error);
}
@@ -495,7 +468,9 @@ export default class SaleInvoicesController extends BaseController {
tenantId,
customerId
);
return res.status(200).send({ salesInvoices });
return res.status(200).send({
sales_invoices: this.transfromToResponse(salesInvoices),
});
} catch (error) {
next(error);
}
@@ -523,6 +498,7 @@ export default class SaleInvoicesController extends BaseController {
invoiceId,
writeoffDTO
);
return res.status(200).send({
id: saleInvoice.id,
message: 'The given sale invoice has been written-off successfully.',
@@ -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.
* @param {Error} error

View File

@@ -1,19 +1,14 @@
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 asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '../BaseController';
import {
ISaleReceiptDTO,
SaleReceiptMailOpts,
SaleReceiptMailOptsDTO,
} from '@/interfaces/SaleReceipt';
import { ISaleReceiptDTO } from '@/interfaces/SaleReceipt';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export default class SalesReceiptsController extends BaseController {
@@ -51,27 +46,6 @@ export default class SalesReceiptsController extends BaseController {
this.saleReceiptSmsDetails,
this.handleServiceErrors
);
router.post(
'/:id/mail',
[
...this.specificReceiptValidationSchema,
body('subject').isString().optional(),
body('from').isString().optional(),
body('to').isString().optional(),
body('body').isString().optional(),
body('attach_receipt').optional().isBoolean().toBoolean(),
],
this.validationResult,
asyncMiddleware(this.sendSaleReceiptMail.bind(this)),
this.handleServiceErrors
);
router.get(
'/:id/mail',
[...this.specificReceiptValidationSchema],
this.validationResult,
asyncMiddleware(this.getSaleReceiptMail.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id',
CheckPolicies(SaleReceiptAction.Edit, AbilitySubject.SaleReceipt),
@@ -231,6 +205,7 @@ export default class SalesReceiptsController extends BaseController {
tenantId,
saleReceiptId
);
return res.status(200).send({
id: saleReceiptId,
message: 'Sale receipt has been deleted successfully.',
@@ -319,10 +294,15 @@ export default class SalesReceiptsController extends BaseController {
...this.matchedQueryData(req),
};
try {
const salesReceiptsWithPagination =
const { data, pagination, filterMeta } =
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) {
next(error);
}
@@ -334,34 +314,36 @@ export default class SalesReceiptsController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
public async getSaleReceipt(req: Request, res: Response) {
async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
const { id: saleReceiptId } = req.params;
const { tenantId } = req;
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves receipt in pdf format.
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf(
tenantId,
saleReceiptId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
// Retrieves receipt in json format.
} else {
try {
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
tenantId,
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.
* @param {Error} error

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
constructor(view: string, data?: { [key: string]: string | number });
constructor(
view: string,
data?: { [key: string]: string | number },
);
send(): Promise<any>;
build(): void;
setData(data: { [key: string]: string | number }): IMailable;
@@ -21,27 +13,4 @@ export interface IMailable {
setView(view: string): IMailable;
render(data?: { [key: string]: string | number }): string;
getViewContent(): string;
}
export interface AddressItem {
label: string;
mail: string;
primary?: boolean;
}
export interface CommonMailOptions {
toAddresses: AddressItem[];
fromAddresses: AddressItem[];
from: string;
to: string | string[];
subject: string;
body: string;
data?: Record<string, any>;
}
export interface CommonMailOptionsDTO {
to?: string | string[];
from?: string;
subject?: string;
body?: string;
}
}

View File

@@ -1,9 +1,5 @@
import { Knex } from 'knex';
import {
CommonMailOptions,
CommonMailOptionsDTO,
ISystemUser,
} from '@/interfaces';
import { ISystemUser } from '@/interfaces';
import { ILedgerEntry } from './Ledger';
import { ISaleInvoice } from './SaleInvoice';
@@ -23,7 +19,7 @@ export interface IPaymentReceive {
createdAt: Date;
updatedAt: Date;
localAmount?: number;
branchId?: number;
branchId?: number
}
export interface IPaymentReceiveCreateDTO {
customerId: number;
@@ -169,7 +165,3 @@ export type IPaymentReceiveGLCommonEntry = Pick<
| 'createdAt'
| '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 { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
export interface ISaleEstimate {
id?: number;
@@ -125,11 +124,3 @@ export interface ISaleEstimateApprovedEvent {
saleEstimate: ISaleEstimate;
trx: Knex.Transaction;
}
export interface SaleEstimateMailOptions extends CommonMailOptions {
attachEstimate?: boolean;
}
export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
attachEstimate?: boolean;
}

View File

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

View File

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

View File

@@ -1,54 +1,45 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IFinancialTable } from './Table';
import {
INumberFormatQuery,
} from './FinancialStatements';
export interface ISalesByItemsReportQuery {
fromDate: Date | string;
toDate: Date | string;
itemsIds: number[];
itemsIds: number[],
numberFormat: INumberFormatQuery;
noneTransactions: 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;
onlyActive: boolean;
};
export interface ISalesByItemsSheet {
data: ISalesByItemsSheetData;
query: ISalesByItemsReportQuery;
meta: ISalesByItemsSheetMeta;
}
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 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 { Container } from 'typedi';
import path from 'path';
import { IMailAttachment } from '@/interfaces';
import { IMailable } from '@/interfaces';
interface IMailAttachment {
filename: string;
path: string;
cid: string;
}
export default class Mail {
view: string;
subject: string = '';
content: string = '';
to: string | string[];
subject: string;
to: string;
from: string = `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`;
data: { [key: string]: string | number };
attachments: IMailAttachment[];
@@ -16,24 +21,16 @@ export default class Mail {
/**
* Mail options.
*/
public get mailOptions() {
private get mailOptions() {
return {
to: this.to,
from: this.from,
subject: this.subject,
html: this.html,
html: this.render(this.data),
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.
*/
@@ -55,7 +52,7 @@ export default class Mail {
* Set send mail to address.
* @param {string} to -
*/
setTo(to: string | string[]) {
setTo(to: string) {
this.to = to;
return this;
}
@@ -65,16 +62,11 @@ export default class Mail {
* @param {string} from
* @return {}
*/
setFrom(from: string) {
private setFrom(from: string) {
this.from = from;
return this;
}
/**
* Set attachments to the mail.
* @param {IMailAttachment[]} attachments
* @returns {Mail}
*/
setAttachments(attachments: IMailAttachment[]) {
this.attachments = attachments;
return this;
@@ -103,26 +95,21 @@ export default class Mail {
return this;
}
setContent(content: string) {
this.content = content;
return this;
}
/**
* Renders the view template with the given data.
* @param {object} data
* @return {string}
*/
static render(view: string, data: Record<string, any>): string {
const viewContent = Mail.getViewContent(view);
render(data): string {
const viewContent = this.getViewContent();
return Mustache.render(viewContent, data);
}
/**
* Retrieve view content from the view directory.
*/
static getViewContent(view: string): string {
const filePath = path.join(global.__views_dir, `/${view}`);
private getViewContent(): string {
const filePath = path.join(global.__views_dir, `/${this.view}`);
return fs.readFileSync(filePath, 'utf8');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -136,7 +136,7 @@ export class EditExpense {
} as IExpenseEventEditingPayload);
// Upsert the expense object with expense entries.
const expense: IExpense = await Expense.query(trx).upsertGraphAndFetch({
const expense: IExpense = await Expense.query(trx).upsertGraph({
id: expenseId,
...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 { formatNumber } from 'utils';
import { IExpense } from '@/interfaces';
import { ExpenseCategoryTransformer } from './ExpenseCategoryTransformer';
export class ExpenseTransfromer extends Transformer {
/**
@@ -13,8 +12,7 @@ export class ExpenseTransfromer extends Transformer {
'formattedAmount',
'formattedLandedCostAmount',
'formattedAllocatedCostAmount',
'formattedDate',
'categories',
'formattedDate'
];
};
@@ -58,16 +56,5 @@ export class ExpenseTransfromer extends Transformer {
*/
protected formattedDate = (expense: IExpense): string => {
return this.formatDate(expense.paymentDate);
};
/**
* Retrieves the transformed expense categories.
* @param {IExpense} expense
* @returns {}
*/
protected categories = (expense: IExpense) => {
return this.item(expense.categories, new ExpenseCategoryTransformer(), {
currencyCode: expense.currencyCode,
});
};
}
}

View File

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

View File

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

View File

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

View File

@@ -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()
export class GeneralLedgerService {
export default class GeneralLedgerService {
@Inject()
tenancy: TenancyService;
@@ -64,7 +64,7 @@ export class GeneralLedgerService {
* @param {number} tenantId -
* @returns {IGeneralLedgerMeta}
*/
reportMetadata(tenantId: number, filter): IGeneralLedgerMeta {
reportMetadata(tenantId: number): IGeneralLedgerMeta {
const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService
@@ -78,15 +78,11 @@ export class GeneralLedgerService {
group: 'organization',
key: 'base_currency',
});
const fromDate = moment(filter.fromDate).format('YYYY MMM DD');
const toDate = moment(filter.toDate).format('YYYY MMM DD');
return {
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
organizationName,
baseCurrency,
fromDate,
toDate
baseCurrency
};
}
@@ -170,7 +166,7 @@ export class GeneralLedgerService {
return {
data: reportData,
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';
import { allPassedConditionsPass, transformToMap } from 'utils';
export class InventoryValuationSheet extends FinancialSheet {
export default class InventoryValuationSheet extends FinancialSheet {
readonly query: IInventoryValuationReportQuery;
readonly items: IItem[];
readonly INInventoryCostLots: Map<number, InventoryCostLotTracker>;
@@ -259,6 +259,6 @@ export class InventoryValuationSheet extends FinancialSheet {
const items = this.itemsSection();
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 {
IInventoryValuationReportQuery,
IInventoryValuationSheet,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { InventoryValuationSheet } from './InventoryValuationSheet';
import InventoryValuationSheet from './InventoryValuationSheet';
import InventoryService from '@/services/Inventory/Inventory';
import { Tenant } from '@/system/models';
@Service()
export class InventoryValuationSheetService {
export default class InventoryValuationSheetService {
@Inject()
tenancy: TenancyService;
@@ -81,7 +80,7 @@ export class InventoryValuationSheetService {
public async inventoryValuationSheet(
tenantId: number,
query: IInventoryValuationReportQuery
): Promise<IInventoryValuationSheet> {
) {
const { Item, InventoryCostLotTracker } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()

View File

@@ -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,
IJournalReport,
IContact,
IJournalTableData,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import moment from 'moment';
export default class JournalSheet extends FinancialSheet {
readonly tenantId: number;
@@ -98,8 +96,6 @@ export default class JournalSheet extends FinancialSheet {
return {
date: groupEntry.date,
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
referenceType: groupEntry.referenceType,
referenceId: groupEntry.referenceId,
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
@@ -135,7 +131,7 @@ export default class JournalSheet extends FinancialSheet {
* Retrieve journal report.
* @return {IJournalReport}
*/
reportData(): IJournalTableData {
reportData(): IJournalReport {
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 moment from 'moment';
import {
IJournalReportQuery,
IJournalSheet,
IJournalSheetMeta,
IJournalTableData,
} from '@/interfaces';
import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
import JournalSheet from './JournalSheet';
import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import InventoryService from '@/services/Inventory/Inventory';
import { Tenant } from '@/system/models';
import { parseBoolean, transformToMap } from 'utils';
import { Tenant } from '@/system/models';
@Service()
export class JournalSheetService {
export default class JournalSheetService {
@Inject()
private tenancy: TenancyService;
tenancy: TenancyService;
@Inject()
private inventoryService: InventoryService;
inventoryService: InventoryService;
@Inject('logger')
logger: any;
/**
* Default journal sheet filter queyr.
@@ -68,13 +67,9 @@ export class JournalSheetService {
/**
* Journal sheet.
* @param {number} tenantId
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalSheet>}
* @param {IJournalSheetFilterQuery} query
*/
async journalSheet(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalSheet> {
async journalSheet(tenantId: number, query: IJournalReportQuery) {
const i18n = this.tenancy.i18n(tenantId);
const { accountRepository, transactionsRepository, contactRepository } =
this.tenancy.repositories(tenantId);
@@ -85,6 +80,11 @@ export class JournalSheetService {
...this.defaultQuery,
...query,
};
this.logger.info('[journal] trying to calculate the report.', {
tenantId,
filter,
});
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');

View File

@@ -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 FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, transformToMap } from 'utils';
import { IAccountTransaction, IItem } from '@/interfaces';
import {
IPurchasesByItemsItem,
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheetData,
IPurchasesByItemsTotal,
} from '@/interfaces/PurchasesByItemsSheet';
IAccountTransaction,
IInventoryValuationTotal,
IInventoryValuationItem,
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IItem,
} from '@/interfaces';
export class PurchasesByItems extends FinancialSheet {
export default class InventoryValuationReport extends FinancialSheet {
readonly baseCurrency: string;
readonly items: IItem[];
readonly itemsTransactions: Map<number, IAccountTransaction>;
readonly query: IPurchasesByItemsReportQuery;
readonly query: IInventoryValuationReportQuery;
/**
* Constructor method.
* @param {IPurchasesByItemsReportQuery} query
* @param {IInventoryValuationReportQuery} query
* @param {IItem[]} items
* @param {IAccountTransaction[]} itemsTransactions
* @param {string} baseCurrency
*/
constructor(
query: IPurchasesByItemsReportQuery,
query: IInventoryValuationReportQuery,
items: IItem[],
itemsTransactions: IAccountTransaction[],
baseCurrency: string
) {
super();
this.baseCurrency = baseCurrency;
this.items = items;
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
@@ -96,7 +98,7 @@ export class PurchasesByItems extends FinancialSheet {
* @param {IInventoryValuationItem} item
* @returns
*/
private itemSectionMapper = (item: IItem): IPurchasesByItemsItem => {
private itemSectionMapper = (item: IItem): IInventoryValuationItem => {
const meta = this.getItemTransaction(item.id);
return {
@@ -143,9 +145,9 @@ export class PurchasesByItems extends FinancialSheet {
/**
* Retrieve the items sections.
* @returns {IPurchasesByItemsItem[]}
* @returns {IInventoryValuationItem[]}
*/
private itemsSection = (): IPurchasesByItemsItem[] => {
private itemsSection = (): IInventoryValuationItem[] => {
return R.compose(
R.when(this.isItemsPostFilter, this.itemsFilter),
this.itemsMapper
@@ -154,10 +156,10 @@ export class PurchasesByItems extends FinancialSheet {
/**
* Retrieve the total section of the sheet.
* @param {IPurchasesByItemsItem[]} items
* @returns {IPurchasesByItemsTotal}
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
*/
private totalSection(items: IPurchasesByItemsItem[]): IPurchasesByItemsTotal {
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
@@ -174,12 +176,12 @@ export class PurchasesByItems extends FinancialSheet {
/**
* Retrieve the sheet data.
* @returns {IInventoryValuationStatement}
* @returns
*/
public reportData(): IPurchasesByItemsSheetData {
reportData(): IInventoryValuationStatement {
const items = this.itemsSection();
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 TenancyService from '@/services/Tenancy/TenancyService';
import { PurchasesByItems } from './PurchasesByItems';
import { Tenant } from '@/system/models';
import moment from 'moment';
import {
IPurchasesByItemsReportQuery,
IPurchasesByItemsSheet,
IPurchasesByItemsSheetMeta,
} from '@/interfaces/PurchasesByItemsSheet';
IInventoryValuationReportQuery,
IInventoryValuationStatement,
IInventoryValuationSheetMeta,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import PurchasesByItems from './PurchasesByItems';
import { Tenant } from '@/system/models';
@Service()
export class PurchasesByItemsService {
export default class InventoryValuationReportService {
@Inject()
private tenancy: TenancyService;
/**
* Defaults purchases by items filter query.
* @return {IPurchasesByItemsReportQuery}
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): IPurchasesByItemsReportQuery {
get defaultQuery(): IInventoryValuationReportQuery {
return {
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
@@ -40,7 +40,7 @@ export class PurchasesByItemsService {
* @param {number} tenantId -
* @returns {IBalanceSheetMeta}
*/
reportMetadata(tenantId: number): IPurchasesByItemsSheetMeta {
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
const settings = this.tenancy.settings(tenantId);
const organizationName = settings.get({
@@ -62,13 +62,18 @@ export class PurchasesByItemsService {
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IPurchasesByItemsReportQuery} query
* @return {Promise<IPurchasesByItemsSheet>}
* @param {IBalanceSheetQuery} query
*
* @return {IBalanceSheetStatement}
*/
public async purchasesByItems(
tenantId: number,
query: IPurchasesByItemsReportQuery
): Promise<IPurchasesByItemsSheet> {
query: IInventoryValuationReportQuery
): Promise<{
data: IInventoryValuationStatement;
query: IInventoryValuationReportQuery;
meta: IInventoryValuationSheetMeta;
}> {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -101,6 +106,7 @@ export class PurchasesByItemsService {
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
}
);
const purchasesByItemsInstance = new PurchasesByItems(
filter,
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,
ISalesByItemsItem,
ISalesByItemsTotal,
ISalesByItemsSheetData,
ISalesByItemsSheetStatement,
IItem,
} from '@/interfaces';
@@ -146,7 +146,7 @@ export default class SalesByItemsReport extends FinancialSheet {
* @param {IInventoryValuationItem[]} items
* @returns {IInventoryValuationTotal}
*/
private totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
totalSection(items: ISalesByItemsItem[]): ISalesByItemsTotal {
const quantitySold = sumBy(items, (item) => item.quantitySold);
const soldCost = sumBy(items, (item) => item.soldCost);
@@ -163,12 +163,12 @@ export default class SalesByItemsReport extends FinancialSheet {
/**
* Retrieve the sheet data.
* @returns {ISalesByItemsSheetData}
* @returns {ISalesByItemsSheetStatement}
*/
public reportData(): ISalesByItemsSheetData {
reportData(): ISalesByItemsSheetStatement {
const items = this.itemsSection();
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 {
ISalesByItemsReportQuery,
ISalesByItemsSheetMeta,
ISalesByItemsSheet,
ISalesByItemsSheetStatement,
ISalesByItemsSheetMeta
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import SalesByItems from './SalesByItems';
import { Tenant } from '@/system/models';
@Service()
export class SalesByItemsReportService {
export default class SalesByItemsReportService {
@Inject()
tenancy: TenancyService;
@@ -63,14 +63,20 @@ export class SalesByItemsReportService {
/**
* Retrieve balance sheet statement.
* -------------
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
* @return {Promise<ISalesByItemsSheet>}
*
* @return {IBalanceSheetStatement}
*/
public async salesByItems(
tenantId: number,
query: ISalesByItemsReportQuery
): Promise<ISalesByItemsSheet> {
): Promise<{
data: ISalesByItemsSheetStatement,
query: ISalesByItemsReportQuery,
meta: ISalesByItemsSheetMeta,
}> {
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
const tenant = await Tenant.query()
@@ -101,19 +107,20 @@ export class SalesByItemsReportService {
builder.whereIn('itemId', inventoryItemsIds);
// Filter the date range of the sheet.
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
}
);
const sheet = new SalesByItems(
const purchasesByItemsInstance = new SalesByItems(
filter,
inventoryItems,
inventoryTransactions,
tenant.metadata.baseCurrency
tenant.metadata.baseCurrency,
);
const salesByItemsData = sheet.reportData();
const purchasesByItemsData = purchasesByItemsInstance.reportData();
return {
data: salesByItemsData,
data: purchasesByItemsData,
query: filter,
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 [
'formattedBillDate',
'formattedDueDate',
'formattedAmount',
'formattedPaymentAmount',
'formattedBalance',
'formattedDueAmount',

View File

@@ -9,7 +9,6 @@ import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import events from '@/subscribers/events';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class EditVendorCredit extends BaseVendorCredit {
@@ -22,9 +21,6 @@ export default class EditVendorCredit extends BaseVendorCredit {
@Inject()
private itemsEntriesService: ItemsEntriesService;
@Inject()
private tenancy: HasTenancyService;
/**
* Deletes the given vendor credit.
* @param {number} tenantId - Tenant id.
@@ -35,7 +31,7 @@ export default class EditVendorCredit extends BaseVendorCredit {
vendorCreditId: number,
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.
const oldVendorCredit = await this.getVendorCreditOrThrowError(

View File

@@ -7,8 +7,6 @@ import {
ISaleEstimate,
ISaleEstimateDTO,
ISalesEstimatesFilter,
SaleEstimateMailOptions,
SaleEstimateMailOptionsDTO,
} from '@/interfaces';
import { EditSaleEstimate } from './EditSaleEstimate';
import { DeleteSaleEstimate } from './DeleteSaleEstimate';
@@ -19,7 +17,6 @@ import { ApproveSaleEstimate } from './ApproveSaleEstimate';
import { RejectSaleEstimate } from './RejectSaleEstimate';
import { SaleEstimateNotifyBySms } from './SaleEstimateSmsNotify';
import { SaleEstimatesPdf } from './SaleEstimatesPdf';
import { SendSaleEstimateMail } from './SendSaleEstimateMail';
@Service()
export class SaleEstimatesApplication {
@@ -53,9 +50,6 @@ export class SaleEstimatesApplication {
@Inject()
private saleEstimatesPdfService: SaleEstimatesPdf;
@Inject()
private sendEstimateMailService: SendSaleEstimateMail;
/**
* Create a sale estimate.
* @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} saleEstimateId
* @param {} saleEstimate
* @returns
*/
public getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
public getSaleEstimatePdf(tenantId: number, saleEstimate) {
return this.saleEstimatesPdfService.getSaleEstimatePdf(
tenantId,
saleEstimateId
);
}
/**
* Send the reminder mail of the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<void>}
*/
public sendSaleEstimateMail(
tenantId: number,
saleEstimateId: number,
saleEstimateMailOpts: SaleEstimateMailOptionsDTO
): Promise<void> {
return this.sendEstimateMailService.triggerMail(
tenantId,
saleEstimateId,
saleEstimateMailOpts
);
}
/**
* Retrieves the default mail options of the given sale estimate.
* @param {number} tenantId
* @param {number} saleEstimateId
* @returns {Promise<SaleEstimateMailOptions>}
*/
public getSaleEstimateMail(
tenantId: number,
saleEstimateId: number
): Promise<SaleEstimateMailOptions> {
return this.sendEstimateMailService.getMailOptions(
tenantId,
saleEstimateId
saleEstimate
);
}
}

View File

@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleEstimate } from './GetSaleEstimate';
@Service()
export class SaleEstimatesPdf {
@@ -11,19 +10,11 @@ export class SaleEstimatesPdf {
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getSaleEstimate: GetSaleEstimate;
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId -
* @param {ISaleInvoice} saleInvoice -
* @param {} saleInvoice -
*/
public async getSaleEstimatePdf(tenantId: number, saleEstimateId: number) {
const saleEstimate = await this.getSaleEstimate.getEstimate(
tenantId,
saleEstimateId
);
async getSaleEstimatePdf(tenantId: number, saleEstimate) {
const htmlContent = await this.templateInjectable.render(
tenantId,
'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 = {
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
@@ -23,7 +8,7 @@ export const ERRORS = {
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED'
};
export const DEFAULT_VIEW_COLUMNS = [];

View File

@@ -24,7 +24,8 @@ export class GetSaleInvoice {
*/
public async getSaleInvoice(
tenantId: number,
saleInvoiceId: number
saleInvoiceId: number,
authorizedUser: ISystemUser
): Promise<ISaleInvoice> {
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 { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import { GetSaleInvoice } from './GetSaleInvoice';
import { ISaleInvoice } from '@/interfaces';
@Service()
export class SaleInvoicePdf {
@@ -11,23 +11,16 @@ export class SaleInvoicePdf {
@Inject()
private templateInjectable: TemplateInjectable;
@Inject()
private getInvoiceService: GetSaleInvoice;
/**
* Retrieve sale invoice pdf content.
* @param {number} tenantId - Tenant Id.
* @param {ISaleInvoice} saleInvoice -
* @returns {Promise<Buffer>}
*/
public async saleInvoicePdf(
async saleInvoicePdf(
tenantId: number,
invoiceId: number
saleInvoice: ISaleInvoice
): Promise<Buffer> {
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
tenantId,
invoiceId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/invoice-regular',

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