mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0868eeaa0e | ||
|
|
17dbe9713b | ||
|
|
ecaf8c99bb | ||
|
|
90bde20674 | ||
|
|
0c61f85707 | ||
|
|
0f678e61c5 | ||
|
|
374f1acf8a | ||
|
|
ada1428193 | ||
|
|
528d447443 | ||
|
|
d6d67b9a51 | ||
|
|
12740223a8 | ||
|
|
f7f77b12c9 | ||
|
|
a6db4fb6df | ||
|
|
b38020d397 | ||
|
|
d090d5a026 | ||
|
|
7b5287ee80 | ||
|
|
ba3ea93a2d | ||
|
|
6b77bda77f | ||
|
|
ba387e81f7 | ||
|
|
0414c090ea | ||
|
|
a52f3a933f | ||
|
|
d01eacf8d9 | ||
|
|
0da151faaa | ||
|
|
f93c8b46dc | ||
|
|
59c5c8979d | ||
|
|
74a07847a4 | ||
|
|
1740226294 | ||
|
|
1b20d1b073 | ||
|
|
ac7175d83b | ||
|
|
d72d8e60d6 | ||
|
|
de5920f910 | ||
|
|
475c4e9967 | ||
|
|
21eb88ef53 | ||
|
|
429159acf9 | ||
|
|
7eb84474a5 | ||
|
|
e42adcae63 |
@@ -96,6 +96,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"bug"
|
"bug"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "asenawritescode",
|
||||||
|
"name": "Asena",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/67445192?v=4",
|
||||||
|
"profile": "https://github.com/asenawritescode",
|
||||||
|
"contributions": [
|
||||||
|
"bug"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
@@ -57,4 +57,10 @@ GOTENBERG_DOCS_URL=http://server:3000/public/
|
|||||||
|
|
||||||
# Gotenberg API - (development)
|
# Gotenberg API - (development)
|
||||||
# GOTENBERG_URL=http://localhost:9000
|
# GOTENBERG_URL=http://localhost:9000
|
||||||
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/
|
# 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=
|
||||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
All notable changes to Bigcapital server-side will be in this file.
|
All notable changes to Bigcapital server-side will be in this file.
|
||||||
|
|
||||||
|
## [0.14.0] - 30-01-2024
|
||||||
|
|
||||||
|
* feat: purchases by items exporting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/327
|
||||||
|
* fix: expense amounts should not be rounded by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/339
|
||||||
|
* feat: get latest exchange rate from third party services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/340
|
||||||
|
* fix(webapp): inconsistency in currency of universal search items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/335
|
||||||
|
* hotfix: editing sales and expense transactions don't reflect GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/342
|
||||||
|
|
||||||
|
## [0.13.3] - 22-01-2024
|
||||||
|
|
||||||
|
* hotfix(server): Unhandled thrown errors of services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/329
|
||||||
|
|
||||||
|
## [0.13.2] - 21-01-2024
|
||||||
|
|
||||||
|
* feat: show customer / vendor balance. by @asenawritescode in https://github.com/bigcapitalhq/bigcapital/pull/311
|
||||||
|
* feat: inventory valuation csv and xlsx export by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/308
|
||||||
|
* feat: sales by items export csv & xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/310
|
||||||
|
* fix(server): the invoice and payment receipt printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/315
|
||||||
|
* fix: get cashflow transaction broken cause transaction type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/318
|
||||||
|
* fix: `AccountActivateAlert` import by @xprnio in https://github.com/bigcapitalhq/bigcapital/pull/322
|
||||||
|
|
||||||
## [0.13.1] - 15-01-2024
|
## [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
|
* feat(webapp): add approve/reject to action bar of estimate details dr… by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/304
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://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://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -26,27 +26,27 @@ export default class ContactsController extends BaseController {
|
|||||||
[...this.autocompleteQuerySchema],
|
[...this.autocompleteQuerySchema],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
|
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
|
||||||
this.dynamicListService.handlerErrorsToResponse
|
this.dynamicListService.handlerErrorsToResponse,
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/:id',
|
'/:id',
|
||||||
[param('id').exists().isNumeric().toInt()],
|
[param('id').exists().isNumeric().toInt()],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.asyncMiddleware(this.getContact.bind(this))
|
this.asyncMiddleware(this.getContact.bind(this)),
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/:id/inactivate',
|
'/:id/inactivate',
|
||||||
[param('id').exists().isNumeric().toInt()],
|
[param('id').exists().isNumeric().toInt()],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.asyncMiddleware(this.inactivateContact.bind(this)),
|
this.asyncMiddleware(this.inactivateContact.bind(this)),
|
||||||
this.handlerServiceErrors
|
this.handlerServiceErrors,
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/:id/activate',
|
'/:id/activate',
|
||||||
[param('id').exists().isNumeric().toInt()],
|
[param('id').exists().isNumeric().toInt()],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.asyncMiddleware(this.activateContact.bind(this)),
|
this.asyncMiddleware(this.activateContact.bind(this)),
|
||||||
this.handlerServiceErrors
|
this.handlerServiceErrors,
|
||||||
);
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ export default class ContactsController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
const contact = await this.contactsService.getContact(
|
const contact = await this.contactsService.getContact(
|
||||||
tenantId,
|
tenantId,
|
||||||
contactId
|
contactId,
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
customer: this.transfromToResponse(contact),
|
customer: this.transfromToResponse(contact),
|
||||||
@@ -105,7 +105,7 @@ export default class ContactsController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
const contacts = await this.contactsService.autocompleteContacts(
|
const contacts = await this.contactsService.autocompleteContacts(
|
||||||
tenantId,
|
tenantId,
|
||||||
filter
|
filter,
|
||||||
);
|
);
|
||||||
return res.status(200).send({ contacts });
|
return res.status(200).send({ contacts });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -153,7 +153,6 @@ export default class ContactsController extends BaseController {
|
|||||||
check('email')
|
check('email')
|
||||||
.optional({ nullable: true })
|
.optional({ nullable: true })
|
||||||
.isString()
|
.isString()
|
||||||
.normalizeEmail()
|
|
||||||
.isEmail()
|
.isEmail()
|
||||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||||
check('website')
|
check('website')
|
||||||
@@ -380,7 +379,7 @@ export default class ContactsController extends BaseController {
|
|||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction,
|
||||||
) {
|
) {
|
||||||
if (error instanceof ServiceError) {
|
if (error instanceof ServiceError) {
|
||||||
if (error.errorType === 'contact_not_found') {
|
if (error.errorType === 'contact_not_found') {
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { check, param, query } from 'express-validator';
|
import { query, oneOf } from 'express-validator';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseController from './BaseController';
|
import BaseController from './BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
|
import { EchangeRateErrors } from '@/lib/ExchangeRate/types';
|
||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import { ExchangeRateApplication } from '@/services/ExchangeRates/ExchangeRateApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ExchangeRatesController extends BaseController {
|
export default class ExchangeRatesController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
exchangeRatesService: ExchangeRatesService;
|
private exchangeRatesApp: ExchangeRateApplication;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
dynamicListService: DynamicListingService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
@@ -22,164 +19,40 @@ export default class ExchangeRatesController extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/latest',
|
||||||
[...this.exchangeRatesListSchema],
|
[
|
||||||
|
oneOf([
|
||||||
|
query('to_currency').exists().isString().isISO4217(),
|
||||||
|
query('from_currency').exists().isString().isISO4217(),
|
||||||
|
]),
|
||||||
|
],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
asyncMiddleware(this.exchangeRates.bind(this)),
|
asyncMiddleware(this.latestExchangeRate.bind(this)),
|
||||||
this.dynamicListService.handlerErrorsToResponse,
|
|
||||||
this.handleServiceError,
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
'/',
|
|
||||||
[...this.exchangeRateDTOSchema],
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.addExchangeRate.bind(this)),
|
|
||||||
this.handleServiceError
|
|
||||||
);
|
|
||||||
router.post(
|
|
||||||
'/:id',
|
|
||||||
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.editExchangeRate.bind(this)),
|
|
||||||
this.handleServiceError
|
|
||||||
);
|
|
||||||
router.delete(
|
|
||||||
'/:id',
|
|
||||||
[...this.exchangeRateIdSchema],
|
|
||||||
this.validationResult,
|
|
||||||
asyncMiddleware(this.deleteExchangeRate.bind(this)),
|
|
||||||
this.handleServiceError
|
this.handleServiceError
|
||||||
);
|
);
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
get exchangeRatesListSchema() {
|
|
||||||
return [
|
|
||||||
query('page').optional().isNumeric().toInt(),
|
|
||||||
query('page_size').optional().isNumeric().toInt(),
|
|
||||||
|
|
||||||
query('column_sort_by').optional(),
|
|
||||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
get exchangeRateDTOSchema() {
|
|
||||||
return [
|
|
||||||
check('exchange_rate').exists().isNumeric().toFloat(),
|
|
||||||
check('currency_code').exists().trim().escape(),
|
|
||||||
check('date').exists().isISO8601(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
get exchangeRateEditDTOSchema() {
|
|
||||||
return [check('exchange_rate').exists().isNumeric().toFloat()];
|
|
||||||
}
|
|
||||||
|
|
||||||
get exchangeRateIdSchema() {
|
|
||||||
return [param('id').isNumeric().toInt()];
|
|
||||||
}
|
|
||||||
|
|
||||||
get exchangeRatesIdsSchema() {
|
|
||||||
return [
|
|
||||||
query('ids').isArray({ min: 2 }),
|
|
||||||
query('ids.*').isNumeric().toInt(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve exchange rates.
|
* Retrieve exchange rates.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
async exchangeRates(req: Request, res: Response, next: NextFunction) {
|
private async latestExchangeRate(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const filter = {
|
const exchangeRateQuery = this.matchedQueryData(req);
|
||||||
page: 1,
|
|
||||||
pageSize: 12,
|
|
||||||
filterRoles: [],
|
|
||||||
columnSortBy: 'created_at',
|
|
||||||
sortOrder: 'asc',
|
|
||||||
...this.matchedQueryData(req),
|
|
||||||
};
|
|
||||||
if (filter.stringifiedFilterRoles) {
|
|
||||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
|
|
||||||
tenantId,
|
|
||||||
filter
|
|
||||||
);
|
|
||||||
return res.status(200).send({ exchange_rates: exchangeRates });
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new exchange rate on the given date.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const exchangeRateDTO = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
|
const exchangeRate = await this.exchangeRatesApp.latest(
|
||||||
tenantId,
|
tenantId,
|
||||||
exchangeRateDTO
|
exchangeRateQuery
|
||||||
);
|
);
|
||||||
return res.status(200).send({ id: exchangeRate.id });
|
return res.status(200).send(exchangeRate);
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edit the given exchange rate.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const { id: exchangeRateId } = req.params;
|
|
||||||
const exchangeRateDTO = this.matchedBodyData(req);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
|
|
||||||
tenantId,
|
|
||||||
exchangeRateId,
|
|
||||||
exchangeRateDTO
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
id: exchangeRateId,
|
|
||||||
message: 'The exchange rate has been edited successfully.',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the given exchange rate from the storage.
|
|
||||||
* @param {Request} req
|
|
||||||
* @param {Response} res
|
|
||||||
* @param {NextFunction} next
|
|
||||||
*/
|
|
||||||
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
|
|
||||||
const { tenantId } = req;
|
|
||||||
const { id: exchangeRateId } = req.params;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.exchangeRatesService.deleteExchangeRate(
|
|
||||||
tenantId,
|
|
||||||
exchangeRateId
|
|
||||||
);
|
|
||||||
return res.status(200).send({ id: exchangeRateId });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -192,26 +65,56 @@ export default class ExchangeRatesController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
handleServiceError(
|
private handleServiceError(
|
||||||
error: Error,
|
error: Error,
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) {
|
) {
|
||||||
if (error instanceof ServiceError) {
|
if (error instanceof ServiceError) {
|
||||||
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
|
if (EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY === error.errorType) {
|
||||||
return res.status(404).send({
|
|
||||||
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
|
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
|
errors: [
|
||||||
|
{
|
||||||
|
type: EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
|
||||||
|
code: 100,
|
||||||
|
message: 'The given base currency is invalid.',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
} else if (
|
||||||
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
|
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED === error.errorType
|
||||||
|
) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
|
errors: [
|
||||||
|
{
|
||||||
|
type: EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
|
||||||
|
code: 200,
|
||||||
|
message: 'The service is not allowed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED === error.errorType
|
||||||
|
) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
|
||||||
|
code: 300,
|
||||||
|
message: 'The API key is required',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} else if (EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED === error.errorType) {
|
||||||
|
return res.status(400).send({
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED,
|
||||||
|
code: 400,
|
||||||
|
message: 'The API rate limit has been exceeded',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { query, ValidationChain } from 'express-validator';
|
import { query, ValidationChain } from 'express-validator';
|
||||||
import moment from 'moment';
|
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||||
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
import { PurchasesByItemsService } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
||||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
import { PurcahsesByItemsApplication } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class PurchasesByItemReportController extends BaseFinancialReportController {
|
export default class PurchasesByItemReportController extends BaseFinancialReportController {
|
||||||
@Inject()
|
@Inject()
|
||||||
purchasesByItemsService: PurchasesByItemsService;
|
private purchasesByItemsApp: PurcahsesByItemsApplication;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
@@ -63,20 +64,47 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
|
|||||||
* @param {Request} req -
|
* @param {Request} req -
|
||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
*/
|
*/
|
||||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
public async purchasesByItems(req: Request, res: Response) {
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const filter = this.matchedQueryData(req);
|
const filter = this.matchedQueryData(req);
|
||||||
|
|
||||||
try {
|
const accept = this.accepts(req);
|
||||||
const { data, query, meta } =
|
|
||||||
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
|
const acceptType = accept.types([
|
||||||
return res.status(200).send({
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
meta: this.transfromToResponse(meta),
|
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
|
||||||
data: this.transfromToResponse(data),
|
ACCEPT_TYPE.APPLICATION_XLSX,
|
||||||
query: this.transfromToResponse(query),
|
ACCEPT_TYPE.APPLICATION_CSV,
|
||||||
});
|
]);
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ export default class BillsController extends BaseController {
|
|||||||
try {
|
try {
|
||||||
const bill = await this.billsApplication.getBill(tenantId, billId);
|
const bill = await this.billsApplication.getBill(tenantId, billId);
|
||||||
|
|
||||||
return res.status(200).send(this.transfromToResponse({ bill }));
|
return res.status(200).send({ bill });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -348,14 +348,11 @@ export default class BillsController extends BaseController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { bills, pagination, filterMeta } =
|
const billsWithPagination = await this.billsApplication.getBills(
|
||||||
await this.billsApplication.getBills(tenantId, filter);
|
tenantId,
|
||||||
|
filter
|
||||||
return res.status(200).send({
|
);
|
||||||
bills: this.transfromToResponse(bills),
|
return res.status(200).send(billsWithPagination);
|
||||||
pagination: this.transfromToResponse(pagination),
|
|
||||||
filter_meta: this.transfromToResponse(filterMeta),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -563,6 +560,16 @@ export default class BillsController extends BaseController {
|
|||||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 1900 }],
|
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 1900 }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (error.errorType === 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT') {
|
||||||
|
return res.boom.badRequest(null, {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
type: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT',
|
||||||
|
code: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,15 +158,11 @@ export default class BillsPayments extends BaseController {
|
|||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { vendorId } = this.matchedQueryData(req);
|
const { vendorId } = this.matchedQueryData(req);
|
||||||
|
|
||||||
try {
|
const entries = await this.billPaymentsPages.getNewPageEntries(
|
||||||
const entries = await this.billPaymentsPages.getNewPageEntries(
|
tenantId,
|
||||||
tenantId,
|
vendorId
|
||||||
vendorId
|
);
|
||||||
);
|
return res.status(200).send({ entries });
|
||||||
return res.status(200).send({
|
|
||||||
entries: this.transfromToResponse(entries),
|
|
||||||
});
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,16 +179,12 @@ export default class BillsPayments extends BaseController {
|
|||||||
const { id: paymentReceiveId } = req.params;
|
const { id: paymentReceiveId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { billPayment, entries } =
|
const billPaymentsWithEditEntries =
|
||||||
await this.billPaymentsPages.getBillPaymentEditPage(
|
await this.billPaymentsPages.getBillPaymentEditPage(
|
||||||
tenantId,
|
tenantId,
|
||||||
paymentReceiveId
|
paymentReceiveId
|
||||||
);
|
);
|
||||||
|
return res.status(200).send(billPaymentsWithEditEntries);
|
||||||
return res.status(200).send({
|
|
||||||
bill_payment: this.transfromToResponse(billPayment),
|
|
||||||
entries: this.transfromToResponse(entries),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -304,9 +296,7 @@ export default class BillsPayments extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
billPaymentId
|
billPaymentId
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({ billPayment });
|
||||||
bill_payment: this.transfromToResponse(billPayment),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -359,17 +349,12 @@ export default class BillsPayments extends BaseController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { billPayments, pagination, filterMeta } =
|
const billPaymentsWithPagination =
|
||||||
await this.billPaymentsApplication.getBillPayments(
|
await this.billPaymentsApplication.getBillPayments(
|
||||||
tenantId,
|
tenantId,
|
||||||
billPaymentsFilter
|
billPaymentsFilter
|
||||||
);
|
);
|
||||||
|
return res.status(200).send(billPaymentsWithPagination);
|
||||||
return res.status(200).send({
|
|
||||||
bill_payments: this.transfromToResponse(billPayments),
|
|
||||||
pagination: this.transfromToResponse(pagination),
|
|
||||||
filter_meta: this.transfromToResponse(filterMeta),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -320,20 +320,19 @@ export default class VendorCreditController extends BaseController {
|
|||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
) => {
|
) => {
|
||||||
const { id: billId } = req.params;
|
const { id: vendorCreditId } = req.params;
|
||||||
const { tenantId, user } = req;
|
const { tenantId, user } = req;
|
||||||
const vendorCreditEditDTO: IVendorCreditEditDTO = this.matchedBodyData(req);
|
const vendorCreditEditDTO: IVendorCreditEditDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.editVendorCreditService.editVendorCredit(
|
await this.editVendorCreditService.editVendorCredit(
|
||||||
tenantId,
|
tenantId,
|
||||||
billId,
|
vendorCreditId,
|
||||||
vendorCreditEditDTO,
|
vendorCreditEditDTO
|
||||||
user
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: billId,
|
id: vendorCreditId,
|
||||||
message: 'The vendor credit has been edited successfully.',
|
message: 'The vendor credit has been edited successfully.',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import GetCreditNoteAssociatedInvoicesToApply from '@/services/CreditNotes/GetCr
|
|||||||
import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCreditNoteAssociatedAppliedInvoices';
|
import GetCreditNoteAssociatedAppliedInvoices from '@/services/CreditNotes/GetCreditNoteAssociatedAppliedInvoices';
|
||||||
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
|
import GetRefundCreditTransaction from '@/services/CreditNotes/GetRefundCreditNoteTransaction';
|
||||||
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
|
import GetCreditNotePdf from '../../../services/CreditNotes/GetCreditNotePdf';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
/**
|
/**
|
||||||
* Credit notes controller.
|
* Credit notes controller.
|
||||||
* @service
|
* @service
|
||||||
@@ -293,7 +294,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
return [
|
return [
|
||||||
check('from_account_id').exists().isNumeric().toInt(),
|
check('from_account_id').exists().isNumeric().toInt(),
|
||||||
check('description').optional(),
|
check('description').optional(),
|
||||||
|
|
||||||
check('amount').exists().isNumeric().toFloat(),
|
check('amount').exists().isNumeric().toFloat(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
|
|
||||||
@@ -438,7 +439,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the payment receive details.
|
* Retrieve the credit note details.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
@@ -451,38 +452,28 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { id: creditNoteId } = req.params;
|
const { id: creditNoteId } = req.params;
|
||||||
|
|
||||||
try {
|
const accept = this.accepts(req);
|
||||||
|
|
||||||
|
const acceptType = accept.types([
|
||||||
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
|
]);
|
||||||
|
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||||
|
const pdfContent = await this.creditNotePdf.getCreditNotePdf(
|
||||||
|
tenantId,
|
||||||
|
creditNoteId
|
||||||
|
);
|
||||||
|
res.set({
|
||||||
|
'Content-Type': 'application/pdf',
|
||||||
|
'Content-Length': pdfContent.length,
|
||||||
|
});
|
||||||
|
res.send(pdfContent);
|
||||||
|
} else {
|
||||||
const creditNote = await this.getCreditNoteService.getCreditNote(
|
const creditNote = await this.getCreditNoteService.getCreditNote(
|
||||||
tenantId,
|
tenantId,
|
||||||
creditNoteId
|
creditNoteId
|
||||||
);
|
);
|
||||||
const ACCEPT_TYPE = {
|
return res.status(200).send({ creditNote });
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
|
|||||||
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
|
import { PaymentReceivesApplication } from '@/services/Sales/PaymentReceives/PaymentReceivesApplication';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class PaymentReceivesController extends BaseController {
|
export default class PaymentReceivesController extends BaseController {
|
||||||
@@ -348,17 +349,12 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { paymentReceives, pagination, filterMeta } =
|
const paymentsReceivedWithPagination =
|
||||||
await this.paymentReceiveApplication.getPaymentReceives(
|
await this.paymentReceiveApplication.getPaymentReceives(
|
||||||
tenantId,
|
tenantId,
|
||||||
filter
|
filter
|
||||||
);
|
);
|
||||||
|
return res.status(200).send(paymentsReceivedWithPagination);
|
||||||
return res.status(200).send({
|
|
||||||
payment_receives: this.transfromToResponse(paymentReceives),
|
|
||||||
pagination: this.transfromToResponse(pagination),
|
|
||||||
filter_meta: this.transfromToResponse(filterMeta),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -435,37 +431,34 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
const { id: paymentReceiveId } = req.params;
|
const { id: paymentReceiveId } = req.params;
|
||||||
|
|
||||||
try {
|
const accept = this.accepts(req);
|
||||||
const ACCEPT_TYPE = {
|
|
||||||
APPLICATION_PDF: 'application/pdf',
|
const acceptType = accept.types([
|
||||||
APPLICATION_JSON: 'application/json',
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
};
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
res.format({
|
]);
|
||||||
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
// Response in pdf format.
|
||||||
const paymentReceive =
|
if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
|
||||||
await this.paymentReceiveApplication.getPaymentReceive(
|
const pdfContent =
|
||||||
tenantId,
|
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
||||||
paymentReceiveId
|
tenantId,
|
||||||
);
|
paymentReceiveId
|
||||||
return res.status(200).send({
|
);
|
||||||
payment_receive: paymentReceive,
|
res.set({
|
||||||
});
|
'Content-Type': 'application/pdf',
|
||||||
},
|
'Content-Length': pdfContent.length,
|
||||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
});
|
||||||
const pdfContent =
|
res.send(pdfContent);
|
||||||
await this.paymentReceiveApplication.getPaymentReceivePdf(
|
// Response in json format.
|
||||||
tenantId,
|
} else {
|
||||||
paymentReceiveId
|
const paymentReceive =
|
||||||
);
|
await this.paymentReceiveApplication.getPaymentReceive(
|
||||||
res.set({
|
tenantId,
|
||||||
'Content-Type': 'application/pdf',
|
paymentReceiveId
|
||||||
'Content-Length': pdfContent.length,
|
);
|
||||||
});
|
return res.status(200).send({
|
||||||
res.send(pdfContent);
|
payment_receive: paymentReceive,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +492,7 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Retrieves the sms details of the given payment receive.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
@@ -588,10 +581,10 @@ export default class PaymentReceivesController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles service errors.
|
* Handles service errors.
|
||||||
* @param error
|
* @param {Error} error
|
||||||
* @param req
|
* @param {Request} req
|
||||||
* @param res
|
* @param {Response} res
|
||||||
* @param next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
private handleServiceErrors(
|
private handleServiceErrors(
|
||||||
error: Error,
|
error: Error,
|
||||||
|
|||||||
@@ -334,7 +334,6 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
estimateId
|
estimateId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: estimateId,
|
id: estimateId,
|
||||||
message: 'The sale estimate has been approved successfully.',
|
message: 'The sale estimate has been approved successfully.',
|
||||||
@@ -363,7 +362,6 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
estimateId
|
estimateId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: estimateId,
|
id: estimateId,
|
||||||
message: 'The sale estimate has been rejected successfully.',
|
message: 'The sale estimate has been rejected successfully.',
|
||||||
@@ -383,33 +381,30 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
const { id: estimateId } = req.params;
|
const { id: estimateId } = req.params;
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
|
||||||
try {
|
const accept = this.accepts(req);
|
||||||
// Response formatter.
|
|
||||||
res.format({
|
const acceptType = accept.types([
|
||||||
// JSON content type.
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
]);
|
||||||
tenantId,
|
// Retrieves estimate in pdf format.
|
||||||
estimateId
|
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||||
);
|
const pdfContent = await this.saleEstimatesApplication.getSaleEstimatePdf(
|
||||||
return res.status(200).send({ estimate });
|
tenantId,
|
||||||
},
|
estimateId
|
||||||
// PDF content type.
|
);
|
||||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
res.set({
|
||||||
const pdfContent =
|
'Content-Type': 'application/pdf',
|
||||||
await this.saleEstimatesApplication.getSaleEstimatePdf(
|
'Content-Length': pdfContent.length,
|
||||||
tenantId,
|
|
||||||
estimateId
|
|
||||||
);
|
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/pdf',
|
|
||||||
'Content-Length': pdfContent.length,
|
|
||||||
});
|
|
||||||
res.send(pdfContent);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
res.send(pdfContent);
|
||||||
next(error);
|
// Retrieves estimates in json format.
|
||||||
|
} else {
|
||||||
|
const estimate = await this.saleEstimatesApplication.getSaleEstimate(
|
||||||
|
tenantId,
|
||||||
|
estimateId
|
||||||
|
);
|
||||||
|
return res.status(200).send({ estimate });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,22 +422,11 @@ export default class SalesEstimatesController extends BaseController {
|
|||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
...this.matchedQueryData(req),
|
...this.matchedQueryData(req),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { salesEstimates, pagination, filterMeta } =
|
const salesEstimatesWithPagination =
|
||||||
await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter);
|
await this.saleEstimatesApplication.getSaleEstimates(tenantId, filter);
|
||||||
|
|
||||||
res.format({
|
return res.status(200).send(salesEstimatesWithPagination);
|
||||||
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
|
|
||||||
return res.status(200).send(
|
|
||||||
this.transfromToResponse({
|
|
||||||
salesEstimates,
|
|
||||||
pagination,
|
|
||||||
filterMeta,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ import {
|
|||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
|
import { SaleInvoiceApplication } from '@/services/Sales/Invoices/SaleInvoicesApplication';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
|
||||||
const ACCEPT_TYPE = {
|
|
||||||
APPLICATION_PDF: 'application/pdf',
|
|
||||||
APPLICATION_JSON: 'application/json',
|
|
||||||
};
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SaleInvoicesController extends BaseController {
|
export default class SaleInvoicesController extends BaseController {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -403,7 +400,6 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
saleInvoiceId,
|
saleInvoiceId,
|
||||||
user
|
user
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: saleInvoiceId,
|
id: saleInvoiceId,
|
||||||
message: 'The sale invoice has been deleted successfully.',
|
message: 'The sale invoice has been deleted successfully.',
|
||||||
@@ -422,30 +418,32 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
const { id: saleInvoiceId } = req.params;
|
const { id: saleInvoiceId } = req.params;
|
||||||
const { tenantId, user } = req;
|
const { tenantId, user } = req;
|
||||||
|
|
||||||
// Response formatter.
|
const accept = this.accepts(req);
|
||||||
return res.format({
|
|
||||||
// JSON content type.
|
const acceptType = accept.types([
|
||||||
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
tenantId,
|
]);
|
||||||
saleInvoiceId,
|
// Retrieves invoice in pdf format.
|
||||||
user
|
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||||
);
|
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
|
||||||
return res.status(200).send(this.transfromToResponse({ saleInvoice }));
|
tenantId,
|
||||||
},
|
saleInvoiceId
|
||||||
// PDF content type.
|
);
|
||||||
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
|
res.set({
|
||||||
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
|
'Content-Type': 'application/pdf',
|
||||||
tenantId,
|
'Content-Length': pdfContent.length,
|
||||||
saleInvoiceId
|
});
|
||||||
);
|
res.send(pdfContent);
|
||||||
res.set({
|
// Retrieves invoice in json format.
|
||||||
'Content-Type': 'application/pdf',
|
} else {
|
||||||
'Content-Length': pdfContent.length,
|
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
|
||||||
});
|
tenantId,
|
||||||
res.send(pdfContent);
|
saleInvoiceId,
|
||||||
},
|
user
|
||||||
});
|
);
|
||||||
|
return res.status(200).send({ saleInvoice });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Retrieve paginated sales invoices with custom view metadata.
|
* Retrieve paginated sales invoices with custom view metadata.
|
||||||
@@ -467,14 +465,10 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
...this.matchedQueryData(req),
|
...this.matchedQueryData(req),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const { salesInvoices, filterMeta, pagination } =
|
const salesInvoicesWithPagination =
|
||||||
await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter);
|
await this.saleInvoiceApplication.getSaleInvoices(tenantId, filter);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send(salesInvoicesWithPagination);
|
||||||
sales_invoices: this.transfromToResponse(salesInvoices),
|
|
||||||
pagination: this.transfromToResponse(pagination),
|
|
||||||
filter_meta: this.transfromToResponse(filterMeta),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -501,9 +495,7 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
customerId
|
customerId
|
||||||
);
|
);
|
||||||
return res.status(200).send({
|
return res.status(200).send({ salesInvoices });
|
||||||
sales_invoices: this.transfromToResponse(salesInvoices),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -531,7 +523,6 @@ export default class SaleInvoicesController extends BaseController {
|
|||||||
invoiceId,
|
invoiceId,
|
||||||
writeoffDTO
|
writeoffDTO
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: saleInvoice.id,
|
id: saleInvoice.id,
|
||||||
message: 'The given sale invoice has been written-off successfully.',
|
message: 'The given sale invoice has been written-off successfully.',
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import { body, check, param, query } from 'express-validator';
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ISaleReceiptDTO, SaleReceiptMailOpts, SaleReceiptMailOptsDTO } from '@/interfaces/SaleReceipt';
|
import {
|
||||||
|
ISaleReceiptDTO,
|
||||||
|
SaleReceiptMailOpts,
|
||||||
|
SaleReceiptMailOptsDTO,
|
||||||
|
} from '@/interfaces/SaleReceipt';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||||
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
|
import { AbilitySubject, SaleReceiptAction } from '@/interfaces';
|
||||||
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
|
import { SaleReceiptApplication } from '@/services/Sales/Receipts/SaleReceiptApplication';
|
||||||
|
import { ACCEPT_TYPE } from '@/interfaces/Http';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SalesReceiptsController extends BaseController {
|
export default class SalesReceiptsController extends BaseController {
|
||||||
@@ -62,9 +67,7 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
'/:id/mail',
|
'/:id/mail',
|
||||||
[
|
[...this.specificReceiptValidationSchema],
|
||||||
...this.specificReceiptValidationSchema,
|
|
||||||
],
|
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
asyncMiddleware(this.getSaleReceiptMail.bind(this)),
|
asyncMiddleware(this.getSaleReceiptMail.bind(this)),
|
||||||
this.handleServiceErrors
|
this.handleServiceErrors
|
||||||
@@ -228,7 +231,6 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
tenantId,
|
tenantId,
|
||||||
saleReceiptId
|
saleReceiptId
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
id: saleReceiptId,
|
id: saleReceiptId,
|
||||||
message: 'Sale receipt has been deleted successfully.',
|
message: 'Sale receipt has been deleted successfully.',
|
||||||
@@ -317,15 +319,10 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
...this.matchedQueryData(req),
|
...this.matchedQueryData(req),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const { data, pagination, filterMeta } =
|
const salesReceiptsWithPagination =
|
||||||
await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter);
|
await this.saleReceiptsApplication.getSaleReceipts(tenantId, filter);
|
||||||
|
|
||||||
const response = this.transfromToResponse({
|
return res.status(200).send(salesReceiptsWithPagination);
|
||||||
data,
|
|
||||||
pagination,
|
|
||||||
filterMeta,
|
|
||||||
});
|
|
||||||
return res.status(200).send(response);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@@ -337,34 +334,34 @@ export default class SalesReceiptsController extends BaseController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
public async getSaleReceipt(req: Request, res: Response, next: NextFunction) {
|
public async getSaleReceipt(req: Request, res: Response) {
|
||||||
const { id: saleReceiptId } = req.params;
|
const { id: saleReceiptId } = req.params;
|
||||||
const { tenantId } = req;
|
const { tenantId } = req;
|
||||||
|
|
||||||
try {
|
const accept = this.accepts(req);
|
||||||
res.format({
|
|
||||||
'application/json': async () => {
|
const acceptType = accept.types([
|
||||||
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
ACCEPT_TYPE.APPLICATION_JSON,
|
||||||
tenantId,
|
ACCEPT_TYPE.APPLICATION_PDF,
|
||||||
saleReceiptId
|
]);
|
||||||
);
|
// Retrieves receipt in pdf format.
|
||||||
return res.status(200).send({ saleReceipt });
|
if (ACCEPT_TYPE.APPLICATION_PDF == acceptType) {
|
||||||
},
|
const pdfContent = await this.saleReceiptsApplication.getSaleReceiptPdf(
|
||||||
'application/pdf': async () => {
|
tenantId,
|
||||||
const pdfContent =
|
saleReceiptId
|
||||||
await this.saleReceiptsApplication.getSaleReceiptPdf(
|
);
|
||||||
tenantId,
|
res.set({
|
||||||
saleReceiptId
|
'Content-Type': 'application/pdf',
|
||||||
);
|
'Content-Length': pdfContent.length,
|
||||||
res.set({
|
|
||||||
'Content-Type': 'application/pdf',
|
|
||||||
'Content-Length': pdfContent.length,
|
|
||||||
});
|
|
||||||
res.send(pdfContent);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
res.send(pdfContent);
|
||||||
next(error);
|
// Retrieves receipt in json format.
|
||||||
|
} else {
|
||||||
|
const saleReceipt = await this.saleReceiptsApplication.getSaleReceipt(
|
||||||
|
tenantId,
|
||||||
|
saleReceiptId
|
||||||
|
);
|
||||||
|
return res.status(200).send({ saleReceipt });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,4 +169,14 @@ module.exports = {
|
|||||||
* to application detarmines to upgrade.
|
* to application detarmines to upgrade.
|
||||||
*/
|
*/
|
||||||
databaseBatch: 4,
|
databaseBatch: 4,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange rate.
|
||||||
|
*/
|
||||||
|
exchangeRate: {
|
||||||
|
service: 'open-exchange-rate',
|
||||||
|
openExchangeRate: {
|
||||||
|
appId: process.env.OPEN_EXCHANGE_RATE_APP_ID,
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
IAgingPeriod,
|
IAgingPeriod,
|
||||||
IAgingPeriodTotal,
|
|
||||||
IAgingAmount,
|
|
||||||
IAgingSummaryQuery,
|
IAgingSummaryQuery,
|
||||||
IAgingSummaryTotal,
|
IAgingSummaryTotal,
|
||||||
IAgingSummaryContact,
|
IAgingSummaryContact,
|
||||||
IAgingSummaryData,
|
IAgingSummaryData,
|
||||||
} from './AgingReport';
|
} from './AgingReport';
|
||||||
import { INumberFormatQuery } from './FinancialStatements';
|
|
||||||
import { IFinancialTable } from './Table';
|
import { IFinancialTable } from './Table';
|
||||||
|
|
||||||
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
|
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
|
||||||
|
|||||||
@@ -1,36 +1,10 @@
|
|||||||
import { IFilterRole } from './DynamicFilter';
|
export interface ExchangeRateLatestDTO {
|
||||||
|
toCurrency: string;
|
||||||
|
fromCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IExchangeRate {
|
export interface EchangeRateLatestPOJO {
|
||||||
id: number,
|
baseCurrency: string;
|
||||||
currencyCode: string,
|
toCurrency: string;
|
||||||
exchangeRate: number,
|
exchangeRate: number;
|
||||||
date: Date,
|
}
|
||||||
createdAt: Date,
|
|
||||||
updatedAt: Date,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IExchangeRateDTO {
|
|
||||||
currencyCode: string,
|
|
||||||
exchangeRate: number,
|
|
||||||
date: Date,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IExchangeRateEditDTO {
|
|
||||||
exchangeRate: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IExchangeRateFilter {
|
|
||||||
page: number,
|
|
||||||
pageSize: number,
|
|
||||||
filterRoles?: IFilterRole[];
|
|
||||||
columnSortBy: string;
|
|
||||||
sortOrder: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface IExchangeRatesService {
|
|
||||||
newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise<IExchangeRate>;
|
|
||||||
editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise<void>;
|
|
||||||
|
|
||||||
deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise<void>;
|
|
||||||
listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise<void>;
|
|
||||||
};
|
|
||||||
|
|||||||
54
packages/server/src/interfaces/PurchasesByItemsSheet.ts
Normal file
54
packages/server/src/interfaces/PurchasesByItemsSheet.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { INumberFormatQuery } from './FinancialStatements';
|
||||||
|
import { IFinancialTable } from './Table';
|
||||||
|
|
||||||
|
export interface IPurchasesByItemsReportQuery {
|
||||||
|
fromDate: Date | string;
|
||||||
|
toDate: Date | string;
|
||||||
|
itemsIds: number[];
|
||||||
|
numberFormat: INumberFormatQuery;
|
||||||
|
noneTransactions: boolean;
|
||||||
|
onlyActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchasesByItemsSheetMeta {
|
||||||
|
organizationName: string;
|
||||||
|
baseCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchasesByItemsItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
quantitySold: number;
|
||||||
|
soldCost: number;
|
||||||
|
averageSellPrice: number;
|
||||||
|
|
||||||
|
quantitySoldFormatted: string;
|
||||||
|
soldCostFormatted: string;
|
||||||
|
averageSellPriceFormatted: string;
|
||||||
|
currencyCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchasesByItemsTotal {
|
||||||
|
quantitySold: number;
|
||||||
|
soldCost: number;
|
||||||
|
quantitySoldFormatted: string;
|
||||||
|
soldCostFormatted: string;
|
||||||
|
currencyCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IPurchasesByItemsSheetData = {
|
||||||
|
items: IPurchasesByItemsItem[];
|
||||||
|
total: IPurchasesByItemsTotal;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IPurchasesByItemsSheet {
|
||||||
|
data: IPurchasesByItemsSheetData;
|
||||||
|
query: IPurchasesByItemsReportQuery;
|
||||||
|
meta: IPurchasesByItemsSheetMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPurchasesByItemsTable extends IFinancialTable {
|
||||||
|
query: IPurchasesByItemsReportQuery;
|
||||||
|
meta: IPurchasesByItemsSheetMeta;
|
||||||
|
}
|
||||||
45
packages/server/src/lib/ExchangeRate/ExchangeRate.ts
Normal file
45
packages/server/src/lib/ExchangeRate/ExchangeRate.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { OpenExchangeRate } from './OpenExchangeRate';
|
||||||
|
import { ExchangeRateServiceType, IExchangeRateService } from './types';
|
||||||
|
|
||||||
|
export class ExchangeRate {
|
||||||
|
private exchangeRateService: IExchangeRateService;
|
||||||
|
private exchangeRateServiceType: ExchangeRateServiceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param {ExchangeRateServiceType} service
|
||||||
|
*/
|
||||||
|
constructor(service: ExchangeRateServiceType) {
|
||||||
|
this.exchangeRateServiceType = service;
|
||||||
|
this.initService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the exchange rate service based on the service type.
|
||||||
|
*/
|
||||||
|
private initService() {
|
||||||
|
if (
|
||||||
|
this.exchangeRateServiceType === ExchangeRateServiceType.OpenExchangeRate
|
||||||
|
) {
|
||||||
|
this.setExchangeRateService(new OpenExchangeRate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the exchange rate service.
|
||||||
|
* @param {IExchangeRateService} service
|
||||||
|
*/
|
||||||
|
private setExchangeRateService(service: IExchangeRateService) {
|
||||||
|
this.exchangeRateService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the latest exchange rate.
|
||||||
|
* @param {string} baseCurrency
|
||||||
|
* @param {string} toCurrency
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public latest(baseCurrency: string, toCurrency: string): Promise<number> {
|
||||||
|
return this.exchangeRateService.latest(baseCurrency, toCurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
81
packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts
Normal file
81
packages/server/src/lib/ExchangeRate/OpenExchangeRate.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import Axios, { AxiosError } from 'axios';
|
||||||
|
import {
|
||||||
|
EchangeRateErrors,
|
||||||
|
IExchangeRateService,
|
||||||
|
OPEN_EXCHANGE_RATE_LATEST_URL,
|
||||||
|
} from './types';
|
||||||
|
import config from '@/config';
|
||||||
|
import { ServiceError } from '@/exceptions';
|
||||||
|
|
||||||
|
export class OpenExchangeRate implements IExchangeRateService {
|
||||||
|
/**
|
||||||
|
* Gets the latest exchange rate.
|
||||||
|
* @param {string} baseCurrency
|
||||||
|
* @param {string} toCurrency
|
||||||
|
* @returns {Promise<number}
|
||||||
|
*/
|
||||||
|
public async latest(
|
||||||
|
baseCurrency: string,
|
||||||
|
toCurrency: string
|
||||||
|
): Promise<number> {
|
||||||
|
// Vaclidates the Open Exchange Rate api id early.
|
||||||
|
this.validateApiIdExistance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await Axios.get(OPEN_EXCHANGE_RATE_LATEST_URL, {
|
||||||
|
params: {
|
||||||
|
app_id: config.exchangeRate.openExchangeRate.appId,
|
||||||
|
base: baseCurrency,
|
||||||
|
symbols: toCurrency,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return result.data.rates[toCurrency] || (1 as number);
|
||||||
|
} catch (error) {
|
||||||
|
this.handleLatestErrors(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the Open Exchange Rate api id.
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
private validateApiIdExistance() {
|
||||||
|
const apiId = config.exchangeRate.openExchangeRate.appId;
|
||||||
|
|
||||||
|
if (!apiId) {
|
||||||
|
throw new ServiceError(
|
||||||
|
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
|
||||||
|
'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the latest errors.
|
||||||
|
* @param {any} error
|
||||||
|
* @throws {ServiceError}
|
||||||
|
*/
|
||||||
|
private handleLatestErrors(error: any) {
|
||||||
|
if (error.response.data?.message === 'missing_app_id') {
|
||||||
|
throw new ServiceError(
|
||||||
|
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
|
||||||
|
'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.'
|
||||||
|
);
|
||||||
|
} else if (error.response.data?.message === 'invalid_app_id') {
|
||||||
|
throw new ServiceError(
|
||||||
|
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
|
||||||
|
'Invalid App ID provided. Please sign up at https://openexchangerates.org/signup, or contact support@openexchangerates.org.'
|
||||||
|
);
|
||||||
|
} else if (error.response.data?.message === 'not_allowed') {
|
||||||
|
throw new ServiceError(
|
||||||
|
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
|
||||||
|
'Getting the exchange rate from the given base currency to the given currency is not allowed.'
|
||||||
|
);
|
||||||
|
} else if (error.response.data?.message === 'invalid_base') {
|
||||||
|
throw new ServiceError(
|
||||||
|
EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
|
||||||
|
'The given base currency is invalid.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
packages/server/src/lib/ExchangeRate/types.ts
Normal file
17
packages/server/src/lib/ExchangeRate/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export interface IExchangeRateService {
|
||||||
|
latest(baseCurrency: string, toCurrency: string): Promise<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ExchangeRateServiceType {
|
||||||
|
OpenExchangeRate = 'OpenExchangeRate',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum EchangeRateErrors {
|
||||||
|
EX_RATE_SERVICE_NOT_ALLOWED = 'EX_RATE_SERVICE_NOT_ALLOWED',
|
||||||
|
EX_RATE_LIMIT_EXCEEDED = 'EX_RATE_LIMIT_EXCEEDED',
|
||||||
|
EX_RATE_SERVICE_API_KEY_REQUIRED = 'EX_RATE_SERVICE_API_KEY_REQUIRED',
|
||||||
|
EX_RATE_INVALID_BASE_CURRENCY = 'EX_RATE_INVALID_BASE_CURRENCY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OPEN_EXCHANGE_RATE_LATEST_URL =
|
||||||
|
'https://openexchangerates.org/api/latest.json';
|
||||||
@@ -2,6 +2,8 @@ import { Model } from 'objection';
|
|||||||
import TenantModel from 'models/TenantModel';
|
import TenantModel from 'models/TenantModel';
|
||||||
|
|
||||||
export default class ExpenseCategory extends TenantModel {
|
export default class ExpenseCategory extends TenantModel {
|
||||||
|
amount: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import {
|
|||||||
IAccount,
|
IAccount,
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
IAccountEditDTO,
|
IAccountEditDTO,
|
||||||
|
IAccountResponse,
|
||||||
IAccountsFilter,
|
IAccountsFilter,
|
||||||
IAccountsTransactionsFilter,
|
IAccountsTransactionsFilter,
|
||||||
|
IFilterMeta,
|
||||||
IGetAccountTransactionPOJO,
|
IGetAccountTransactionPOJO,
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { CreateAccount } from './CreateAccount';
|
import { CreateAccount } from './CreateAccount';
|
||||||
@@ -14,6 +16,7 @@ import { ActivateAccount } from './ActivateAccount';
|
|||||||
import { GetAccounts } from './GetAccounts';
|
import { GetAccounts } from './GetAccounts';
|
||||||
import { GetAccount } from './GetAccount';
|
import { GetAccount } from './GetAccount';
|
||||||
import { GetAccountTransactions } from './GetAccountTransactions';
|
import { GetAccountTransactions } from './GetAccountTransactions';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class AccountsApplication {
|
export class AccountsApplication {
|
||||||
@Inject()
|
@Inject()
|
||||||
@@ -113,19 +116,22 @@ export class AccountsApplication {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the accounts list.
|
* Retrieves the accounts list.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IAccountsFilter} filterDTO
|
* @param {IAccountsFilter} filterDTO
|
||||||
* @returns
|
* @returns {Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }>}
|
||||||
*/
|
*/
|
||||||
public getAccounts = (tenantId: number, filterDTO: IAccountsFilter) => {
|
public getAccounts = (
|
||||||
|
tenantId: number,
|
||||||
|
filterDTO: IAccountsFilter
|
||||||
|
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
|
||||||
return this.getAccountsService.getAccountsList(tenantId, filterDTO);
|
return this.getAccountsService.getAccountsList(tenantId, filterDTO);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the given account transactions.
|
* Retrieves the given account transactions.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IAccountsTransactionsFilter} filter
|
* @param {IAccountsTransactionsFilter} filter
|
||||||
* @returns {Promise<IGetAccountTransactionPOJO[]>}
|
* @returns {Promise<IGetAccountTransactionPOJO[]>}
|
||||||
*/
|
*/
|
||||||
public getAccountsTransactions = (
|
public getAccountsTransactions = (
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export class CreditNoteTransformer extends Transformer {
|
|||||||
'formattedCreditNoteDate',
|
'formattedCreditNoteDate',
|
||||||
'formattedAmount',
|
'formattedAmount',
|
||||||
'formattedCreditsUsed',
|
'formattedCreditsUsed',
|
||||||
|
'formattedSubtotal',
|
||||||
'entries',
|
'entries',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -60,6 +61,15 @@ export class CreditNoteTransformer extends Transformer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted subtotal.
|
||||||
|
* @param {ICreditNote} credit
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedSubtotal = (credit): string => {
|
||||||
|
return formatNumber(credit.amount, { money: false });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the entries of the credit note.
|
* Retrieves the entries of the credit note.
|
||||||
* @param {ICreditNote} credit
|
* @param {ICreditNote} credit
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default class EditCreditNote extends BaseCreditNotes {
|
|||||||
} as ICreditNoteEditingPayload);
|
} as ICreditNoteEditingPayload);
|
||||||
|
|
||||||
// Saves the credit note graph to the storage.
|
// Saves the credit note graph to the storage.
|
||||||
const creditNote = await CreditNote.query(trx).upsertGraph({
|
const creditNote = await CreditNote.query(trx).upsertGraphAndFetch({
|
||||||
id: creditNoteId,
|
id: creditNoteId,
|
||||||
...creditNoteModel,
|
...creditNoteModel,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
|
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
|
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable';
|
||||||
|
import GetCreditNote from './GetCreditNote';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class GetCreditNotePdf {
|
export default class GetCreditNotePdf {
|
||||||
@@ -10,11 +11,19 @@ export default class GetCreditNotePdf {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private templateInjectable: TemplateInjectable;
|
private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getCreditNoteService: GetCreditNote;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sale invoice pdf content.
|
* Retrieve sale invoice pdf content.
|
||||||
* @param {} saleInvoice -
|
* @param {number} tenantId - Tenant id.
|
||||||
|
* @param {number} creditNoteId - Credit note id.
|
||||||
*/
|
*/
|
||||||
public async getCreditNotePdf(tenantId: number, creditNote) {
|
public async getCreditNotePdf(tenantId: number, creditNoteId: number) {
|
||||||
|
const creditNote = await this.getCreditNoteService.getCreditNote(
|
||||||
|
tenantId,
|
||||||
|
creditNoteId
|
||||||
|
);
|
||||||
const htmlContent = await this.templateInjectable.render(
|
const htmlContent = await this.templateInjectable.render(
|
||||||
tenantId,
|
tenantId,
|
||||||
'modules/credit-note-standard',
|
'modules/credit-note-standard',
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Inject } from 'typedi';
|
||||||
|
import { ExchangeRatesService } from './ExchangeRatesService';
|
||||||
|
import { EchangeRateLatestPOJO, ExchangeRateLatestDTO } from '@/interfaces';
|
||||||
|
|
||||||
|
export class ExchangeRateApplication {
|
||||||
|
@Inject()
|
||||||
|
private exchangeRateService: ExchangeRatesService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the latest exchange rate.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {ExchangeRateLatestDTO} exchangeRateLatestDTO
|
||||||
|
* @returns {Promise<EchangeRateLatestPOJO>}
|
||||||
|
*/
|
||||||
|
public latest(
|
||||||
|
tenantId: number,
|
||||||
|
exchangeRateLatestDTO: ExchangeRateLatestDTO
|
||||||
|
): Promise<EchangeRateLatestPOJO> {
|
||||||
|
return this.exchangeRateService.latest(tenantId, exchangeRateLatestDTO);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,193 +1,37 @@
|
|||||||
import moment from 'moment';
|
import { Service } from 'typedi';
|
||||||
import { difference } from 'lodash';
|
import { ExchangeRate } from '@/lib/ExchangeRate/ExchangeRate';
|
||||||
import { Service, Inject } from 'typedi';
|
import { ExchangeRateServiceType } from '@/lib/ExchangeRate/types';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { EchangeRateLatestPOJO, ExchangeRateLatestDTO } from '@/interfaces';
|
||||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
import { TenantMetadata } from '@/system/models';
|
||||||
import {
|
|
||||||
EventDispatcher,
|
|
||||||
EventDispatcherInterface,
|
|
||||||
} from 'decorators/eventDispatcher';
|
|
||||||
import {
|
|
||||||
IExchangeRateDTO,
|
|
||||||
IExchangeRate,
|
|
||||||
IExchangeRatesService,
|
|
||||||
IExchangeRateEditDTO,
|
|
||||||
IExchangeRateFilter,
|
|
||||||
} from '@/interfaces';
|
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
|
|
||||||
const ERRORS = {
|
|
||||||
NOT_FOUND_EXCHANGE_RATES: 'NOT_FOUND_EXCHANGE_RATES',
|
|
||||||
EXCHANGE_RATE_PERIOD_EXISTS: 'EXCHANGE_RATE_PERIOD_EXISTS',
|
|
||||||
EXCHANGE_RATE_NOT_FOUND: 'EXCHANGE_RATE_NOT_FOUND',
|
|
||||||
};
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ExchangeRatesService implements IExchangeRatesService {
|
export class ExchangeRatesService {
|
||||||
@Inject('logger')
|
|
||||||
logger: any;
|
|
||||||
|
|
||||||
@EventDispatcher()
|
|
||||||
eventDispatcher: EventDispatcherInterface;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
tenancy: TenancyService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
dynamicListService: DynamicListingService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new exchange rate.
|
* Gets the latest exchange rate.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IExchangeRateDTO} exchangeRateDTO
|
* @param {number} exchangeRateLatestDTO
|
||||||
* @returns {Promise<IExchangeRate>}
|
* @returns {EchangeRateLatestPOJO}
|
||||||
*/
|
*/
|
||||||
public async newExchangeRate(
|
public async latest(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
exchangeRateDTO: IExchangeRateDTO
|
exchangeRateLatestDTO: ExchangeRateLatestDTO
|
||||||
): Promise<IExchangeRate> {
|
): Promise<EchangeRateLatestPOJO> {
|
||||||
const { ExchangeRate } = this.tenancy.models(tenantId);
|
const organization = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
this.logger.info('[exchange_rates] trying to insert new exchange rate.', {
|
// Assign the organization base currency as a default currency
|
||||||
tenantId,
|
// if no currency is provided
|
||||||
exchangeRateDTO,
|
const fromCurrency =
|
||||||
});
|
exchangeRateLatestDTO.fromCurrency || organization.baseCurrency;
|
||||||
await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO);
|
const toCurrency =
|
||||||
|
exchangeRateLatestDTO.toCurrency || organization.baseCurrency;
|
||||||
|
|
||||||
const exchangeRate = await ExchangeRate.query().insertAndFetch({
|
const exchange = new ExchangeRate(ExchangeRateServiceType.OpenExchangeRate);
|
||||||
...exchangeRateDTO,
|
const exchangeRate = await exchange.latest(fromCurrency, toCurrency);
|
||||||
date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'),
|
|
||||||
});
|
|
||||||
this.logger.info('[exchange_rates] inserted successfully.', {
|
|
||||||
tenantId,
|
|
||||||
exchangeRateDTO,
|
|
||||||
});
|
|
||||||
return exchangeRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return {
|
||||||
* Edits the exchange rate details.
|
baseCurrency: fromCurrency,
|
||||||
* @param {number} tenantId - Tenant id.
|
toCurrency: exchangeRateLatestDTO.toCurrency,
|
||||||
* @param {number} exchangeRateId - Exchange rate id.
|
exchangeRate,
|
||||||
* @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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export class EditExpense {
|
|||||||
} as IExpenseEventEditingPayload);
|
} as IExpenseEventEditingPayload);
|
||||||
|
|
||||||
// Upsert the expense object with expense entries.
|
// Upsert the expense object with expense entries.
|
||||||
const expense: IExpense = await Expense.query(trx).upsertGraph({
|
const expense: IExpense = await Expense.query(trx).upsertGraphAndFetch({
|
||||||
id: expenseId,
|
id: expenseId,
|
||||||
...expenseObj,
|
...expenseObj,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
import { ExpenseCategory } from '@/models';
|
||||||
|
import { formatNumber } from '@/utils';
|
||||||
|
|
||||||
|
export class ExpenseCategoryTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to expense object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['amountFormatted'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted amount.
|
||||||
|
* @param {ExpenseCategory} category
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected amountFormatted(category: ExpenseCategory) {
|
||||||
|
return formatNumber(category.amount, {
|
||||||
|
currencyCode: this.context.currencyCode,
|
||||||
|
money: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
import { formatNumber } from 'utils';
|
import { formatNumber } from 'utils';
|
||||||
import { IExpense } from '@/interfaces';
|
import { IExpense } from '@/interfaces';
|
||||||
|
import { ExpenseCategoryTransformer } from './ExpenseCategoryTransformer';
|
||||||
|
|
||||||
export class ExpenseTransfromer extends Transformer {
|
export class ExpenseTransfromer extends Transformer {
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +13,8 @@ export class ExpenseTransfromer extends Transformer {
|
|||||||
'formattedAmount',
|
'formattedAmount',
|
||||||
'formattedLandedCostAmount',
|
'formattedLandedCostAmount',
|
||||||
'formattedAllocatedCostAmount',
|
'formattedAllocatedCostAmount',
|
||||||
'formattedDate'
|
'formattedDate',
|
||||||
|
'categories',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,5 +58,16 @@ export class ExpenseTransfromer extends Transformer {
|
|||||||
*/
|
*/
|
||||||
protected formattedDate = (expense: IExpense): string => {
|
protected formattedDate = (expense: IExpense): string => {
|
||||||
return this.formatDate(expense.paymentDate);
|
return this.formatDate(expense.paymentDate);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the transformed expense categories.
|
||||||
|
* @param {IExpense} expense
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
protected categories = (expense: IExpense) => {
|
||||||
|
return this.item(expense.categories, new ExpenseCategoryTransformer(), {
|
||||||
|
currencyCode: expense.currencyCode,
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import { ExpenseGLEntries } from './ExpenseGLEntries';
|
import { ExpenseGLEntries } from './ExpenseGLEntries';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ export class ExpensesWriteGLSubscriber {
|
|||||||
authorizedUser,
|
authorizedUser,
|
||||||
trx,
|
trx,
|
||||||
}: IExpenseEventEditPayload) => {
|
}: IExpenseEventEditPayload) => {
|
||||||
// In case expense published, write journal entries.
|
// Cannot continue if the expense is not published.
|
||||||
if (expense.publishedAt) return;
|
if (!expense.publishedAt) return;
|
||||||
|
|
||||||
await this.expenseGLEntries.writeExpenseGLEntries(
|
await this.expenseGLEntries.rewriteExpenseGLEntries(
|
||||||
tenantId,
|
tenantId,
|
||||||
expense.id,
|
expense.id,
|
||||||
trx
|
trx
|
||||||
|
|||||||
@@ -2,36 +2,34 @@ import { get, isEmpty, sumBy } from 'lodash';
|
|||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import FinancialSheet from '../FinancialSheet';
|
import FinancialSheet from '../FinancialSheet';
|
||||||
import { allPassedConditionsPass, transformToMap } from 'utils';
|
import { allPassedConditionsPass, transformToMap } from 'utils';
|
||||||
|
import { IAccountTransaction, IItem } from '@/interfaces';
|
||||||
import {
|
import {
|
||||||
IAccountTransaction,
|
IPurchasesByItemsItem,
|
||||||
IInventoryValuationTotal,
|
IPurchasesByItemsReportQuery,
|
||||||
IInventoryValuationItem,
|
IPurchasesByItemsSheetData,
|
||||||
IInventoryValuationReportQuery,
|
IPurchasesByItemsTotal,
|
||||||
IInventoryValuationStatement,
|
} from '@/interfaces/PurchasesByItemsSheet';
|
||||||
IItem,
|
|
||||||
} from '@/interfaces';
|
|
||||||
|
|
||||||
export default class InventoryValuationReport extends FinancialSheet {
|
export class PurchasesByItems extends FinancialSheet {
|
||||||
readonly baseCurrency: string;
|
readonly baseCurrency: string;
|
||||||
readonly items: IItem[];
|
readonly items: IItem[];
|
||||||
readonly itemsTransactions: Map<number, IAccountTransaction>;
|
readonly itemsTransactions: Map<number, IAccountTransaction>;
|
||||||
readonly query: IInventoryValuationReportQuery;
|
readonly query: IPurchasesByItemsReportQuery;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {IInventoryValuationReportQuery} query
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
* @param {IItem[]} items
|
* @param {IItem[]} items
|
||||||
* @param {IAccountTransaction[]} itemsTransactions
|
* @param {IAccountTransaction[]} itemsTransactions
|
||||||
* @param {string} baseCurrency
|
* @param {string} baseCurrency
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
query: IInventoryValuationReportQuery,
|
query: IPurchasesByItemsReportQuery,
|
||||||
items: IItem[],
|
items: IItem[],
|
||||||
itemsTransactions: IAccountTransaction[],
|
itemsTransactions: IAccountTransaction[],
|
||||||
baseCurrency: string
|
baseCurrency: string
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.baseCurrency = baseCurrency;
|
this.baseCurrency = baseCurrency;
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
|
this.itemsTransactions = transformToMap(itemsTransactions, 'itemId');
|
||||||
@@ -98,7 +96,7 @@ export default class InventoryValuationReport extends FinancialSheet {
|
|||||||
* @param {IInventoryValuationItem} item
|
* @param {IInventoryValuationItem} item
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private itemSectionMapper = (item: IItem): IInventoryValuationItem => {
|
private itemSectionMapper = (item: IItem): IPurchasesByItemsItem => {
|
||||||
const meta = this.getItemTransaction(item.id);
|
const meta = this.getItemTransaction(item.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -145,9 +143,9 @@ export default class InventoryValuationReport extends FinancialSheet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the items sections.
|
* Retrieve the items sections.
|
||||||
* @returns {IInventoryValuationItem[]}
|
* @returns {IPurchasesByItemsItem[]}
|
||||||
*/
|
*/
|
||||||
private itemsSection = (): IInventoryValuationItem[] => {
|
private itemsSection = (): IPurchasesByItemsItem[] => {
|
||||||
return R.compose(
|
return R.compose(
|
||||||
R.when(this.isItemsPostFilter, this.itemsFilter),
|
R.when(this.isItemsPostFilter, this.itemsFilter),
|
||||||
this.itemsMapper
|
this.itemsMapper
|
||||||
@@ -156,10 +154,10 @@ export default class InventoryValuationReport extends FinancialSheet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the total section of the sheet.
|
* Retrieve the total section of the sheet.
|
||||||
* @param {IInventoryValuationItem[]} items
|
* @param {IPurchasesByItemsItem[]} items
|
||||||
* @returns {IInventoryValuationTotal}
|
* @returns {IPurchasesByItemsTotal}
|
||||||
*/
|
*/
|
||||||
totalSection(items: IInventoryValuationItem[]): IInventoryValuationTotal {
|
private totalSection(items: IPurchasesByItemsItem[]): IPurchasesByItemsTotal {
|
||||||
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
|
const quantityPurchased = sumBy(items, (item) => item.quantityPurchased);
|
||||||
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
|
const purchaseCost = sumBy(items, (item) => item.purchaseCost);
|
||||||
|
|
||||||
@@ -176,12 +174,12 @@ export default class InventoryValuationReport extends FinancialSheet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the sheet data.
|
* Retrieve the sheet data.
|
||||||
* @returns
|
* @returns {IInventoryValuationStatement}
|
||||||
*/
|
*/
|
||||||
reportData(): IInventoryValuationStatement {
|
public reportData(): IPurchasesByItemsSheetData {
|
||||||
const items = this.itemsSection();
|
const items = this.itemsSection();
|
||||||
const total = this.totalSection(items);
|
const total = this.totalSection(items);
|
||||||
|
|
||||||
return items.length > 0 ? { items, total } : {};
|
return { items, total };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import { PurchasesByItemsExport } from './PurchasesByItemsExport';
|
||||||
|
import {
|
||||||
|
IPurchasesByItemsReportQuery,
|
||||||
|
IPurchasesByItemsSheet,
|
||||||
|
IPurchasesByItemsTable,
|
||||||
|
} from '@/interfaces/PurchasesByItemsSheet';
|
||||||
|
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
|
||||||
|
import { PurchasesByItemsService } from './PurchasesByItemsService';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class PurcahsesByItemsApplication {
|
||||||
|
@Inject()
|
||||||
|
private purchasesByItemsSheet: PurchasesByItemsService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private purchasesByItemsExport: PurchasesByItemsExport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items in json format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public sheet(
|
||||||
|
tenantId: number,
|
||||||
|
query: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<IPurchasesByItemsSheet> {
|
||||||
|
return this.purchasesByItemsSheet.purchasesByItems(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items in table format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
|
* @returns {Promise<IPurchasesByItemsTable>}
|
||||||
|
*/
|
||||||
|
public table(
|
||||||
|
tenantId: number,
|
||||||
|
query: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<IPurchasesByItemsTable> {
|
||||||
|
return this.purchasesByItemsTable.table(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items in csv format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
public csv(
|
||||||
|
tenantId: number,
|
||||||
|
query: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<string> {
|
||||||
|
return this.purchasesByItemsExport.csv(tenantId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items in xlsx format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public xlsx(
|
||||||
|
tenantId: number,
|
||||||
|
query: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<Buffer> {
|
||||||
|
return this.purchasesByItemsExport.xlsx(tenantId, query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { TableSheet } from '@/lib/Xlsx/TableSheet';
|
||||||
|
import { PurchasesByItemsTableInjectable } from './PurchasesByItemsTableInjectable';
|
||||||
|
import { IPurchasesByItemsReportQuery } from '@/interfaces/PurchasesByItemsSheet';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class PurchasesByItemsExport {
|
||||||
|
@Inject()
|
||||||
|
private purchasesByItemsTable: PurchasesByItemsTableInjectable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items sheet in XLSX format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async xlsx(
|
||||||
|
tenantId: number,
|
||||||
|
query: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<Buffer> {
|
||||||
|
const table = await this.purchasesByItemsTable.table(tenantId, query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToXLSX();
|
||||||
|
|
||||||
|
return tableSheet.convertToBuffer(tableCsv, 'xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items sheet in CSV format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
|
* @returns {Promise<Buffer>}
|
||||||
|
*/
|
||||||
|
public async csv(
|
||||||
|
tenantId: number,
|
||||||
|
query: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<string> {
|
||||||
|
const table = await this.purchasesByItemsTable.table(tenantId, query);
|
||||||
|
|
||||||
|
const tableSheet = new TableSheet(table.table);
|
||||||
|
const tableCsv = tableSheet.convertToCSV();
|
||||||
|
|
||||||
|
return tableCsv;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {
|
import { Service, Inject } from 'typedi';
|
||||||
IInventoryValuationReportQuery,
|
|
||||||
IInventoryValuationStatement,
|
|
||||||
IInventoryValuationSheetMeta,
|
|
||||||
} from '@/interfaces';
|
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import PurchasesByItems from './PurchasesByItems';
|
import { PurchasesByItems } from './PurchasesByItems';
|
||||||
import { Tenant } from '@/system/models';
|
import { Tenant } from '@/system/models';
|
||||||
|
import {
|
||||||
|
IPurchasesByItemsReportQuery,
|
||||||
|
IPurchasesByItemsSheet,
|
||||||
|
IPurchasesByItemsSheetMeta,
|
||||||
|
} from '@/interfaces/PurchasesByItemsSheet';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class InventoryValuationReportService {
|
export class PurchasesByItemsService {
|
||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: TenancyService;
|
private tenancy: TenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defaults balance sheet filter query.
|
* Defaults purchases by items filter query.
|
||||||
* @return {IBalanceSheetQuery}
|
* @return {IPurchasesByItemsReportQuery}
|
||||||
*/
|
*/
|
||||||
get defaultQuery(): IInventoryValuationReportQuery {
|
get defaultQuery(): IPurchasesByItemsReportQuery {
|
||||||
return {
|
return {
|
||||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||||
toDate: moment().format('YYYY-MM-DD'),
|
toDate: moment().format('YYYY-MM-DD'),
|
||||||
@@ -40,7 +40,7 @@ export default class InventoryValuationReportService {
|
|||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @returns {IBalanceSheetMeta}
|
* @returns {IBalanceSheetMeta}
|
||||||
*/
|
*/
|
||||||
reportMetadata(tenantId: number): IInventoryValuationSheetMeta {
|
reportMetadata(tenantId: number): IPurchasesByItemsSheetMeta {
|
||||||
const settings = this.tenancy.settings(tenantId);
|
const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
const organizationName = settings.get({
|
const organizationName = settings.get({
|
||||||
@@ -62,18 +62,13 @@ export default class InventoryValuationReportService {
|
|||||||
* Retrieve balance sheet statement.
|
* Retrieve balance sheet statement.
|
||||||
* -------------
|
* -------------
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {IBalanceSheetQuery} query
|
* @param {IPurchasesByItemsReportQuery} query
|
||||||
*
|
* @return {Promise<IPurchasesByItemsSheet>}
|
||||||
* @return {IBalanceSheetStatement}
|
|
||||||
*/
|
*/
|
||||||
public async purchasesByItems(
|
public async purchasesByItems(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
query: IInventoryValuationReportQuery
|
query: IPurchasesByItemsReportQuery
|
||||||
): Promise<{
|
): Promise<IPurchasesByItemsSheet> {
|
||||||
data: IInventoryValuationStatement;
|
|
||||||
query: IInventoryValuationReportQuery;
|
|
||||||
meta: IInventoryValuationSheetMeta;
|
|
||||||
}> {
|
|
||||||
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const tenant = await Tenant.query()
|
const tenant = await Tenant.query()
|
||||||
@@ -106,7 +101,6 @@ export default class InventoryValuationReportService {
|
|||||||
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
|
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const purchasesByItemsInstance = new PurchasesByItems(
|
const purchasesByItemsInstance = new PurchasesByItems(
|
||||||
filter,
|
filter,
|
||||||
inventoryItems,
|
inventoryItems,
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { ITableColumn, ITableColumnAccessor, ITableRow } from '@/interfaces';
|
||||||
|
import { ROW_TYPE } from './_types';
|
||||||
|
import { tableRowMapper } from '@/utils';
|
||||||
|
import { FinancialTable } from '../FinancialTable';
|
||||||
|
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||||
|
import FinancialSheet from '../FinancialSheet';
|
||||||
|
import {
|
||||||
|
IPurchasesByItemsItem,
|
||||||
|
IPurchasesByItemsSheetData,
|
||||||
|
IPurchasesByItemsTotal,
|
||||||
|
} from '@/interfaces/PurchasesByItemsSheet';
|
||||||
|
|
||||||
|
export class PurchasesByItemsTable extends R.compose(
|
||||||
|
FinancialTable,
|
||||||
|
FinancialSheetStructure
|
||||||
|
)(FinancialSheet) {
|
||||||
|
private data: IPurchasesByItemsSheetData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor method.
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
constructor(data) {
|
||||||
|
super();
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves thge common table accessors.
|
||||||
|
* @returns {ITableColumnAccessor[]}
|
||||||
|
*/
|
||||||
|
private commonTableAccessors(): ITableColumnAccessor[] {
|
||||||
|
return [
|
||||||
|
{ key: 'item_name', accessor: 'name' },
|
||||||
|
{ key: 'quantity_purchases', accessor: 'quantityPurchasedFormatted' },
|
||||||
|
{ key: 'purchase_amount', accessor: 'purchaseCostFormatted' },
|
||||||
|
{ key: 'average_cost', accessor: 'averageCostPriceFormatted' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the common table columns.
|
||||||
|
* @returns {ITableColumn[]}
|
||||||
|
*/
|
||||||
|
private commonTableColumns(): ITableColumn[] {
|
||||||
|
return [
|
||||||
|
{ label: 'Item name', key: 'item_name' },
|
||||||
|
{ label: 'Quantity Purchased', key: 'quantity_purchases' },
|
||||||
|
{ label: 'Purchase Amount', key: 'purchase_amount' },
|
||||||
|
{ label: 'Average Price', key: 'average_cost' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given item node to table row.
|
||||||
|
* @param {IPurchasesByItemsItem} item
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private itemMap = (item: IPurchasesByItemsItem): ITableRow => {
|
||||||
|
const columns = this.commonTableAccessors();
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.ITEM],
|
||||||
|
};
|
||||||
|
return tableRowMapper(item, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given items nodes to table rows.
|
||||||
|
* @param {IPurchasesByItemsItem[]} items - Items nodes.
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
private itemsMap = (items: IPurchasesByItemsItem[]): ITableRow[] => {
|
||||||
|
return R.map(this.itemMap)(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given total node to table rows.
|
||||||
|
* @param {IPurchasesByItemsTotal} total
|
||||||
|
* @returns {ITableRow}
|
||||||
|
*/
|
||||||
|
private totalNodeMap = (total: IPurchasesByItemsTotal): ITableRow => {
|
||||||
|
const columns = this.commonTableAccessors();
|
||||||
|
const meta = {
|
||||||
|
rowTypes: [ROW_TYPE.TOTAL],
|
||||||
|
};
|
||||||
|
return tableRowMapper(total, columns, meta);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the table columns.
|
||||||
|
* @returns {ITableColumn[]}
|
||||||
|
*/
|
||||||
|
public tableColumns(): ITableColumn[] {
|
||||||
|
const columns = this.commonTableColumns();
|
||||||
|
return R.compose(this.tableColumnsCellIndexing)(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the table rows.
|
||||||
|
* @returns {ITableRow[]}
|
||||||
|
*/
|
||||||
|
public tableData(): ITableRow[] {
|
||||||
|
const itemsRows = this.itemsMap(this.data.items);
|
||||||
|
const totalRow = this.totalNodeMap(this.data.total);
|
||||||
|
|
||||||
|
return R.compose(
|
||||||
|
R.when(R.always(R.not(R.isEmpty(itemsRows))), R.append(totalRow))
|
||||||
|
)(itemsRows) as ITableRow[];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
IPurchasesByItemsReportQuery,
|
||||||
|
IPurchasesByItemsTable,
|
||||||
|
} from '@/interfaces/PurchasesByItemsSheet';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { PurchasesByItemsService } from './PurchasesByItemsService';
|
||||||
|
import { PurchasesByItemsTable } from './PurchasesByItemsTable';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class PurchasesByItemsTableInjectable {
|
||||||
|
@Inject()
|
||||||
|
private purchasesByItemsSheet: PurchasesByItemsService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items table format.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {IPurchasesByItemsReportQuery} filter
|
||||||
|
* @returns {Promise<IPurchasesByItemsTable>}
|
||||||
|
*/
|
||||||
|
public async table(
|
||||||
|
tenantId: number,
|
||||||
|
filter: IPurchasesByItemsReportQuery
|
||||||
|
): Promise<IPurchasesByItemsTable> {
|
||||||
|
const { data, query, meta } =
|
||||||
|
await this.purchasesByItemsSheet.purchasesByItems(tenantId, filter);
|
||||||
|
|
||||||
|
const table = new PurchasesByItemsTable(data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
table: {
|
||||||
|
columns: table.tableColumns(),
|
||||||
|
rows: table.tableData(),
|
||||||
|
},
|
||||||
|
meta,
|
||||||
|
query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
export enum ROW_TYPE {
|
||||||
|
TOTAL = 'TOTAL',
|
||||||
|
ITEM = 'ITEM'
|
||||||
|
}
|
||||||
@@ -252,10 +252,6 @@ export default class TrialBalanceSheet extends FinancialSheet {
|
|||||||
* @return {ITrialBalanceSheetData}
|
* @return {ITrialBalanceSheetData}
|
||||||
*/
|
*/
|
||||||
public reportData(): ITrialBalanceSheetData {
|
public reportData(): ITrialBalanceSheetData {
|
||||||
// Don't return noting if the journal has no transactions.
|
|
||||||
if (this.repository.totalAccountsLedger.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Retrieve accounts nodes.
|
// Retrieve accounts nodes.
|
||||||
const accounts = this.accountsSection(this.repository.accounts);
|
const accounts = this.accountsSection(this.repository.accounts);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class TrialBalanceSheetTable extends R.compose(
|
|||||||
this.query = query;
|
this.query = query;
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the common columns for all report nodes.
|
* Retrieve the common columns for all report nodes.
|
||||||
* @param {ITableColumnAccessor[]}
|
* @param {ITableColumnAccessor[]}
|
||||||
@@ -123,7 +123,7 @@ export class TrialBalanceSheetTable extends R.compose(
|
|||||||
*/
|
*/
|
||||||
public tableRows = (): ITableRow[] => {
|
public tableRows = (): ITableRow[] => {
|
||||||
return R.compose(
|
return R.compose(
|
||||||
R.append(this.totalTableRow()),
|
R.unless(R.isEmpty, R.append(this.totalTableRow())),
|
||||||
R.concat(this.accountsTableRows())
|
R.concat(this.accountsTableRows())
|
||||||
)([]);
|
)([]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||||
import { BillPayment } from '@/models';
|
|
||||||
import { ERRORS } from './constants';
|
import { ERRORS } from './constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -18,19 +17,6 @@ export class BillPaymentValidators {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private tenancy: TenancyService;
|
private tenancy: TenancyService;
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the payment existance.
|
|
||||||
* @param {BillPayment | undefined | null} payment
|
|
||||||
* @throws {ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND)}
|
|
||||||
*/
|
|
||||||
public async validateBillPaymentExistance(
|
|
||||||
payment: BillPayment | undefined | null
|
|
||||||
) {
|
|
||||||
if (!payment) {
|
|
||||||
throw new ServiceError(ERRORS.PAYMENT_MADE_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the bill payment existance.
|
* Validates the bill payment existance.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import UnitOfWork from '@/services/UnitOfWork';
|
import UnitOfWork from '@/services/UnitOfWork';
|
||||||
import { BillPaymentValidators } from './BillPaymentValidators';
|
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
@@ -21,9 +20,6 @@ export class DeleteBillPayment {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private uow: UnitOfWork;
|
private uow: UnitOfWork;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private validators: BillPaymentValidators;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the bill payment and associated transactions.
|
* Deletes the bill payment and associated transactions.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
@@ -36,10 +32,8 @@ export class DeleteBillPayment {
|
|||||||
// Retrieve the bill payment or throw not found service error.
|
// Retrieve the bill payment or throw not found service error.
|
||||||
const oldBillPayment = await BillPayment.query()
|
const oldBillPayment = await BillPayment.query()
|
||||||
.withGraphFetched('entries')
|
.withGraphFetched('entries')
|
||||||
.findById(billPaymentId);
|
.findById(billPaymentId)
|
||||||
|
.throwIfNotFound();
|
||||||
// Validates the bill payment existance.
|
|
||||||
this.validators.validateBillPaymentExistance(oldBillPayment);
|
|
||||||
|
|
||||||
// Deletes the bill transactions with associated transactions under
|
// Deletes the bill transactions with associated transactions under
|
||||||
// unit-of-work envirement.
|
// unit-of-work envirement.
|
||||||
|
|||||||
@@ -57,12 +57,12 @@ export class EditBillPayment {
|
|||||||
|
|
||||||
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
const oldBillPayment = await BillPayment.query().findById(billPaymentId);
|
const oldBillPayment = await BillPayment.query()
|
||||||
|
.findById(billPaymentId)
|
||||||
|
.withGraphFetched('entries')
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validates the bill payment existance.
|
// Retrieves the bill payment vendor or throw not found error.
|
||||||
this.validators.validateBillPaymentExistance(oldBillPayment);
|
|
||||||
|
|
||||||
//
|
|
||||||
const vendor = await Contact.query()
|
const vendor = await Contact.query()
|
||||||
.modify('vendor')
|
.modify('vendor')
|
||||||
.findById(billPaymentDTO.vendorId)
|
.findById(billPaymentDTO.vendorId)
|
||||||
@@ -126,7 +126,7 @@ export class EditBillPayment {
|
|||||||
trx,
|
trx,
|
||||||
} as IBillPaymentEditingPayload);
|
} as IBillPaymentEditingPayload);
|
||||||
|
|
||||||
// Deletes the bill payment transaction graph from the storage.
|
// Edits the bill payment transaction graph on the storage.
|
||||||
const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({
|
const billPayment = await BillPayment.query(trx).upsertGraphAndFetch({
|
||||||
id: billPaymentId,
|
id: billPaymentId,
|
||||||
...billPaymentObj,
|
...billPaymentObj,
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { IBillPayment } from '@/interfaces';
|
import { IBillPayment } from '@/interfaces';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { ERRORS } from './constants';
|
|
||||||
import { ServiceError } from '@/exceptions';
|
|
||||||
import { BillPaymentTransformer } from './BillPaymentTransformer';
|
import { BillPaymentTransformer } from './BillPaymentTransformer';
|
||||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
import { BillsValidators } from '../Bills/BillsValidators';
|
|
||||||
import { BillPaymentValidators } from './BillPaymentValidators';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class GetBillPayment {
|
export class GetBillPayment {
|
||||||
@@ -16,9 +12,6 @@ export class GetBillPayment {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private transformer: TransformerInjectable;
|
private transformer: TransformerInjectable;
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private validators: BillPaymentValidators;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve bill payment.
|
* Retrieve bill payment.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
@@ -37,10 +30,8 @@ export class GetBillPayment {
|
|||||||
.withGraphFetched('paymentAccount')
|
.withGraphFetched('paymentAccount')
|
||||||
.withGraphFetched('transactions')
|
.withGraphFetched('transactions')
|
||||||
.withGraphFetched('branch')
|
.withGraphFetched('branch')
|
||||||
.findById(billPyamentId);
|
.findById(billPyamentId)
|
||||||
|
.throwIfNotFound();
|
||||||
// Validates the bill payment existance.
|
|
||||||
this.validators.validateBillPaymentExistance(billPayment);
|
|
||||||
|
|
||||||
return this.transformer.transform(
|
return this.transformer.transform(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ export class GetPaymentBills {
|
|||||||
public async getPaymentBills(tenantId: number, billPaymentId: number) {
|
public async getPaymentBills(tenantId: number, billPaymentId: number) {
|
||||||
const { Bill, BillPayment } = this.tenancy.models(tenantId);
|
const { Bill, BillPayment } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const billPayment = await BillPayment.query().findById(billPaymentId);
|
const billPayment = await BillPayment.query()
|
||||||
|
.findById(billPaymentId)
|
||||||
// Validates the bill payment existance.
|
.throwIfNotFound();
|
||||||
this.validators.validateBillPaymentExistance(billPayment);
|
|
||||||
|
|
||||||
const paymentBillsIds = billPayment.entries.map((entry) => entry.id);
|
const paymentBillsIds = billPayment.entries.map((entry) => entry.id);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,20 @@ export class BillsValidators {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the bill amount is bigger than paid amount.
|
||||||
|
* @param {number} billAmount
|
||||||
|
* @param {number} paidAmount
|
||||||
|
*/
|
||||||
|
public validateBillAmountBiggerPaidAmount(
|
||||||
|
billAmount: number,
|
||||||
|
paidAmount: number,
|
||||||
|
) {
|
||||||
|
if (billAmount < paidAmount) {
|
||||||
|
throw new ServiceError(ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the bill number existance.
|
* Validates the bill number existance.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export class EditBill {
|
|||||||
tenantId,
|
tenantId,
|
||||||
billDTO.entries
|
billDTO.entries
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transforms the bill DTO to model object.
|
// Transforms the bill DTO to model object.
|
||||||
const billObj = await this.transformerDTO.billDTOToModel(
|
const billObj = await this.transformerDTO.billDTOToModel(
|
||||||
tenantId,
|
tenantId,
|
||||||
@@ -111,6 +112,11 @@ export class EditBill {
|
|||||||
authorizedUser,
|
authorizedUser,
|
||||||
oldBill
|
oldBill
|
||||||
);
|
);
|
||||||
|
// Validate bill total amount should be bigger than paid amount.
|
||||||
|
this.validators.validateBillAmountBiggerPaidAmount(
|
||||||
|
billObj.amount,
|
||||||
|
oldBill.paymentAmount
|
||||||
|
);
|
||||||
// Validate landed cost entries that have allocated cost could not be deleted.
|
// Validate landed cost entries that have allocated cost could not be deleted.
|
||||||
await this.entriesService.validateLandedCostEntriesNotDeleted(
|
await this.entriesService.validateLandedCostEntriesNotDeleted(
|
||||||
oldBill.entries,
|
oldBill.entries,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
|
|||||||
return [
|
return [
|
||||||
'formattedBillDate',
|
'formattedBillDate',
|
||||||
'formattedDueDate',
|
'formattedDueDate',
|
||||||
|
'formattedAmount',
|
||||||
'formattedPaymentAmount',
|
'formattedPaymentAmount',
|
||||||
'formattedBalance',
|
'formattedBalance',
|
||||||
'formattedDueAmount',
|
'formattedDueAmount',
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const ERRORS = {
|
|||||||
LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS:
|
LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS:
|
||||||
'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS',
|
'LANDED_COST_ENTRIES_SHOULD_BE_INVENTORY_ITEMS',
|
||||||
BILL_HAS_APPLIED_TO_VENDOR_CREDIT: 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT',
|
BILL_HAS_APPLIED_TO_VENDOR_CREDIT: 'BILL_HAS_APPLIED_TO_VENDOR_CREDIT',
|
||||||
|
BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_VIEW_COLUMNS = [];
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import UnitOfWork from '@/services/UnitOfWork';
|
|||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EditVendorCredit extends BaseVendorCredit {
|
export default class EditVendorCredit extends BaseVendorCredit {
|
||||||
@@ -21,6 +22,9 @@ export default class EditVendorCredit extends BaseVendorCredit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private itemsEntriesService: ItemsEntriesService;
|
private itemsEntriesService: ItemsEntriesService;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private tenancy: HasTenancyService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the given vendor credit.
|
* Deletes the given vendor credit.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {number} tenantId - Tenant id.
|
||||||
@@ -31,7 +35,7 @@ export default class EditVendorCredit extends BaseVendorCredit {
|
|||||||
vendorCreditId: number,
|
vendorCreditId: number,
|
||||||
vendorCreditDTO: IVendorCreditEditDTO
|
vendorCreditDTO: IVendorCreditEditDTO
|
||||||
) => {
|
) => {
|
||||||
const { VendorCredit } = this.tenancy.models(tenantId);
|
const { VendorCredit, Contact } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Retrieve the vendor credit or throw not found service error.
|
// Retrieve the vendor credit or throw not found service error.
|
||||||
const oldVendorCredit = await this.getVendorCreditOrThrowError(
|
const oldVendorCredit = await this.getVendorCreditOrThrowError(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export class VendorCreditTransformer extends Transformer {
|
|||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [
|
return [
|
||||||
'formattedAmount',
|
'formattedAmount',
|
||||||
|
'formattedSubtotal',
|
||||||
'formattedVendorCreditDate',
|
'formattedVendorCreditDate',
|
||||||
'formattedCreditsRemaining',
|
'formattedCreditsRemaining',
|
||||||
'entries',
|
'entries',
|
||||||
@@ -37,6 +38,15 @@ export class VendorCreditTransformer extends Transformer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the vendor credit formatted subtotal.
|
||||||
|
* @param {IVendorCredit} vendorCredit
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedSubtotal = (vendorCredit): string => {
|
||||||
|
return formatNumber(vendorCredit.amount, { money: false });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted credits remaining.
|
* Retrieve formatted credits remaining.
|
||||||
* @param {IVendorCredit} credit
|
* @param {IVendorCredit} credit
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class SaleEstimateTransfromer extends Transformer {
|
|||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [
|
return [
|
||||||
|
'formattedSubtotal',
|
||||||
'formattedAmount',
|
'formattedAmount',
|
||||||
'formattedEstimateDate',
|
'formattedEstimateDate',
|
||||||
'formattedExpirationDate',
|
'formattedExpirationDate',
|
||||||
@@ -76,6 +77,15 @@ export class SaleEstimateTransfromer extends Transformer {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted invoice subtotal.
|
||||||
|
* @param {ISaleEstimate} estimate
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedSubtotal = (estimate: ISaleEstimate): string => {
|
||||||
|
return formatNumber(estimate.amount, { money: false });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the entries of the sale estimate.
|
* Retrieves the entries of the sale estimate.
|
||||||
* @param {ISaleEstimate} estimate
|
* @param {ISaleEstimate} estimate
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ export class ItemEntryTransformer extends Transformer {
|
|||||||
* @returns {Array}
|
* @returns {Array}
|
||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return ['rateFormatted', 'totalFormatted'];
|
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted quantitty of item entry.
|
||||||
|
* @param {IItemEntry} entry
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected quantityFormatted = (entry: IItemEntry): string => {
|
||||||
|
return formatNumber(entry.quantity, { money: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ export class EditPaymentReceive {
|
|||||||
// Validate the payment receive existance.
|
// Validate the payment receive existance.
|
||||||
const oldPaymentReceive = await PaymentReceive.query()
|
const oldPaymentReceive = await PaymentReceive.query()
|
||||||
.withGraphFetched('entries')
|
.withGraphFetched('entries')
|
||||||
.findById(paymentReceiveId);
|
.findById(paymentReceiveId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
// Validates the payment existance.
|
// Validates the payment existance.
|
||||||
this.validators.validatePaymentExistance(oldPaymentReceive);
|
this.validators.validatePaymentExistance(oldPaymentReceive);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export class PaymentReceiveTransfromer extends Transformer {
|
|||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [
|
return [
|
||||||
|
'subtotalFormatted',
|
||||||
'formattedPaymentDate',
|
'formattedPaymentDate',
|
||||||
'formattedAmount',
|
'formattedAmount',
|
||||||
'formattedExchangeRate',
|
'formattedExchangeRate',
|
||||||
@@ -26,6 +27,18 @@ export class PaymentReceiveTransfromer extends Transformer {
|
|||||||
return this.formatDate(payment.paymentDate);
|
return this.formatDate(payment.paymentDate);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the formatted payment subtotal.
|
||||||
|
* @param {IPaymentReceive} payment
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected subtotalFormatted = (payment: IPaymentReceive): string => {
|
||||||
|
return formatNumber(payment.amount, {
|
||||||
|
currencyCode: payment.currencyCode,
|
||||||
|
money: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted payment amount.
|
* Retrieve formatted payment amount.
|
||||||
* @param {ISaleInvoice} invoice
|
* @param {ISaleInvoice} invoice
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class SaleReceiptGLEntries {
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const { SaleReceipt } = this.tenancy.models(tenantId);
|
const { SaleReceipt } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const saleReceipt = await SaleReceipt.query()
|
const saleReceipt = await SaleReceipt.query(trx)
|
||||||
.findById(saleReceiptId)
|
.findById(saleReceiptId)
|
||||||
.withGraphFetched('entries.item');
|
.withGraphFetched('entries.item');
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export class SaleReceiptTransformer extends Transformer {
|
|||||||
*/
|
*/
|
||||||
public includeAttributes = (): string[] => {
|
public includeAttributes = (): string[] => {
|
||||||
return [
|
return [
|
||||||
|
'formattedSubtotal',
|
||||||
'formattedAmount',
|
'formattedAmount',
|
||||||
'formattedReceiptDate',
|
'formattedReceiptDate',
|
||||||
'formattedClosedAtDate',
|
'formattedClosedAtDate',
|
||||||
@@ -37,6 +38,15 @@ export class SaleReceiptTransformer extends Transformer {
|
|||||||
return this.formatDate(receipt.closedAt);
|
return this.formatDate(receipt.closedAt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the estimate formatted subtotal.
|
||||||
|
* @param {ISaleReceipt} receipt
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedSubtotal = (receipt: ISaleReceipt): string => {
|
||||||
|
return formatNumber(receipt.amount, { money: false });
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve formatted invoice amount.
|
* Retrieve formatted invoice amount.
|
||||||
* @param {ISaleReceipt} estimate
|
* @param {ISaleReceipt} estimate
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import BaseModel from 'models/Model';
|
import BaseModel from 'models/Model';
|
||||||
|
|
||||||
export default class TenantMetadata extends BaseModel {
|
export default class TenantMetadata extends BaseModel {
|
||||||
|
baseCurrency: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function DetailItem({ label, children, name, align, className }) {
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item__content">{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export const ERROR = {
|
|||||||
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
|
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
|
||||||
SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE:
|
SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE:
|
||||||
'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE',
|
'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE',
|
||||||
|
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
|
||||||
|
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
|
||||||
|
|
||||||
// Sales Receipts
|
// Sales Receipts
|
||||||
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
|
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
|
||||||
@@ -17,6 +19,6 @@ export const ERROR = {
|
|||||||
// Bills
|
// Bills
|
||||||
BILL_NUMBER_EXISTS: 'BILL.NUMBER.EXISTS',
|
BILL_NUMBER_EXISTS: 'BILL.NUMBER.EXISTS',
|
||||||
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
|
SALE_INVOICE_NO_IS_REQUIRED: 'SALE_INVOICE_NO_IS_REQUIRED',
|
||||||
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:"ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED",
|
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:
|
||||||
|
'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ function LoginFooterLinks() {
|
|||||||
<AuthFooterLinks>
|
<AuthFooterLinks>
|
||||||
{!signupDisabled && (
|
{!signupDisabled && (
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
|
<T id={'dont_have_an_account'} /> <Link to={'/auth/register'}><T id={'sign_up'} /></Link>
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
)}
|
)}
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function RegisterFooterLinks() {
|
|||||||
return (
|
return (
|
||||||
<AuthFooterLinks>
|
<AuthFooterLinks>
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Return to <Link to={'/auth/login'}>Sign In</Link>
|
<T id={'return_to'} /> <Link to={'/auth/login'}><T id={'sign_in'} /></Link>
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
|
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Formik } from 'formik';
|
|||||||
import { Intent, Position } from '@blueprintjs/core';
|
import { Intent, Position } from '@blueprintjs/core';
|
||||||
import { Link, useParams, useHistory } from 'react-router-dom';
|
import { Link, useParams, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||||
import { useAuthResetPassword } from '@/hooks/query';
|
import { useAuthResetPassword } from '@/hooks/query';
|
||||||
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
import AuthInsider from '@/containers/Authentication/AuthInsider';
|
||||||
|
|
||||||
@@ -86,11 +86,11 @@ function ResetPasswordFooterLinks() {
|
|||||||
<AuthFooterLinks>
|
<AuthFooterLinks>
|
||||||
{!signupDisabled && (
|
{!signupDisabled && (
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
|
<T id={'dont_have_an_account'} /> <Link to={'/auth/register'}><T id={'sign_up'} /></Link>
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
)}
|
)}
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Return to <Link to={'/auth/login'}>Sign In</Link>
|
<T id={'return_to'} /> <Link to={'/auth/login'}><T id={'sign_in'} /></Link>
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
</AuthFooterLinks>
|
</AuthFooterLinks>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Formik } from 'formik';
|
|||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
|
|
||||||
import { AppToaster } from '@/components';
|
import { AppToaster, FormattedMessage as T } from '@/components';
|
||||||
import { useAuthSendResetPassword } from '@/hooks/query';
|
import { useAuthSendResetPassword } from '@/hooks/query';
|
||||||
|
|
||||||
import SendResetPasswordForm from './SendResetPasswordForm';
|
import SendResetPasswordForm from './SendResetPasswordForm';
|
||||||
@@ -82,11 +82,11 @@ function SendResetPasswordFooterLinks() {
|
|||||||
<AuthFooterLinks>
|
<AuthFooterLinks>
|
||||||
{!signupDisabled && (
|
{!signupDisabled && (
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Don't have an account? <Link to={'/auth/register'}>Sign up</Link>
|
<T id={'dont_have_an_account'} /> <Link to={'/auth/register'}><T id={'sign_up'} /></Link>
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
)}
|
)}
|
||||||
<AuthFooterLink>
|
<AuthFooterLink>
|
||||||
Return to <Link to={'/auth/login'}>Sign In</Link>
|
<T id={'return_to'} /> <Link to={'/auth/login'}><T id={'sign_in'} /></Link>
|
||||||
</AuthFooterLink>
|
</AuthFooterLink>
|
||||||
</AuthFooterLinks>
|
</AuthFooterLinks>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ export default function SendResetPasswordForm({ isSubmitting }) {
|
|||||||
return (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<TopParagraph>
|
<TopParagraph>
|
||||||
Enter the email address associated with your account and we'll send you
|
<T id={'enter_the_email_address_associated_with_your_account'} />
|
||||||
a link to reset your password.
|
|
||||||
</TopParagraph>
|
</TopParagraph>
|
||||||
|
|
||||||
<FFormGroup name={'crediential'} label={<T id={'email_address'} />}>
|
<FFormGroup name={'crediential'} label={<T id={'email_address'} />}>
|
||||||
@@ -29,7 +28,7 @@ export default function SendResetPasswordForm({ isSubmitting }) {
|
|||||||
large={true}
|
large={true}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
Reset Password
|
<T id={'reset_password'} />
|
||||||
</AuthSubmitButton>
|
</AuthSubmitButton>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
T,
|
T,
|
||||||
TotalLines,
|
TotalLines,
|
||||||
TotalLine,
|
TotalLine,
|
||||||
FormatNumber,
|
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
@@ -23,7 +20,7 @@ export default function CreditNoteDetailTableFooter() {
|
|||||||
<CreditNoteTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<CreditNoteTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'credit_note.drawer.label_subtotal'} />}
|
title={<T id={'credit_note.drawer.label_subtotal'} />}
|
||||||
value={<FormatNumber value={creditNote.formatted_amount} />}
|
value={creditNote.formatted_subtotal}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'credit_note.drawer.label_total'} />}
|
title={<T id={'credit_note.drawer.label_total'} />}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
TotalLine,
|
TotalLine,
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
FormatNumber,
|
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
|
import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ export default function EstimateDetailTableFooter() {
|
|||||||
<EstimateTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<EstimateTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'estimate.details.subtotal'} />}
|
title={<T id={'estimate.details.subtotal'} />}
|
||||||
value={<FormatNumber value={estimate.amount} />}
|
value={estimate.formatted_subtotal}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import { getColumnWidth } from '@/utils';
|
import { getColumnWidth } from '@/utils';
|
||||||
import { FormatNumberCell, TextOverviewTooltipCell } from '@/components';
|
import { TextOverviewTooltipCell } from '@/components';
|
||||||
import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
|
import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,9 +35,8 @@ export const useEstimateReadonlyEntriesColumns = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('quantity'),
|
Header: intl.get('quantity'),
|
||||||
accessor: 'quantity',
|
accessor: 'quantity_formatted',
|
||||||
Cell: FormatNumberCell,
|
width: getColumnWidth(entries, 'quantity_formatted', {
|
||||||
width: getColumnWidth(entries, 'quantity', {
|
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
magicSpacing: 5,
|
magicSpacing: 5,
|
||||||
}),
|
}),
|
||||||
@@ -59,7 +58,6 @@ export const useEstimateReadonlyEntriesColumns = () => {
|
|||||||
{
|
{
|
||||||
Header: intl.get('amount'),
|
Header: intl.get('amount'),
|
||||||
accessor: 'total_formatted',
|
accessor: 'total_formatted',
|
||||||
Cell: FormatNumberCell,
|
|
||||||
width: getColumnWidth(entries, 'total_formatted', {
|
width: getColumnWidth(entries, 'total_formatted', {
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
magicSpacing: 5,
|
magicSpacing: 5,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useExpenseDrawerContext } from './ExpenseDrawerProvider';
|
import { useExpenseDrawerContext } from './ExpenseDrawerProvider';
|
||||||
import { FormatNumber, TotalLine } from '@/components';
|
import { TotalLine } from '@/components';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Footer details of expense readonly details.
|
* Footer details of expense readonly details.
|
||||||
@@ -22,12 +22,12 @@ export default function ExpenseDrawerFooter() {
|
|||||||
<ExpenseTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<ExpenseTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'expense.details.subtotal'} />}
|
title={<T id={'expense.details.subtotal'} />}
|
||||||
value={<FormatNumber value={expense.total_amount} />}
|
value={expense.formatted_amount}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'expense.details.total'} />}
|
title={<T id={'expense.details.total'} />}
|
||||||
value={<FormatNumber value={expense.formatted_amount} />}
|
value={expense.formatted_amount}
|
||||||
borderStyle={TotalLineBorderStyle.DoubleDark}
|
borderStyle={TotalLineBorderStyle.DoubleDark}
|
||||||
textStyle={TotalLineTextStyle.Bold}
|
textStyle={TotalLineTextStyle.Bold}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ export const useExpenseReadEntriesColumns = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('amount'),
|
Header: intl.get('amount'),
|
||||||
accessor: 'amount',
|
accessor: 'amount_formatted',
|
||||||
Cell: FormatNumberCell,
|
|
||||||
width: getColumnWidth(categories, 'amount', {
|
width: getColumnWidth(categories, 'amount', {
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
magicSpacing: 5,
|
magicSpacing: 5,
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import React from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FormatNumber,
|
T,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
T,
|
|
||||||
TotalLine,
|
TotalLine,
|
||||||
TotalLines,
|
TotalLines,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
@@ -27,7 +26,7 @@ export default function PaymentReceiveDetailTableFooter() {
|
|||||||
>
|
>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'payment_receive.details.subtotal'} />}
|
title={<T id={'payment_receive.details.subtotal'} />}
|
||||||
value={<FormatNumber value={paymentReceive.amount} />}
|
value={paymentReceive.subtotal_formatted}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'payment_receive.details.total'} />}
|
title={<T id={'payment_receive.details.total'} />}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Menu,
|
Menu,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import { Icon, FormatNumberCell } from '@/components';
|
import { Icon } from '@/components';
|
||||||
import { getColumnWidth } from '@/utils';
|
import { getColumnWidth } from '@/utils';
|
||||||
import { usePaymentReceiveDetailContext } from './PaymentReceiveDetailProvider';
|
import { usePaymentReceiveDetailContext } from './PaymentReceiveDetailProvider';
|
||||||
|
|
||||||
@@ -40,9 +40,8 @@ export const usePaymentReceiveEntriesColumns = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('invoice_amount'),
|
Header: intl.get('invoice_amount'),
|
||||||
accessor: 'invoice.balance',
|
accessor: 'invoice.total_formatted',
|
||||||
Cell: FormatNumberCell,
|
width: getColumnWidth(entries, 'invoice.total_formatted', {
|
||||||
width: getColumnWidth(entries, 'invoice.balance', {
|
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
magicSpacing: 5,
|
magicSpacing: 5,
|
||||||
}),
|
}),
|
||||||
@@ -51,10 +50,9 @@ export const usePaymentReceiveEntriesColumns = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('amount_due'),
|
Header: intl.get('amount_due'),
|
||||||
accessor: 'invoice.due_amount',
|
accessor: 'invoice.due_amount_formatted',
|
||||||
Cell: FormatNumberCell,
|
|
||||||
align: 'right',
|
align: 'right',
|
||||||
width: getColumnWidth(entries, 'invoice.due_amount', {
|
width: getColumnWidth(entries, 'invoice.due_amount_formatted', {
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
magicSpacing: 5,
|
magicSpacing: 5,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function ReceiptDetailTableFooter() {
|
|||||||
<ReceiptTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<ReceiptTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'receipt.details.subtotal'} />}
|
title={<T id={'receipt.details.subtotal'} />}
|
||||||
value={receipt.formatted_amount}
|
value={receipt.formatted_subtotal}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'receipt.details.total'} />}
|
title={<T id={'receipt.details.total'} />}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export const useJournalEntriesTransactionsColumns = () => {
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
Header: intl.get('date'),
|
Header: intl.get('date'),
|
||||||
accessor: 'date',
|
|
||||||
accessor: 'formatted_date',
|
accessor: 'formatted_date',
|
||||||
Cell: FormatDateCell,
|
Cell: FormatDateCell,
|
||||||
width: 140,
|
width: 140,
|
||||||
@@ -34,15 +33,17 @@ export const useJournalEntriesTransactionsColumns = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('credit'),
|
Header: intl.get('credit'),
|
||||||
accessor: ({ credit }) => credit.formatted_amount,
|
accessor: 'credit.formatted_amount',
|
||||||
width: 100,
|
width: 100,
|
||||||
className: 'credit',
|
className: 'credit',
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('debit'),
|
Header: intl.get('debit'),
|
||||||
accessor: ({ debit }) => debit.formatted_amount,
|
accessor: 'debit.formatted_amount',
|
||||||
width: 100,
|
width: 100,
|
||||||
className: 'debit',
|
className: 'debit',
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
TotalLine,
|
TotalLine,
|
||||||
TotalLineBorderStyle,
|
TotalLineBorderStyle,
|
||||||
TotalLineTextStyle,
|
TotalLineTextStyle,
|
||||||
FormatNumber,
|
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
|
import { useVendorCreditDetailDrawerContext } from './VendorCreditDetailDrawerProvider';
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ export default function VendorCreditDetailDrawerFooter() {
|
|||||||
<VendorCreditTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
<VendorCreditTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
title={<T id={'vendor_credit.drawer.label_subtotal'} />}
|
title={<T id={'vendor_credit.drawer.label_subtotal'} />}
|
||||||
value={vendorCredit.formatted_amount}
|
value={vendorCredit.formatted_subtotal}
|
||||||
borderStyle={TotalLineBorderStyle.SingleDark}
|
borderStyle={TotalLineBorderStyle.SingleDark}
|
||||||
/>
|
/>
|
||||||
<TotalLine
|
<TotalLine
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ export const useVendorCreditReadonlyEntriesTableColumns = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Header: intl.get('quantity'),
|
Header: intl.get('quantity'),
|
||||||
accessor: 'quantity',
|
accessor: 'quantity_formatted',
|
||||||
Cell: FormatNumberCell,
|
width: getColumnWidth(entries, 'quantity_formatted', {
|
||||||
width: getColumnWidth(entries, 'quantity', {
|
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
magicSpacing: 5,
|
magicSpacing: 5,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useExchangeRate } from '@/hooks/query';
|
|
||||||
import { useCurrentOrganization } from '@/hooks/state';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLatestExchangeRate } from '@/hooks/query';
|
||||||
|
|
||||||
interface AutoExchangeRateProviderProps {
|
interface AutoExchangeRateProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -18,16 +17,19 @@ const AutoExchangeRateContext = React.createContext(
|
|||||||
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
|
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
|
||||||
const [autoExRateCurrency, setAutoExRateCurrency] =
|
const [autoExRateCurrency, setAutoExRateCurrency] =
|
||||||
React.useState<string>('');
|
React.useState<string>('');
|
||||||
const currentOrganization = useCurrentOrganization();
|
|
||||||
|
|
||||||
// Retrieves the exchange rate.
|
// Retrieves the exchange rate.
|
||||||
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
|
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
|
||||||
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
|
useLatestExchangeRate(
|
||||||
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
|
{ fromCurrency: autoExRateCurrency },
|
||||||
refetchOnWindowFocus: false,
|
{
|
||||||
staleTime: 0,
|
enabled: Boolean(autoExRateCurrency),
|
||||||
cacheTime: 0,
|
refetchOnWindowFocus: false,
|
||||||
});
|
staleTime: 0,
|
||||||
|
cacheTime: 0,
|
||||||
|
retry: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
autoExRateCurrency,
|
autoExRateCurrency,
|
||||||
|
|||||||
@@ -98,24 +98,30 @@ interface UseSyncExRateToFormProps {
|
|||||||
*/
|
*/
|
||||||
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
|
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
|
||||||
const { setFieldValue, values } = useFormikContext();
|
const { setFieldValue, values } = useFormikContext();
|
||||||
const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext();
|
const { autoExRateCurrency, autoExchangeRate, isAutoExchangeRateLoading } =
|
||||||
|
useAutoExRateContext();
|
||||||
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
|
||||||
|
|
||||||
// Sync the fetched real-time exchanage rate to the form.
|
// Sync the fetched real-time exchanage rate to the form.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
|
if (!isAutoExchangeRateLoading && autoExRateCurrency) {
|
||||||
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
|
// Sets a default ex. rate to 1 in case the exchange rate service wasn't configured.
|
||||||
|
// or returned an error from the server-side.
|
||||||
|
const exchangeRate = autoExchangeRate?.exchange_rate || 1;
|
||||||
|
|
||||||
|
setFieldValue('exchange_rate', exchangeRate + '');
|
||||||
setFieldValue(
|
setFieldValue(
|
||||||
'entries',
|
'entries',
|
||||||
updateEntriesOnExChange(
|
updateEntriesOnExChange(values.exchange_rate, exchangeRate),
|
||||||
values.exchange_rate,
|
|
||||||
autoExchangeRate?.exchange_rate,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
onSynced?.();
|
onSynced?.();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
|
}, [
|
||||||
|
autoExchangeRate?.exchange_rate,
|
||||||
|
autoExRateCurrency,
|
||||||
|
isAutoExchangeRateLoading,
|
||||||
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import withPurchasesByItems from './withPurchasesByItems';
|
|||||||
import withPurchasesByItemsActions from './withPurchasesByItemsActions';
|
import withPurchasesByItemsActions from './withPurchasesByItemsActions';
|
||||||
import { compose, saveInvoke } from '@/utils';
|
import { compose, saveInvoke } from '@/utils';
|
||||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||||
|
import { PurchasesByItemsExportMenu } from './components';
|
||||||
|
|
||||||
function PurchasesByItemsActionsBar({
|
function PurchasesByItemsActionsBar({
|
||||||
// #withPurchasesByItems
|
// #withPurchasesByItems
|
||||||
@@ -106,11 +107,18 @@ function PurchasesByItemsActionsBar({
|
|||||||
icon={<Icon icon="print-16" iconSize={16} />}
|
icon={<Icon icon="print-16" iconSize={16} />}
|
||||||
text={<T id={'print'} />}
|
text={<T id={'print'} />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Popover
|
||||||
className={Classes.MINIMAL}
|
content={<PurchasesByItemsExportMenu />}
|
||||||
icon={<Icon icon="file-export-16" iconSize={16} />}
|
interactionKind={PopoverInteractionKind.CLICK}
|
||||||
text={<T id={'export'} />}
|
placement="bottom-start"
|
||||||
/>
|
minimal
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
className={Classes.MINIMAL}
|
||||||
|
icon={<Icon icon="file-export-16" iconSize={16} />}
|
||||||
|
text={<T id={'export'} />}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
</NavbarGroup>
|
</NavbarGroup>
|
||||||
</DashboardActionsBar>
|
</DashboardActionsBar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import FinancialReportPage from '../FinancialReportPage';
|
import FinancialReportPage from '../FinancialReportPage';
|
||||||
import { usePurchasesByItems } from '@/hooks/query';
|
import { usePurchasesByItemsTable } from '@/hooks/query';
|
||||||
import { transformFilterFormToQuery } from '../common';
|
import { transformFilterFormToQuery } from '../common';
|
||||||
|
|
||||||
const PurchasesByItemsContext = createContext();
|
const PurchasesByItemsContext = createContext();
|
||||||
@@ -13,7 +13,7 @@ function PurchasesByItemsProvider({ query, ...props }) {
|
|||||||
isFetching,
|
isFetching,
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
} = usePurchasesByItems(
|
} = usePurchasesByItemsTable(
|
||||||
{
|
{
|
||||||
...transformFilterFormToQuery(query),
|
...transformFilterFormToQuery(query),
|
||||||
},
|
},
|
||||||
@@ -26,7 +26,6 @@ function PurchasesByItemsProvider({ query, ...props }) {
|
|||||||
purchaseByItems,
|
purchaseByItems,
|
||||||
isFetching,
|
isFetching,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
||||||
refetchSheet: refetch,
|
refetchSheet: refetch,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import styled from 'styled-components';
|
|||||||
import { ReportDataTable, FinancialSheet } from '@/components';
|
import { ReportDataTable, FinancialSheet } from '@/components';
|
||||||
|
|
||||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||||
import { usePurchasesByItemsTableColumns } from './components';
|
|
||||||
|
|
||||||
import { tableRowTypesToClassnames } from '@/utils';
|
import { tableRowTypesToClassnames } from '@/utils';
|
||||||
import { TableStyle } from '@/constants';
|
import { TableStyle } from '@/constants';
|
||||||
|
import { usePurchasesByItemsTableColumns } from './dynamicColumns';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purchases by items data table.
|
* Purchases by items data table.
|
||||||
@@ -17,7 +17,7 @@ import { TableStyle } from '@/constants';
|
|||||||
export default function PurchasesByItemsTable({ companyName }) {
|
export default function PurchasesByItemsTable({ companyName }) {
|
||||||
// Purchases by items context.
|
// Purchases by items context.
|
||||||
const {
|
const {
|
||||||
purchaseByItems: { tableRows, query },
|
purchaseByItems: { table, query },
|
||||||
} = usePurchaseByItemsContext();
|
} = usePurchaseByItemsContext();
|
||||||
|
|
||||||
// Purchases by items table columns.
|
// Purchases by items table columns.
|
||||||
@@ -32,7 +32,7 @@ export default function PurchasesByItemsTable({ companyName }) {
|
|||||||
>
|
>
|
||||||
<PurchasesByItemsDataTable
|
<PurchasesByItemsDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={tableRows}
|
data={table.rows}
|
||||||
expandable={true}
|
expandable={true}
|
||||||
expandToggleColumn={1}
|
expandToggleColumn={1}
|
||||||
expandColumnSpace={1}
|
expandColumnSpace={1}
|
||||||
@@ -58,7 +58,7 @@ const PurchasesByItemsDataTable = styled(ReportDataTable)`
|
|||||||
padding-top: 0.36rem;
|
padding-top: 0.36rem;
|
||||||
padding-bottom: 0.36rem;
|
padding-bottom: 0.36rem;
|
||||||
}
|
}
|
||||||
.tr.row_type--total .td {
|
.tr.row_type--TOTAL .td {
|
||||||
border-top: 1px solid #bbb;
|
border-top: 1px solid #bbb;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-bottom: 3px double #000;
|
border-bottom: 3px double #000;
|
||||||
|
|||||||
@@ -1,69 +1,22 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import { useRef } from 'react';
|
||||||
import intl from 'react-intl-universal';
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
Classes,
|
||||||
|
Intent,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
ProgressBar,
|
||||||
|
Text,
|
||||||
|
} from '@blueprintjs/core';
|
||||||
|
|
||||||
import { If } from '@/components';
|
import { AppToaster, If, Stack } from '@/components';
|
||||||
import { Align } from '@/constants';
|
|
||||||
import { CellTextSpan } from '@/components/Datatable/Cells';
|
|
||||||
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||||
import { getColumnWidth } from '@/utils';
|
|
||||||
import FinancialLoadingBar from '../FinancialLoadingBar';
|
import FinancialLoadingBar from '../FinancialLoadingBar';
|
||||||
|
import {
|
||||||
/**
|
usePurchasesByItemsCsvExport,
|
||||||
* Retrieve purchases by items table columns.
|
usePurchasesByItemsXlsxExport,
|
||||||
*/
|
} from '@/hooks/query';
|
||||||
export const usePurchasesByItemsTableColumns = () => {
|
|
||||||
// purchases by items context.
|
|
||||||
const {
|
|
||||||
purchaseByItems: { tableRows },
|
|
||||||
} = usePurchaseByItemsContext();
|
|
||||||
|
|
||||||
return React.useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
Header: intl.get('item_name'),
|
|
||||||
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
|
|
||||||
className: 'name',
|
|
||||||
width: 180,
|
|
||||||
textOverview: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: intl.get('quantity_purchased'),
|
|
||||||
accessor: 'quantity_purchased_formatted',
|
|
||||||
Cell: CellTextSpan,
|
|
||||||
className: 'quantity_purchased_formatted',
|
|
||||||
width: getColumnWidth(tableRows, `quantity_purchased_formatted`, {
|
|
||||||
minWidth: 150,
|
|
||||||
}),
|
|
||||||
textOverview: true,
|
|
||||||
align: Align.Right,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: intl.get('purchase_amount'),
|
|
||||||
accessor: 'purchase_cost_formatted',
|
|
||||||
Cell: CellTextSpan,
|
|
||||||
className: 'purchase_cost_formatted',
|
|
||||||
width: getColumnWidth(tableRows, `purchase_cost_formatted`, {
|
|
||||||
minWidth: 150,
|
|
||||||
}),
|
|
||||||
textOverview: true,
|
|
||||||
align: Align.Right,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: intl.get('average_price'),
|
|
||||||
accessor: 'average_cost_price_formatted',
|
|
||||||
Cell: CellTextSpan,
|
|
||||||
className: 'average_cost_price_formatted',
|
|
||||||
width: getColumnWidth(tableRows, `average_cost_price_formatted`, {
|
|
||||||
minWidth: 180,
|
|
||||||
}),
|
|
||||||
textOverview: true,
|
|
||||||
align: Align.Right,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[tableRows],
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purchases by items progress loading bar.
|
* Purchases by items progress loading bar.
|
||||||
@@ -77,3 +30,88 @@ export function PurchasesByItemsLoadingBar() {
|
|||||||
</If>
|
</If>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items export menu.
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
export const PurchasesByItemsExportMenu = () => {
|
||||||
|
const toastKey = useRef(null);
|
||||||
|
const commonToastConfig = {
|
||||||
|
isCloseButtonShown: true,
|
||||||
|
timeout: 2000,
|
||||||
|
};
|
||||||
|
const { query } = usePurchaseByItemsContext();
|
||||||
|
|
||||||
|
const openProgressToast = (amount: number) => {
|
||||||
|
return (
|
||||||
|
<Stack spacing={8}>
|
||||||
|
<Text>The report has been exported successfully.</Text>
|
||||||
|
<ProgressBar
|
||||||
|
className={classNames('toast-progress', {
|
||||||
|
[Classes.PROGRESS_NO_STRIPES]: amount >= 100,
|
||||||
|
})}
|
||||||
|
intent={amount < 100 ? Intent.PRIMARY : Intent.SUCCESS}
|
||||||
|
value={amount / 100}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Export the report to xlsx.
|
||||||
|
const { mutateAsync: xlsxExport } = usePurchasesByItemsXlsxExport(query, {
|
||||||
|
onDownloadProgress: (xlsxExportProgress: number) => {
|
||||||
|
if (!toastKey.current) {
|
||||||
|
toastKey.current = AppToaster.show({
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
AppToaster.show(
|
||||||
|
{
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
},
|
||||||
|
toastKey.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Export the report to csv.
|
||||||
|
const { mutateAsync: csvExport } = usePurchasesByItemsCsvExport(query, {
|
||||||
|
onDownloadProgress: (xlsxExportProgress: number) => {
|
||||||
|
if (!toastKey.current) {
|
||||||
|
toastKey.current = AppToaster.show({
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
AppToaster.show(
|
||||||
|
{
|
||||||
|
message: openProgressToast(xlsxExportProgress),
|
||||||
|
...commonToastConfig,
|
||||||
|
},
|
||||||
|
toastKey.current,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Handle csv export button click.
|
||||||
|
const handleCsvExportBtnClick = () => {
|
||||||
|
csvExport();
|
||||||
|
};
|
||||||
|
// Handle xlsx export button click.
|
||||||
|
const handleXlsxExportBtnClick = () => {
|
||||||
|
xlsxExport();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu>
|
||||||
|
<MenuItem
|
||||||
|
text={'XLSX (Microsoft Excel)'}
|
||||||
|
onClick={handleXlsxExportBtnClick}
|
||||||
|
/>
|
||||||
|
<MenuItem text={'CSV'} onClick={handleCsvExportBtnClick} />
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { getColumnWidth } from '@/utils';
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { Align } from '@/constants';
|
||||||
|
import { usePurchaseByItemsContext } from './PurchasesByItemsProvider';
|
||||||
|
|
||||||
|
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
|
||||||
|
|
||||||
|
const getReportColWidth = (data, accessor, headerText) => {
|
||||||
|
return getColumnWidth(
|
||||||
|
data,
|
||||||
|
accessor,
|
||||||
|
{ magicSpacing: 10, minWidth: 100 },
|
||||||
|
headerText,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Account name column mapper.
|
||||||
|
*/
|
||||||
|
const commonColumnMapper = R.curry((data, column) => {
|
||||||
|
const accessor = getTableCellValueAccessor(column.cell_index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: column.key,
|
||||||
|
Header: column.label,
|
||||||
|
accessor,
|
||||||
|
className: column.key,
|
||||||
|
textOverview: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Numeric columns accessor.
|
||||||
|
*/
|
||||||
|
const numericColumnAccessor = R.curry((data, column) => {
|
||||||
|
const accessor = getTableCellValueAccessor(column.cell_index);
|
||||||
|
const width = getReportColWidth(data, accessor, column.label);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
align: Align.Right,
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item name column accessor.
|
||||||
|
*/
|
||||||
|
const itemNameColumnAccessor = R.curry((data, column) => {
|
||||||
|
return {
|
||||||
|
...column,
|
||||||
|
width: 180,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const dynamiColumnMapper = R.curry((data, column) => {
|
||||||
|
const _numericColumnAccessor = numericColumnAccessor(data);
|
||||||
|
const _itemNameColumnAccessor = itemNameColumnAccessor(data);
|
||||||
|
|
||||||
|
return R.compose(
|
||||||
|
R.when(R.pathEq(['key'], 'item_name'), _itemNameColumnAccessor),
|
||||||
|
R.when(R.pathEq(['key'], 'quantity_purchases'), _numericColumnAccessor),
|
||||||
|
R.when(R.pathEq(['key'], 'purchase_amount'), _numericColumnAccessor),
|
||||||
|
R.when(R.pathEq(['key'], 'average_cost'), _numericColumnAccessor),
|
||||||
|
commonColumnMapper(data),
|
||||||
|
)(column);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes the dynamic columns that fetched from request to columns to table component.
|
||||||
|
*/
|
||||||
|
export const dynamicColumns = R.curry((data, columns) => {
|
||||||
|
return R.map(dynamiColumnMapper(data), columns);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the purchases by items sheet table columns for table component.
|
||||||
|
*/
|
||||||
|
export const usePurchasesByItemsTableColumns = () => {
|
||||||
|
const { purchaseByItems } = usePurchaseByItemsContext();
|
||||||
|
|
||||||
|
if (!purchaseByItems) {
|
||||||
|
throw new Error('Purchases by items context not found');
|
||||||
|
}
|
||||||
|
const { table } = purchaseByItems;
|
||||||
|
|
||||||
|
return dynamicColumns(table.rows, table.columns);
|
||||||
|
};
|
||||||
@@ -67,6 +67,7 @@ export const ERRORS = {
|
|||||||
BILL_NUMBER_EXISTS: 'BILL.NUMBER.EXISTS',
|
BILL_NUMBER_EXISTS: 'BILL.NUMBER.EXISTS',
|
||||||
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:
|
ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED:
|
||||||
'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED',
|
'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED',
|
||||||
|
BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT: 'BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT',
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Transformes the bill to initial values of edit form.
|
* Transformes the bill to initial values of edit form.
|
||||||
@@ -200,6 +201,14 @@ export const handleErrors = (errors, { setErrors }) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
errors.some((e) => e.type === ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT)
|
||||||
|
) {
|
||||||
|
AppToaster.show({
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
message: intl.get('bill.total_smaller_than_paid_amount'),
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSetPrimaryBranchToForm = () => {
|
export const useSetPrimaryBranchToForm = () => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function VendorCreditUniversalSearchSelectComponent({
|
|||||||
openDrawer,
|
openDrawer,
|
||||||
}) {
|
}) {
|
||||||
if (resourceType === RESOURCES_TYPES.VENDOR_CREDIT) {
|
if (resourceType === RESOURCES_TYPES.VENDOR_CREDIT) {
|
||||||
openDrawer(DRAWERS.VENDOR_CREDIT_DETAIL_DRAWER, {
|
openDrawer(DRAWERS.VENDOR_CREDIT_DETAILS, {
|
||||||
vendorCreditId: resourceId,
|
vendorCreditId: resourceId,
|
||||||
});
|
});
|
||||||
onAction && onAction();
|
onAction && onAction();
|
||||||
@@ -83,7 +83,7 @@ export function VendorCreditUniversalSearchItem(
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<div class="amount">${item.reference.amount}</div>
|
<div class="amount">{item.reference.formatted_amount}</div>
|
||||||
<VendorCreditUniversalSearchStatus receipt={item.reference} />
|
<VendorCreditUniversalSearchStatus receipt={item.reference} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function PaymentMadeEntriesTable({
|
|||||||
// Formik context.
|
// Formik context.
|
||||||
const {
|
const {
|
||||||
values: { vendor_id },
|
values: { vendor_id },
|
||||||
|
errors,
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
|
|
||||||
// Handle update data.
|
// Handle update data.
|
||||||
@@ -63,7 +64,7 @@ export default function PaymentMadeEntriesTable({
|
|||||||
data={entries}
|
data={entries}
|
||||||
spinnerProps={false}
|
spinnerProps={false}
|
||||||
payload={{
|
payload={{
|
||||||
errors: [],
|
errors: errors?.entries || [],
|
||||||
updateData: handleUpdateData,
|
updateData: handleUpdateData,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Icon } from '@/components';
|
|||||||
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
import { RESOURCES_TYPES } from '@/constants/resourcesTypes';
|
||||||
import { highlightText } from '@/utils';
|
import { highlightText } from '@/utils';
|
||||||
import { AbilitySubject, PaymentMadeAction } from '@/constants/abilityOption';
|
import { AbilitySubject, PaymentMadeAction } from '@/constants/abilityOption';
|
||||||
|
import { DRAWERS } from '@/constants/drawers';
|
||||||
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
import withDrawerActions from '@/containers/Drawer/withDrawerActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function CreditNoteUniversalSearchItem(
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<div class="amount">${item.reference.amount}</div>
|
<div class="amount">{item.reference.formatted_amount}</div>
|
||||||
<CreditNoteUniversalSearchStatus receipt={item.reference} />
|
<CreditNoteUniversalSearchStatus receipt={item.reference} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,16 @@ export const transformErrors = (errors, { setErrors }) => {
|
|||||||
intent: Intent.DANGER,
|
intent: Intent.DANGER,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
errors.some(
|
||||||
|
({ type }) => type === ERROR.INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AppToaster.show({
|
||||||
|
message: intl.get('sale_invoice.total_smaller_than_paid_amount'),
|
||||||
|
intent: Intent.DANGER,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
errors.some((error) => error.type === ERROR.SALE_INVOICE_NO_IS_REQUIRED)
|
errors.some((error) => error.type === ERROR.SALE_INVOICE_NO_IS_REQUIRED)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ export function InvoiceUniversalSearchItem(
|
|||||||
<span class="bp4-text-muted">
|
<span class="bp4-text-muted">
|
||||||
{highlightText(item.reference.invoice_no, query)}{' '}
|
{highlightText(item.reference.invoice_no, query)}{' '}
|
||||||
<Icon icon={'caret-right-16'} iconSize={16} />
|
<Icon icon={'caret-right-16'} iconSize={16} />
|
||||||
{item.reference.formatted_invoice_date}
|
{item.reference.invoice_date_formatted}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<div class="amount">${item.reference.balance}</div>
|
<div class="amount">{item.reference.total_formatted}</div>
|
||||||
<InvoiceStatus customer={item.reference} />
|
<InvoiceStatus customer={item.reference} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default function PaymentReceiveItemsTable({
|
|||||||
// Formik context.
|
// Formik context.
|
||||||
const {
|
const {
|
||||||
values: { customer_id },
|
values: { customer_id },
|
||||||
|
errors,
|
||||||
} = useFormikContext();
|
} = useFormikContext();
|
||||||
|
|
||||||
// No results message.
|
// No results message.
|
||||||
@@ -58,7 +59,7 @@ export default function PaymentReceiveItemsTable({
|
|||||||
data={entries}
|
data={entries}
|
||||||
spinnerProps={false}
|
spinnerProps={false}
|
||||||
payload={{
|
payload={{
|
||||||
errors: [],
|
errors: errors?.entries || [],
|
||||||
updateData: handleUpdateData,
|
updateData: handleUpdateData,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ReceiptUniversalSearchItem(
|
|||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<>
|
<>
|
||||||
<div class="amount">${item.reference.amount}</div>
|
<div class="amount">{item.reference.formatted_amount}</div>
|
||||||
<ReceiptStatus receipt={item.reference} />
|
<ReceiptStatus receipt={item.reference} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const VendorUniversalSearchSelectAction = withDrawerActions(
|
|||||||
const vendorToSearch = (contact) => ({
|
const vendorToSearch = (contact) => ({
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
text: contact.display_name,
|
text: contact.display_name,
|
||||||
label: contact.balance > 0 ? contact.formatted_balance + '' : '',
|
label: contact.formatted_balance,
|
||||||
reference: contact,
|
reference: contact,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ const commonInvalidateQueries = (queryClient) => {
|
|||||||
// Invalidate financial reports.
|
// Invalidate financial reports.
|
||||||
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
|
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
|
||||||
|
|
||||||
|
// Invalidate the transactions by reference.
|
||||||
|
queryClient.invalidateQueries(t.TRANSACTIONS_BY_REFERENCE);
|
||||||
|
|
||||||
// Invalidate items associated bills transactions.
|
// Invalidate items associated bills transactions.
|
||||||
queryClient.invalidateQueries(t.ITEMS_ASSOCIATED_WITH_BILLS);
|
queryClient.invalidateQueries(t.ITEMS_ASSOCIATED_WITH_BILLS);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ const commonInvalidateQueries = (queryClient) => {
|
|||||||
// Invalidate financial reports.
|
// Invalidate financial reports.
|
||||||
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
|
queryClient.invalidateQueries(t.FINANCIAL_REPORT);
|
||||||
|
|
||||||
|
// Invalidate transactions by reference.
|
||||||
|
queryClient.invalidateQueries(t.TRANSACTIONS_BY_REFERENCE);
|
||||||
|
|
||||||
// Invalidate mutate base currency abilities.
|
// Invalidate mutate base currency abilities.
|
||||||
queryClient.invalidateQueries(t.ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES);
|
queryClient.invalidateQueries(t.ORGANIZATION_MUTATE_BASE_CURRENCY_ABILITIES);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import QUERY_TYPES from './types';
|
import QUERY_TYPES from './types';
|
||||||
|
import useApiRequest from '../useRequest';
|
||||||
|
|
||||||
function getRandomItemFromArray(arr) {
|
interface LatestExchangeRateQuery {
|
||||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
fromCurrency?: string;
|
||||||
return arr[randomIndex];
|
toCurrency?: string;
|
||||||
}
|
|
||||||
function delay(t, val) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, t, val));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves tax rates.
|
* Retrieves latest exchange rate.
|
||||||
* @param {number} customerId - Customer id.
|
* @param {number} customerId - Customer id.
|
||||||
*/
|
*/
|
||||||
export function useExchangeRate(
|
export function useLatestExchangeRate(
|
||||||
fromCurrency: string,
|
{ toCurrency, fromCurrency }: LatestExchangeRateQuery,
|
||||||
toCurrency: string,
|
|
||||||
props,
|
props,
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
const apiRequest = useApiRequest();
|
||||||
[QUERY_TYPES.EXCHANGE_RATE, fromCurrency, toCurrency],
|
|
||||||
async () => {
|
|
||||||
await delay(100);
|
|
||||||
|
|
||||||
return {
|
return useQuery(
|
||||||
from_currency: fromCurrency,
|
[QUERY_TYPES.EXCHANGE_RATE, toCurrency, fromCurrency],
|
||||||
to_currency: toCurrency,
|
() =>
|
||||||
exchange_rate: 1.00,
|
apiRequest
|
||||||
};
|
.http({
|
||||||
},
|
url: `/api/exchange_rates/latest`,
|
||||||
|
method: 'get',
|
||||||
|
params: {
|
||||||
|
to_currency: toCurrency,
|
||||||
|
from_currency: fromCurrency,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => res.data),
|
||||||
props,
|
props,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,20 +426,58 @@ export function usePurchasesByItems(query, props) {
|
|||||||
params: query,
|
params: query,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
select: (res) => ({
|
select: (res) => res.data,
|
||||||
tableRows: purchasesByItemsReducer(res.data.data),
|
|
||||||
...res.data,
|
|
||||||
}),
|
|
||||||
defaultData: {
|
|
||||||
tableRows: [],
|
|
||||||
data: [],
|
|
||||||
query: {},
|
|
||||||
},
|
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function usePurchasesByItemsTable(query, props) {
|
||||||
|
return useRequestQuery(
|
||||||
|
[t.FINANCIAL_REPORT, t.PURCHASES_BY_ITEMS, query],
|
||||||
|
{
|
||||||
|
method: 'get',
|
||||||
|
url: '/financial_statements/purchases-by-items',
|
||||||
|
params: query,
|
||||||
|
headers: {
|
||||||
|
accept: 'application/json+table',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
select: (res) => res.data,
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePurchasesByItemsCsvExport = (query, args) => {
|
||||||
|
return useDownloadFile({
|
||||||
|
url: '/financial_statements/purchases-by-items',
|
||||||
|
config: {
|
||||||
|
headers: {
|
||||||
|
accept: 'application/csv',
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
},
|
||||||
|
filename: 'purchases_by_items.csv',
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePurchasesByItemsXlsxExport = (query, args) => {
|
||||||
|
return useDownloadFile({
|
||||||
|
url: '/financial_statements/purchases-by-items',
|
||||||
|
config: {
|
||||||
|
headers: {
|
||||||
|
accept: 'application/xlsx',
|
||||||
|
},
|
||||||
|
params: query,
|
||||||
|
},
|
||||||
|
filename: 'purchases_by_items.xlsx',
|
||||||
|
...args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve sales by items.
|
* Retrieve sales by items.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user