Compare commits

...

36 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
0868eeaa0e Merge pull request #353 from ANasouf/BIG-112-Customer-emails-addresses-gets-the-.-removed-when-a-new-one-is-added
fix: gmail email addresses dots gets removed
2024-02-10 23:39:18 +02:00
a.nasouf
17dbe9713b fix: remove normalizeEmail function 2024-02-10 19:59:12 +02:00
Ahmed Bouhuolia
ecaf8c99bb Merge pull request #354 from ANasouf/BIG-109-some-keywords-are-not-localized
fix: some keywords are not localized
2024-02-08 20:12:27 +02:00
Ahmed Bouhuolia
90bde20674 Merge pull request #358 from bigcapitalhq/big-127-amount-decimals-are-set-to-zero-when-creating-an-new-payment
fix: payment receive subtotal shouldn't be rounded
2024-02-06 20:39:27 +02:00
Ahmed Bouhuolia
0c61f85707 chore: remove format number from estimate quantity 2024-02-06 20:38:25 +02:00
Ahmed Bouhuolia
0f678e61c5 fix: Decimal amounts are rounded when create a new transaction on some transactions types 2024-02-06 20:31:48 +02:00
Ahmed Bouhuolia
374f1acf8a fix: payment receive subtotal shouldn't be rounded 2024-02-06 10:54:41 +02:00
Ahmed Bouhuolia
ada1428193 Merge pull request #357 from bigcapitalhq/big-139-trial-balance-total-row-shouldnt-show-if-accounts-have-no
fix(server): Trial balance total row shouldn't show if accounts have no balances
2024-02-05 23:05:02 +02:00
Ahmed Bouhuolia
528d447443 fix(server): Trial balance total row shouldn't show if accounts have no balances 2024-02-05 23:04:02 +02:00
Ahmed Bouhuolia
d6d67b9a51 Merge pull request #355 from bigcapitalhq/big-126-invalid-bill-payment-amount-on-editing-bill-payment
fix: Invalid bill payment amount on editing bill payment
2024-02-05 22:39:52 +02:00
Ahmed Bouhuolia
12740223a8 fix: Invalid bill payment amount on editing bill payment 2024-02-05 22:38:56 +02:00
a.nasouf
f7f77b12c9 fix: file formatting 2024-02-05 20:05:10 +02:00
a.nasouf
a6db4fb6df fix: some keywords are not localized 2024-02-05 19:52:48 +02:00
a.nasouf
b38020d397 fix: gmail email addresses dots gets removed 2024-02-05 18:58:02 +02:00
Ahmed Bouhuolia
d090d5a026 Merge pull request #352 from bigcapitalhq/big-138-revert-the-paid-amount-to-bill-transaction-after-editing
fix(server): Revert the paid amount to bill transaction after editing bill payment amount
2024-02-05 18:52:09 +02:00
Ahmed Bouhuolia
7b5287ee80 fix(server): Revert the paid amount to bill transaction after editing bill payment amount 2024-02-05 18:50:34 +02:00
Ahmed Bouhuolia
ba3ea93a2d chore: dump CHANGELOG.md 2024-01-30 00:28:23 +02:00
Ahmed Bouhuolia
6b77bda77f Merge pull request #342 from bigcapitalhq/big-130-edited-transactions-does-not-reflect-on-reports
hotfix: editing sales and expense transactions don't reflect GL entries
2024-01-29 23:27:42 +02:00
Ahmed Bouhuolia
ba387e81f7 hotfix: editing sales and expense transactions don't reflect GL entries 2024-01-29 23:25:37 +02:00
Ahmed Bouhuolia
0414c090ea Merge pull request #335 from bigcapitalhq/big-128-inconsistency-in-currency-display-on-the-quick-find-feature
fix(webapp): inconsistency in currency of universal search items
2024-01-29 19:24:16 +02:00
Ahmed Bouhuolia
a52f3a933f fix(webapp): inconsistency in currency of universal search items 2024-01-29 19:19:46 +02:00
Ahmed Bouhuolia
d01eacf8d9 feat(server): add formattedAmount to purchase invoice to POJO 2024-01-29 19:18:59 +02:00
Ahmed Bouhuolia
0da151faaa Merge pull request #340 from bigcapitalhq/big-65-get-realtime-exchange-rate-from-third-party-service
feat: get latest exchange rate from third party services
2024-01-29 18:44:05 +02:00
Ahmed Bouhuolia
f93c8b46dc chore: change env name in .env.example 2024-01-29 18:42:54 +02:00
Ahmed Bouhuolia
59c5c8979d feat(webapp): add default exchange rate value to 1 in cause the ex. rate service returned error or failed. 2024-01-29 18:39:28 +02:00
Ahmed Bouhuolia
74a07847a4 feat(server): ability to assign the base currency as api query when getting latest ex. rate 2024-01-28 20:11:44 +02:00
Ahmed Bouhuolia
1740226294 feat(webapp): hook up latest exchange rate api 2024-01-28 18:48:35 +02:00
Ahmed Bouhuolia
1b20d1b073 feat(server): add application layer to exchange rate service 2024-01-28 18:47:49 +02:00
Ahmed Bouhuolia
ac7175d83b feat: get latest exchange rate from third party services 2024-01-28 15:52:54 +02:00
Ahmed Bouhuolia
d72d8e60d6 Merge pull request #339 from bigcapitalhq/big-122-bug-expense-details-amount-should-not-be-rounded
fix: Expense amounts should not be rounded
2024-01-26 23:52:30 +02:00
Ahmed Bouhuolia
de5920f910 fix: Expense amounts should not be rounded 2024-01-26 23:46:45 +02:00
Ahmed Bouhuolia
475c4e9967 fix(webapp): inconsistency in currency of universal search items 2024-01-26 00:05:24 +02:00
Ahmed Bouhuolia
21eb88ef53 chore: dump CHANGELOG.md file 2024-01-24 00:26:56 +02:00
Ahmed Bouhuolia
429159acf9 feat: export purchases by items to csv/xlsx (#327) 2024-01-23 12:33:43 +02:00
Ahmed Bouhuolia
7eb84474a5 hotfix(server): Unhandled thrown errors of services (#329) 2024-01-22 21:57:14 +02:00
allcontributors[bot]
e42adcae63 docs: add asenawritescode as a contributor for bug (#325)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-01-21 14:46:11 +02:00
106 changed files with 1478 additions and 920 deletions

View File

@@ -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,

View File

@@ -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=

View File

@@ -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

View File

@@ -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>

View File

@@ -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') {

View File

@@ -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',
},
],
}); });
} }
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -303,7 +303,7 @@ export default class BillsController extends BaseController {
try { try {
const bill = await this.billsApplication.getBill(tenantId, billId); const bill = await this.billsApplication.getBill(tenantId, billId);
return res.status(200).send(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);
} }

View File

@@ -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);
} }

View File

@@ -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) {

View File

@@ -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);
} }
}; };

View File

@@ -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,

View File

@@ -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);
} }

View File

@@ -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.',

View File

@@ -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 });
} }
} }

View File

@@ -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,
}
}
}; };

View File

@@ -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 {

View File

@@ -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>;
};

View 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;
}

View 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);
}
}

View 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.'
);
}
}
}

View 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';

View File

@@ -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
*/ */

View File

@@ -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,

View File

@@ -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

View File

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

View File

@@ -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',

View File

@@ -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);
}
}

View File

@@ -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);
}
} }
} }

View File

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

View File

@@ -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,
});
}
}

View File

@@ -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,
});
};
} }

View File

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

View File

@@ -70,10 +70,10 @@ export class ExpensesWriteGLSubscriber {
authorizedUser, authorizedUser,
trx, trx,
}: IExpenseEventEditPayload) => { }: IExpenseEventEditPayload) => {
// 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

View File

@@ -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 };
} }
} }

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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[];
}
}

View File

@@ -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,
};
}
}

View File

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

View File

@@ -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);

View File

@@ -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())
)([]); )([]);
}; };

View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -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,

View File

@@ -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);

View File

@@ -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.
*/ */

View File

@@ -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,

View File

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

View File

@@ -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 = [];

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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 });
}; };
/** /**

View File

@@ -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);

View File

@@ -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

View File

@@ -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');

View File

@@ -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

View File

@@ -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.
*/ */

View File

@@ -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>
); );
} }

View File

@@ -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',
}; };

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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>
); );

View File

@@ -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'} />}

View File

@@ -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

View File

@@ -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,

View File

@@ -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}
/> />

View File

@@ -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,

View File

@@ -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'} />}

View File

@@ -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,
}), }),

View File

@@ -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'} />}

View File

@@ -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',
}, },
], ],
[], [],

View File

@@ -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

View File

@@ -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,
}), }),

View File

@@ -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,

View File

@@ -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;
}; };

View File

@@ -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>
); );

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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);
};

View File

@@ -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 = () => {

View File

@@ -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} />
</> </>
} }

View File

@@ -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,
}} }}

View File

@@ -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';
/** /**

View File

@@ -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} />
</> </>
} }

View File

@@ -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)
) { ) {

View File

@@ -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} />
</> </>
} }

View File

@@ -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,
}} }}

View File

@@ -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} />
</> </>
} }

View File

@@ -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,
}); });

View File

@@ -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);

View File

@@ -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);
}; };

View File

@@ -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,
); );
} }

View File

@@ -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