Compare commits

...

44 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
abffdd1029 fix(server): hotbug retireving empty results of inventory valuation and sales by items sheets 2024-01-21 14:34:36 +02:00
allcontributors[bot]
d052c23560 docs: add xprnio as a contributor for bug (#324)
* 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 00:21:46 +02:00
Ragnar Laud
f02afd3c9f fix(webapp): AccountActivateAlert import (#322) 2024-01-21 00:18:31 +02:00
Ahmed Bouhuolia
3df17390e2 Merge pull request #318 from bigcapitalhq/big-120-get-cashflow-transaction-broken-cause-transaction-type
fix: `BIG-120` get cashflow transaction broken cause transaction type
2024-01-20 18:04:51 +02:00
Ahmed Bouhuolia
df38f8893d fix(server): get cashflow transaction type 2024-01-20 18:03:43 +02:00
Ahmed Bouhuolia
8882bc677e fix(webapp): undefined transactionNumber function 2024-01-20 18:03:26 +02:00
Ahmed Bouhuolia
f03d01113c Merge pull request #315 from bigcapitalhq/hotfix-pdf-printing
fix(server): the invoice and payment receipt printing
2024-01-20 15:53:59 +02:00
Ahmed Bouhuolia
8f431597d9 fix(server): hotbug the invoice and payment receipt printing 2024-01-20 15:51:57 +02:00
Ahmed Bouhuolia
03bc78a068 feat(webapp): remove the un-used functions 2024-01-19 23:33:15 +02:00
Ahmed Bouhuolia
ecf5d60db0 Merge pull request #310 from bigcapitalhq/big-99-purchases-by-items
feat: sales by items export csv & xlsx
2024-01-19 11:41:56 +02:00
Ahmed Bouhuolia
3a5fd2782a fix: sales by items issues 2024-01-19 11:39:00 +02:00
Ahmed Bouhuolia
1d8416ebfe fix: sales by items TS types 2024-01-19 11:25:19 +02:00
Ahmed Bouhuolia
3672abe7a5 feat: inventory valuation csv and xlsx export (#308)
* feat: inventory valuation csv and xlsx export

* feat(server): inventory valuation sheet exporting

* feat(webapp): inventory valuation sheet dyanmic columns

* feat: inventory valuation dynamic columns

* feat: inventory valuation TS types
2024-01-18 20:16:29 +02:00
Ahmed Bouhuolia
2753908b83 feat: sales by items dynamic columns 2024-01-18 19:52:11 +02:00
Asena
8495990ec2 Show customer / vendor balance. (#311)
* feat : Update the Customer select prop to balance
* feat : Update the Vendor select prop to balance
* feat: Update balance to formatted_balance
2024-01-18 14:47:07 +02:00
Ahmed Bouhuolia
471ce1b7af feat(webapp): dynamic columns of sales by items sheet 2024-01-18 14:39:56 +02:00
Ahmed Bouhuolia
4a920176f4 feat(server): sales by items table 2024-01-17 18:49:38 +02:00
Ahmed Bouhuolia
74fd76ce77 feat(server): sales by items export csv & xlsx 2024-01-17 00:24:13 +02:00
Ahmed Bouhuolia
c9f57d9a75 chore: update CHANGELOG.md file 2024-01-15 00:30:08 +02:00
Ahmed Bouhuolia
6c9810ea4c Merge pull request #270 from bigcapitalhq/big-44-auto-re-calculate-the-items-rate-once-changing-the-invoice
feat: Auto re-calculate the items rate once changing the invoice exchange rate.
2024-01-14 16:08:56 +02:00
Ahmed Bouhuolia
1e4b29f83c fix: content tweaks in rates re-calc popover 2024-01-14 15:59:25 +02:00
Ahmed Bouhuolia
2b03ac7f16 feat: implement auto entries rates re-calculation after change the exchange rate 2024-01-14 14:44:48 +02:00
Ahmed Bouhuolia
2cb8c2932f Merge branch 'develop' into big-44-auto-re-calculate-the-items-rate-once-changing-the-invoice 2024-01-11 20:27:42 +02:00
Ahmed Bouhuolia
2814d23f75 Update README.md 2024-01-10 20:24:11 +02:00
Ahmed Bouhuolia
35fa2389e1 feat: add headless accounting on README.md file 2024-01-10 20:21:46 +02:00
Ahmed Bouhuolia
c18b450129 Merge pull request #303 from bigcapitalhq/journal-sheet-export
Export general ledger & Journal to CSV and XLSX
2024-01-10 17:45:04 +02:00
Ahmed Bouhuolia
8db6e9f049 fix: remove blank files 2024-01-10 17:43:26 +02:00
Ahmed Bouhuolia
b63a2006d3 Merge pull request #305 from bigcapitalhq/all-contributors/add-ANasouf
docs: add ANasouf as a contributor for code
2024-01-09 22:23:15 +02:00
allcontributors[bot]
8c1ca503bd docs: update .all-contributorsrc [skip ci] 2024-01-09 20:22:36 +00:00
allcontributors[bot]
0ac79e661c docs: update README.md [skip ci] 2024-01-09 20:22:35 +00:00
Ahmed Bouhuolia
66ba4d35aa Merge pull request #304 from ANasouf/BIG-53-Add-approve-reject-buttons-on-action-bar-of-the-estimate-details-drawer
feat(webapp): add approve/reject to action bar of estimate details dr…
2024-01-09 22:17:43 +02:00
Ahmed Bouhuolia
46d25dff4c fix: move approve/reject estimate buttons to more menu 2024-01-09 22:14:10 +02:00
Ahmed Bouhuolia
5ef99f2cb3 feat: general ledger and journal exporting 2024-01-07 18:48:14 +02:00
Ahmed Bouhuolia
7aee76e461 feat: general ledger and journal rows types 2024-01-06 22:30:41 +02:00
Ahmed Bouhuolia
79f3f1b63d feat: wip journal and general ledger dyanmic columns 2024-01-06 20:16:22 +02:00
a.nasouf
26e104b9f1 feat(webapp): add approve/reject to action bar of estimate details drawer 2024-01-06 15:28:02 +02:00
Ahmed Bouhuolia
c71836ec27 feat: wip general ledger table 2024-01-04 21:43:57 +02:00
Ahmed Bouhuolia
60b1bc9ed7 feat(server): wip journal and general ledger table layer 2024-01-04 17:22:13 +02:00
Ahmed Bouhuolia
e6a3daa2c3 feat(server): general ledger exporting to csv/xlsx 2024-01-02 21:54:10 +02:00
Ahmed Bouhuolia
276ef1c907 feat(server): journal sheet csv/xlsx export 2024-01-02 21:53:37 +02:00
Ahmed Bouhuolia
83e48cce42 feat: remove hint popup of mail notification form 2024-01-01 20:59:31 +02:00
Ahmed Bouhuolia
99ca683d13 chore: update CHANGELOG file 2024-01-01 20:57:08 +02:00
Ahmed Bouhuolia
5062d891e1 feat: journal sheet export 2024-01-01 20:43:16 +02:00
Ahmed Bouhuolia
9531730d7a feat: Auto re-calculate the items rate once changing the invoice exchange rate. 2023-10-16 19:14:27 +02:00
138 changed files with 3744 additions and 2080 deletions

View File

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

View File

@@ -56,5 +56,5 @@ GOTENBERG_URL=http://gotenberg:3000
GOTENBERG_DOCS_URL=http://server:3000/public/
# Gotenberg API - (development)
# GOTENBERG_URL=http://gotenberg:3000
# GOTENBERG_DOCS_URL=http://server:3000/public/
# GOTENBERG_URL=http://localhost:9000
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/

View File

@@ -2,6 +2,19 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.13.1] - 15-01-2024
* feat(webapp): add approve/reject to action bar of estimate details dr… by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/304
* docs: add ANasouf as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/305
* feat: Export general ledger & Journal to CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/303
* feat: Auto re-calculate the items rate once changing the invoice exchange rate. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/270
## [0.13.0] - 31-12-2023
* feat: Send an invoice mail the customer email by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/292
* fix: Allow non-numeric postal codes by @cschuijt in https://github.com/bigcapitalhq/bigcapital/pull/294
* docs: add cschuijt as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/295
## [0.12.1] - 17-11-2023
* feat: Add default customer message and terms conditions to the transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/291

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -531,7 +531,6 @@ export default class PaymentReceivesController extends BaseController {
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
public sendPaymentReceiveByMail = async (
req: Request,
@@ -546,6 +545,7 @@ export default class PaymentReceivesController extends BaseController {
includeOptionals: false,
}
);
try {
await this.paymentReceiveApplication.notifyPaymentByMail(
tenantId,

View File

@@ -418,44 +418,34 @@ export default class SaleInvoicesController extends BaseController {
* @param {Request} req - Request object.
* @param {Response} res - Response object.
*/
private async getSaleInvoice(
req: Request,
res: Response,
next: NextFunction
) {
private async getSaleInvoice(req: Request, res: Response) {
const { id: saleInvoiceId } = req.params;
const { tenantId, user } = req;
try {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId,
saleInvoiceId,
user
);
// Response formatter.
res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: () => {
return res
.status(200)
.send(this.transfromToResponse({ saleInvoice }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoice
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
} catch (error) {
next(error);
}
// Response formatter.
return res.format({
// JSON content type.
[ACCEPT_TYPE.APPLICATION_JSON]: async () => {
const saleInvoice = await this.saleInvoiceApplication.getSaleInvoice(
tenantId,
saleInvoiceId,
user
);
return res.status(200).send(this.transfromToResponse({ saleInvoice }));
},
// PDF content type.
[ACCEPT_TYPE.APPLICATION_PDF]: async () => {
const pdfContent = await this.saleInvoiceApplication.saleInvoicePdf(
tenantId,
saleInvoiceId
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
},
});
}
/**
* Retrieve paginated sales invoices with custom view metadata.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ const ERRORS = {
};
@Service()
export default class GeneralLedgerService {
export class GeneralLedgerService {
@Inject()
tenancy: TenancyService;
@@ -64,7 +64,7 @@ export default class GeneralLedgerService {
* @param {number} tenantId -
* @returns {IGeneralLedgerMeta}
*/
reportMetadata(tenantId: number): IGeneralLedgerMeta {
reportMetadata(tenantId: number, filter): IGeneralLedgerMeta {
const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService
@@ -78,11 +78,15 @@ export default class GeneralLedgerService {
group: 'organization',
key: 'base_currency',
});
const fromDate = moment(filter.fromDate).format('YYYY MMM DD');
const toDate = moment(filter.toDate).format('YYYY MMM DD');
return {
isCostComputeRunning: parseBoolean(isCostComputeRunning, false),
organizationName,
baseCurrency
baseCurrency,
fromDate,
toDate
};
}
@@ -166,7 +170,7 @@ export default class GeneralLedgerService {
return {
data: reportData,
query: filter,
meta: this.reportMetadata(tenantId),
meta: this.reportMetadata(tenantId, filter),
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,10 @@ import {
IJournalReportQuery,
IJournalReport,
IContact,
IJournalTableData,
} from '@/interfaces';
import FinancialSheet from '../FinancialSheet';
import moment from 'moment';
export default class JournalSheet extends FinancialSheet {
readonly tenantId: number;
@@ -96,6 +98,8 @@ export default class JournalSheet extends FinancialSheet {
return {
date: groupEntry.date,
dateFormatted: moment(groupEntry.date).format('YYYY MMM DD'),
referenceType: groupEntry.referenceType,
referenceId: groupEntry.referenceId,
referenceTypeFormatted: this.i18n.__(groupEntry.referenceTypeFormatted),
@@ -131,7 +135,7 @@ export default class JournalSheet extends FinancialSheet {
* Retrieve journal report.
* @return {IJournalReport}
*/
reportData(): IJournalReport {
reportData(): IJournalTableData {
return this.entriesWalker(this.journal.entries);
}
}

View File

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

View File

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

View File

@@ -1,24 +1,25 @@
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { IJournalReportQuery, IJournalSheetMeta } from '@/interfaces';
import {
IJournalReportQuery,
IJournalSheet,
IJournalSheetMeta,
IJournalTableData,
} from '@/interfaces';
import JournalSheet from './JournalSheet';
import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean, transformToMap } from 'utils';
import { Tenant } from '@/system/models';
import { parseBoolean, transformToMap } from 'utils';
@Service()
export default class JournalSheetService {
export class JournalSheetService {
@Inject()
tenancy: TenancyService;
private tenancy: TenancyService;
@Inject()
inventoryService: InventoryService;
@Inject('logger')
logger: any;
private inventoryService: InventoryService;
/**
* Default journal sheet filter queyr.
@@ -67,9 +68,13 @@ export default class JournalSheetService {
/**
* Journal sheet.
* @param {number} tenantId
* @param {IJournalSheetFilterQuery} query
* @param {IJournalReportQuery} query
* @returns {Promise<IJournalSheet>}
*/
async journalSheet(tenantId: number, query: IJournalReportQuery) {
async journalSheet(
tenantId: number,
query: IJournalReportQuery
): Promise<IJournalSheet> {
const i18n = this.tenancy.i18n(tenantId);
const { accountRepository, transactionsRepository, contactRepository } =
this.tenancy.repositories(tenantId);
@@ -80,11 +85,6 @@ export default class JournalSheetService {
...this.defaultQuery,
...query,
};
this.logger.info('[journal] trying to calculate the report.', {
tenantId,
filter,
});
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import { Inject, Service } from 'typedi';
import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { GetSaleInvoice } from './GetSaleInvoice';
@Service()
export class SaleInvoicePdf {
@@ -13,10 +12,7 @@ export class SaleInvoicePdf {
private templateInjectable: TemplateInjectable;
@Inject()
private validators: CommandSaleInvoiceValidators;
@Inject()
private tenancy: HasTenancyService;
private getInvoiceService: GetSaleInvoice;
/**
* Retrieve sale invoice pdf content.
@@ -28,18 +24,10 @@ export class SaleInvoicePdf {
tenantId: number,
invoiceId: number
): Promise<Buffer> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const saleInvoice = await SaleInvoice.query()
.findById(invoiceId)
.withGraphFetched('entries.item')
.withGraphFetched('entries.tax')
.withGraphFetched('customer')
.withGraphFetched('taxes.taxRate');
// Validates the given sale invoice existance.
this.validators.validateInvoiceExistance(saleInvoice);
const saleInvoice = await this.getInvoiceService.getSaleInvoice(
tenantId,
invoiceId
);
const htmlContent = await this.templateInjectable.render(
tenantId,
'modules/invoice-regular',

View File

@@ -7,6 +7,7 @@ import {
DEFAULT_INVOICE_REMINDER_MAIL_CONTENT,
DEFAULT_INVOICE_REMINDER_MAIL_SUBJECT,
} from './constants';
import { parseAndValidateMailOptions } from '@/services/MailNotification/utils';
@Service()
export class SendInvoiceMailReminder {
@@ -66,10 +67,10 @@ export class SendInvoiceMailReminder {
) {
const localMessageOpts = await this.getMailOption(tenantId, saleInvoiceId);
const messageOpts = {
...localMessageOpts,
...messageOptions,
};
const messageOpts = parseAndValidateMailOptions(
localMessageOpts,
messageOptions
);
const mail = new Mail()
.setSubject(messageOpts.subject)
.setTo(messageOpts.to)

View File

@@ -8,7 +8,7 @@ export class PaymentReceiveEntryTransfromer extends Transformer {
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['paymentAmountFormatted', 'entry'];
return ['paymentAmountFormatted', 'invoice'];
};
/**

View File

@@ -1,5 +1,4 @@
import Container, { Service } from 'typedi';
import events from '@/subscribers/events';
import { SendPaymentReceiveMailNotification } from './PaymentReceiveMailNotification';
@Service()
@@ -22,8 +21,6 @@ export class PaymentReceiveMailNotificationJob {
const { tenantId, paymentReceiveId, messageDTO } = job.attrs.data;
const paymentMail = Container.get(SendPaymentReceiveMailNotification);
console.log(tenantId, paymentReceiveId, messageDTO);
try {
await paymentMail.sendMail(tenantId, paymentReceiveId, messageDTO);
done();

View File

@@ -1,7 +1,6 @@
import { IPaymentReceive, IPaymentReceiveEntry } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
import { SaleInvoiceTransformer } from '../Invoices/SaleInvoiceTransformer';
import { PaymentReceiveEntryTransfromer } from './PaymentReceiveEntryTransformer';
export class PaymentReceiveTransfromer extends Transformer {

View File

@@ -177,6 +177,7 @@ export class SaleReceiptApplication {
* Sends the receipt mail of the given sale receipt.
* @param {number} tenantId
* @param {number} saleReceiptId
* @param {SaleReceiptMailOptsDTO} messageOpts
* @returns {Promise<void>}
*/
public sendSaleReceiptMail(

View File

@@ -34,7 +34,7 @@ function CustomerSelectRoot({
<FSelect
items={items}
textAccessor={'display_name'}
labelAccessor={'code'}
labelAccessor={'formatted_balance'}
valueAccessor={'id'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
createNewItemRenderer={maybeCreateNewItemRenderer}

View File

@@ -51,8 +51,8 @@ export default function Dashboard() {
</Switch>
<DashboardUniversalSearch />
<DialogsContainer />
<GlobalHotkeys />
<DialogsContainer />
<DrawersContainer />
<AlertsContainer />
</DashboardProvider>

View File

@@ -3,8 +3,6 @@ import InviteUserDialog from '@/containers/Dialogs/InviteUserDialog';
import UserFormDialog from '@/containers/Dialogs/UserFormDialog';
import ItemCategoryDialog from '@/containers/Dialogs/ItemCategoryDialog';
import CurrencyFormDialog from '@/containers/Dialogs/CurrencyFormDialog';
import ExchangeRateFormDialog from '@/containers/Dialogs/ExchangeRateFormDialog';
import InventoryAdjustmentDialog from '@/containers/Dialogs/InventoryAdjustmentFormDialog';
import PaymentViaVoucherDialog from '@/containers/Dialogs/PaymentViaVoucherDialog';
import KeyboardShortcutsDialog from '@/containers/Dialogs/keyboardShortcutsDialog';
@@ -47,6 +45,7 @@ import ProjectInvoicingFormDialog from '@/containers/Projects/containers/Project
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
import { DialogsName } from '@/constants/dialogs';
import InvoiceExchangeRateChangeDialog from '@/containers/Sales/Invoices/InvoiceForm/Dialogs/InvoiceExchangeRateChangeDialog';
import InvoiceMailDialog from '@/containers/Sales/Invoices/InvoiceMailDialog/InvoiceMailDialog';
import EstimateMailDialog from '@/containers/Sales/Estimates/EstimateMailDialog/EstimateMailDialog';
import ReceiptMailDialog from '@/containers/Sales/Receipts/ReceiptMailDialog/ReceiptMailDialog';
@@ -62,7 +61,6 @@ export default function DialogsContainer() {
<CurrencyFormDialog dialogName={DialogsName.CurrencyForm} />
<InviteUserDialog dialogName={DialogsName.InviteForm} />
<UserFormDialog dialogName={DialogsName.UserForm} />
<ExchangeRateFormDialog dialogName={DialogsName.ExchangeRateForm} />
<ItemCategoryDialog dialogName={DialogsName.ItemCategoryForm} />
<InventoryAdjustmentDialog
dialogName={DialogsName.InventoryAdjustmentForm}
@@ -141,6 +139,9 @@ export default function DialogsContainer() {
dialogName={DialogsName.ProjectBillableEntriesForm}
/>
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
<InvoiceExchangeRateChangeDialog
dialogName={DialogsName.InvoiceExchangeRateChangeNotice}
/>
<InvoiceMailDialog dialogName={DialogsName.InvoiceMail} />
<EstimateMailDialog dialogName={DialogsName.EstimateMail} />
<ReceiptMailDialog dialogName={DialogsName.ReceiptMail} />

View File

@@ -1,30 +1,159 @@
// @ts-nocheck
import React from 'react';
import { useState } from 'react';
import styled from 'styled-components';
import { ControlGroup } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import {
Button,
Classes,
ControlGroup,
Intent,
Popover,
Position,
Spinner,
} from '@blueprintjs/core';
import { FlagIcon } from '../Tags';
import { FMoneyInputGroup, FFormGroup } from '../Forms';
import { useUncontrolled } from '@/hooks/useUncontrolled';
interface ExchangeRateValuesBag {
oldExchangeRate: string;
exchangeRate: string;
}
interface ExchangeRateInputGroupProps {
name: string;
fromCurrency: string;
toCurrency: string;
isLoading?: boolean;
inputGroupProps?: any;
formGroupProps?: any;
withPopoverRecalcConfirm?: boolean;
onRecalcConfirm: (bag: ExchangeRateValuesBag) => void;
onCancel: (bag: ExchangeRateValuesBag) => void;
isConfirmPopoverOpen?: boolean;
initialConfirmPopoverOpen?: boolean;
onConfirmPopoverOpen?: (isOpen: boolean) => void;
}
export function ExchangeRateInputGroup({
name,
fromCurrency,
toCurrency,
isLoading,
inputGroupProps,
formGroupProps,
name,
}) {
withPopoverRecalcConfirm = false,
onRecalcConfirm,
onCancel,
isConfirmPopoverOpen,
initialConfirmPopoverOpen,
onConfirmPopoverOpen,
}: ExchangeRateInputGroupProps) {
const [isOpen, handlePopoverOpen] = useUncontrolled<boolean>({
value: isConfirmPopoverOpen,
initialValue: initialConfirmPopoverOpen,
finalValue: false,
onChange: onConfirmPopoverOpen,
});
const { values, setFieldValue } = useFormikContext();
const [oldExchangeRate, setOldExchangeRate] = useState<string>('');
const exchangeRate = values[name];
const exchangeRateValuesBag: ExchangeRateValuesBag = {
exchangeRate,
oldExchangeRate,
};
// Handle re-calc confirm button click.
const handleRecalcConfirmBtn = () => {
handlePopoverOpen(false);
onRecalcConfirm && onRecalcConfirm(exchangeRateValuesBag);
};
// Handle cancel button click.
const handleCancelBtn = () => {
handlePopoverOpen(false);
onCancel && onCancel(exchangeRateValuesBag);
};
// Handle exchange rate field blur.
const handleExchangeRateFieldBlur = (value: string) => {
if (value !== values[name]) {
handlePopoverOpen(true);
setFieldValue(name, value);
setOldExchangeRate(values[name]);
}
};
const exchangeRateField = (
<ExchangeRateField
allowDecimals={true}
allowNegativeValue={true}
asyncControl={true}
onChange={() => null}
onBlur={handleExchangeRateFieldBlur}
rightElement={isLoading && <Spinner size={16} />}
decimalsLimit={5}
{...inputGroupProps}
name={name}
/>
);
const popoverConfirmContent = (
<PopoverContent>
<p>
Are you want to re-calculate item prices based on this exchange rate.
</p>
<div
style={{
display: 'flex',
marginTop: 15,
}}
>
<Button
intent={Intent.WARNING}
className={Classes.POPOVER_DISMISS}
onClick={handleRecalcConfirmBtn}
small
>
Calculate
</Button>
<Button
className={Classes.POPOVER_DISMISS}
style={{ marginRight: 10 }}
onClick={handleCancelBtn}
small
minimal
>
Cancel
</Button>
</div>
</PopoverContent>
);
return (
<FFormGroup inline={true} {...formGroupProps} name={name}>
<ControlGroup>
<ExchangeRatePrepend>
<ExchangeFlagIcon currencyCode={fromCurrency} /> 1 {fromCurrency} =
</ExchangeRatePrepend>
<ExchangeRateField
allowDecimals={true}
allowNegativeValue={true}
{...inputGroupProps}
name={name}
/>
{withPopoverRecalcConfirm ? (
<Popover
isOpen={isOpen}
content={popoverConfirmContent}
position={Position.RIGHT}
>
{exchangeRateField}
</Popover>
) : (
exchangeRateField
)}
<ExchangeRateAppend>
<ExchangeFlagIcon currencyCode={toCurrency} /> {toCurrency}
</ExchangeRateAppend>
@@ -34,7 +163,7 @@ export function ExchangeRateInputGroup({
}
const ExchangeRateField = styled(FMoneyInputGroup)`
max-width: 75px;
max-width: 85px;
`;
const ExchangeRateSideIcon = styled.div`
@@ -57,3 +186,8 @@ const ExchangeFlagIcon = styled(FlagIcon)`
margin-left: 5px;
display: inline-block;
`;
const PopoverContent = styled('div')`
padding: 20px;
width: 300px;
`;

View File

@@ -35,7 +35,7 @@ function VendorsSelectRoot({
<FSelect
items={items}
textAccessor={'display_name'}
labelAccessor={'code'}
labelAccessor={'formatted_balance'}
valueAccessor={'id'}
popoverProps={{ minimal: true, usePortal: true, inline: false }}
createNewItemRenderer={maybeCreateNewItemRenderer}

View File

@@ -48,6 +48,7 @@ export enum DialogsName {
ProjectBillableEntriesForm = 'project-billable-entries',
InvoiceNumberSettings = 'InvoiceNumberSettings',
TaxRateForm = 'tax-rate-form',
InvoiceExchangeRateChangeNotice = 'InvoiceExchangeRateChangeNotice',
InvoiceMail = 'invoice-mail',
EstimateMail = 'estimate-mail',
ReceiptMail = 'receipt-mail',

View File

@@ -8,7 +8,7 @@ const AccountInactivateAlert = React.lazy(
() => import('@/containers/Alerts/Accounts/AccountInactivateAlert'),
);
const AccountActivateAlert = React.lazy(
() => import('@/containers/Alerts/Accounts/AccountDeleteAlert'),
() => import('@/containers/Alerts/Accounts/AccountActivateAlert'),
);
export default [

View File

@@ -32,7 +32,7 @@ function AccountsDataTable({
// #withAlertsDialog
openAlert,
// #withDial
// #withDialog
openDialog,
// #withDrawerActions

View File

@@ -12,7 +12,6 @@ import PaymentMadesAlerts from '@/containers/Purchases/PaymentMades/PaymentMades
import CustomersAlerts from '@/containers/Customers/CustomersAlerts';
import VendorsAlerts from '@/containers/Vendors/VendorsAlerts';
import ManualJournalsAlerts from '@/containers/Accounting/JournalsLanding/ManualJournalsAlerts';
import ExchangeRatesAlerts from '@/containers/ExchangeRates/ExchangeRatesAlerts';
import ExpensesAlerts from '@/containers/Expenses/ExpensesAlerts';
import AccountTransactionsAlerts from '@/containers/CashFlow/AccountTransactions/AccountTransactionsAlerts';
import UsersAlerts from '@/containers/Preferences/Users/UsersAlerts';
@@ -41,7 +40,6 @@ export default [
...CustomersAlerts,
...VendorsAlerts,
...ManualJournalsAlerts,
...ExchangeRatesAlerts,
...ExpensesAlerts,
...AccountTransactionsAlerts,
...UsersAlerts,
@@ -54,5 +52,5 @@ export default [
...WarehousesTransfersAlerts,
...BranchesAlerts,
...ProjectAlerts,
...TaxRatesAlerts
...TaxRatesAlerts,
];

View File

@@ -8,9 +8,9 @@ import { TransitionGroup, CSSTransition } from 'react-transition-group';
import authenticationRoutes from '@/routes/authentication';
import { Icon, FormattedMessage as T } from '@/components';
import { useIsAuthenticated } from '@/hooks/state';
import { AuthMetaBootProvider } from './AuthMetaBoot';
import '@/style/pages/Authentication/Auth.scss';
import { AuthMetaBootProvider } from './AuthMetaBoot';
export function Authentication() {
const to = { pathname: '/' };

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { MoneyInDialogProvider } from './MoneyInDialogProvider';
import MoneyInForm from './MoneyInForm';

View File

@@ -72,7 +72,7 @@ function MoneyInForm({
};
// Handles the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
const handleFormSubmit = (values, { setSubmitting }) => {
const form = {
...omit(values, ['currency_code']),
publish: true,

View File

@@ -51,7 +51,6 @@ function MoneyOutForm({
accountId,
accountType,
createCashflowTransactionMutate,
submitPayload,
} = useMoneyOutDialogContext();
// transaction number.

View File

@@ -14,6 +14,7 @@ import { useUpdateEffect } from '@/hooks';
import withSettings from '@/containers/Settings/withSettings';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { transactionNumber } from '@/utils';
/**
* Syncs cashflow auto-increment settings to the form once update.

View File

@@ -1,19 +0,0 @@
// @ts-nocheck
import * as Yup from 'yup';
import intl from 'react-intl-universal';
import { DATATYPES_LENGTH } from '@/constants/dataTypes';
const Schema = Yup.object().shape({
exchange_rate: Yup.number()
.required()
.label(intl.get('exchange_rate_')),
currency_code: Yup.string()
.max(3)
.required(intl.get('currency_code_')),
date: Yup.date()
.required()
.label(intl.get('date')),
});
export const CreateExchangeRateFormSchema = Schema;
export const EditExchangeRateFormSchema = Schema;

View File

@@ -1,114 +0,0 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import moment from 'moment';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { AppToaster } from '@/components';
import {
CreateExchangeRateFormSchema,
EditExchangeRateFormSchema,
} from './ExchangeRateForm.schema';
import ExchangeRateFormContent from './ExchangeRateFormContent';
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose, transformToForm } from '@/utils';
const defaultInitialValues = {
exchange_rate: '',
currency_code: '',
date: moment(new Date()).format('YYYY-MM-DD'),
};
/**
* Exchange rate form.
*/
function ExchangeRateForm({
// #withDialogActions
closeDialog,
}) {
const {
createExchangeRateMutate,
editExchangeRateMutate,
isNewMode,
dialogName,
exchangeRate,
} = useExchangeRateFromContext();
// Form validation schema in create and edit mode.
const validationSchema = isNewMode
? CreateExchangeRateFormSchema
: EditExchangeRateFormSchema;
const initialValues = useMemo(
() => ({
...defaultInitialValues,
...transformToForm(exchangeRate, defaultInitialValues),
}),
[],
);
// Transformers response errors.
const transformErrors = (errors, { setErrors }) => {
if (
errors.find((error) => error.type === 'EXCHANGE.RATE.DATE.PERIOD.DEFINED')
) {
setErrors({
exchange_rate: intl.get(
'there_is_exchange_rate_in_this_date_with_the_same_currency',
),
});
}
};
// Handle the form submit.
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Handle close the dialog after success response.
const afterSubmit = () => {
closeDialog(dialogName);
};
const onSuccess = ({ response }) => {
AppToaster.show({
message: intl.get(
!isNewMode
? 'the_exchange_rate_has_been_edited_successfully'
: 'the_exchange_rate_has_been_created_successfully',
),
intent: Intent.SUCCESS,
});
afterSubmit(response);
};
// Handle the response error.
const onError = (error) => {
const {
response: {
data: { errors },
},
} = error;
transformErrors(errors, { setErrors });
setSubmitting(false);
};
if (isNewMode) {
createExchangeRateMutate(values).then(onSuccess).catch(onError);
} else {
editExchangeRateMutate([exchangeRate.id, values])
.then(onSuccess)
.catch(onError);
}
};
return (
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleFormSubmit}
>
<ExchangeRateFormContent />
</Formik>
);
}
export default compose(withDialogActions)(ExchangeRateForm);

View File

@@ -1,14 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Form } from 'formik';
import ExchangeRateFormFields from './ExchangeRateFormFields';
import ExchangeRateFormFooter from './ExchangeRateFormFooter';
export default function ExchangeRateFormContent() {
return (
<Form>
<ExchangeRateFormFields />
<ExchangeRateFormFooter />
</Form>
);
}

View File

@@ -1,27 +0,0 @@
// @ts-nocheck
import React from 'react';
import ExchangeRateForm from './ExchangeRateForm';
import { ExchangeRateFormProvider } from './ExchangeRateFormProvider';
import '@/style/pages/ExchangeRate/ExchangeRateDialog.scss';
/**
* Exchange rate form content.
*/
export default function ExchangeRateFormDialogContent({
// #ownProp
action,
exchangeRateId,
dialogName,
}) {
return (
<ExchangeRateFormProvider
dialogName={dialogName}
exchangeRate={exchangeRateId}
action={action}
>
<ExchangeRateForm />
</ExchangeRateFormProvider>
);
}

View File

@@ -1,89 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Classes, FormGroup, InputGroup, Position } from '@blueprintjs/core';
import { FastField } from 'formik';
import { DateInput } from '@blueprintjs/datetime';
import { FormattedMessage as T } from '@/components';
import classNames from 'classnames';
import {
momentFormatter,
tansformDateValue,
handleDateChange,
inputIntent,
} from '@/utils';
import {
ErrorMessage,
FieldRequiredHint,
CurrencySelectList,
} from '@/components';
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
export default function ExchangeRateFormFields() {
const { action, currencies } = useExchangeRateFromContext();
return (
<div className={Classes.DIALOG_BODY}>
{/* ----------- Date ----------- */}
<FastField name={'date'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'date'} />}
labelInfo={FieldRequiredHint}
className={classNames('form-group--select-list', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="date" />}
inline={true}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={tansformDateValue(value)}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('date', formattedDate);
})}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
disabled={action === 'edit'}
/>
</FormGroup>
)}
</FastField>
{/* ----------- Currency Code ----------- */}
<FastField name={'currency_code'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'currency_code'} />}
labelInfo={<FieldRequiredHint />}
className={classNames('form-group--currency', Classes.FILL)}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="currency_code" />}
inline={true}
>
<CurrencySelectList
currenciesList={currencies}
selectedCurrencyCode={value}
onCurrencySelected={({ currency_code }) => {
form.setFieldValue('currency_code', currency_code);
}}
disabled={action === 'edit'}
/>
</FormGroup>
)}
</FastField>
{/*------------ Exchange Rate -----------*/}
<FastField name={'exchange_rate'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'exchange_rate'} />}
labelInfo={<FieldRequiredHint />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="exchange_rate" />}
inline={true}
>
<InputGroup intent={inputIntent({ error, touched })} {...field} />
</FormGroup>
)}
</FastField>
</div>
);
}

View File

@@ -1,36 +0,0 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from '@/components';
import { useExchangeRateFromContext } from './ExchangeRateFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
function ExchangeRateFormFooter({
// #withDialogActions
closeDialog,
}) {
const { isSubmitting } = useFormikContext();
const { dialogName, action } = useExchangeRateFromContext();
const handleClose = () => {
closeDialog(dialogName);
};
return (
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}>
<T id={'close'} />
</Button>
<Button intent={Intent.PRIMARY} type="submit" disabled={isSubmitting}>
{action === 'edit' ? <T id={'edit'} /> : <T id={'submit'} />}
</Button>
</div>
</div>
);
}
export default compose(withDialogActions)(ExchangeRateFormFooter);

View File

@@ -1,53 +0,0 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import {
useCreateExchangeRate,
useEdiExchangeRate,
useCurrencies,
useExchangeRates,
} from '@/hooks/query';
import { DialogContent } from '@/components';
const ExchangeRateFormContext = createContext();
/**
* Exchange rate Form page provider.
*/
function ExchangeRateFormProvider({
exchangeRate,
action,
dialogName,
...props
}) {
// Create and edit exchange rate mutations.
const { mutateAsync: createExchangeRateMutate } = useCreateExchangeRate();
const { mutateAsync: editExchangeRateMutate } = useEdiExchangeRate();
// Load Currencies list.
const { data: currencies, isFetching: isCurrenciesLoading } = useCurrencies();
const { isFetching: isExchangeRatesLoading } = useExchangeRates();
const isNewMode = !exchangeRate;
// Provider state.
const provider = {
createExchangeRateMutate,
editExchangeRateMutate,
dialogName,
exchangeRate,
action,
currencies,
isExchangeRatesLoading,
isNewMode,
};
return (
<DialogContent isLoading={isCurrenciesLoading} name={'exchange-rate-form'}>
<ExchangeRateFormContext.Provider value={provider} {...props} />
</DialogContent>
);
}
const useExchangeRateFromContext = () => useContext(ExchangeRateFormContext);
export { ExchangeRateFormProvider, useExchangeRateFromContext };

View File

@@ -1,45 +0,0 @@
// @ts-nocheck
import React, { lazy } from 'react';
import { Dialog, DialogSuspense, FormattedMessage as T } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const ExchangeRateFormDialogContent = lazy(
() => import('./ExchangeRateFormDialogContent'),
);
/**
* Exchange rate form dialog.
*/
function ExchangeRateFormDialog({
dialogName,
payload = { action: '', id: null, exchangeRate: '' },
isOpen,
}) {
return (
<Dialog
name={dialogName}
title={
payload.action === 'edit' ? (
<T id={'edit_exchange_rate'} />
) : (
<T id={'new_exchange_rate'} />
)
}
className={'dialog--exchangeRate-form'}
isOpen={isOpen}
autoFocus={true}
canEscapeKeyClose={true}
>
<DialogSuspense>
<ExchangeRateFormDialogContent
dialogName={dialogName}
action={payload.action}
exchangeRateId={payload.exchangeRate}
/>
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(ExchangeRateFormDialog);

View File

@@ -9,11 +9,11 @@ import ReferenceNumberForm from '@/containers/JournalNumber/ReferenceNumberForm'
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { compose } from '@/utils';
import {
transformFormToSettings,
transformSettingsToForm,
} from '@/containers/JournalNumber/utils';
import { compose } from '@/utils';
/**
* Transaction number dialog content.

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { useHistory } from 'react-router-dom';
import {
@@ -22,6 +21,7 @@ import {
Icon,
FormattedMessage as T,
Can,
Choose,
} from '@/components';
import { compose } from '@/utils';
@@ -52,7 +52,6 @@ function EstimateDetailActionsBar({
history.push(`/estimates/${estimateId}/edit`);
closeDrawer(DRAWERS.ESTIMATE_DETAILS);
};
// Handle delete sale estimate.
const handleDeleteEstimate = () => {
openAlert('estimate-delete', { estimateId });
@@ -83,6 +82,7 @@ function EstimateDetailActionsBar({
/>
<NavbarDivider />
</Can>
<Can I={SaleEstimateAction.View} a={AbilitySubject.Estimate}>
<Button
className={Classes.MINIMAL}

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import {
Intent,
Button,
@@ -9,9 +8,15 @@ import {
MenuItem,
Menu,
Tag,
MenuDivider,
Classes,
} from '@blueprintjs/core';
import * as R from 'ramda';
import { Icon, T, Choose } from '@/components';
import { Icon, T, Choose, Can } from '@/components';
import { AbilitySubject, SaleEstimateAction } from '@/constants/abilityOption';
import withAlertsActions from '@/containers/Alert/withAlertActions';
import { useEstimateDetailDrawerContext } from './EstimateDetailDrawerProvider';
/**
* Estimate details status.
@@ -49,25 +54,85 @@ export function EstimateDetailsStatus({ estimate }) {
);
}
export function EstimateMoreMenuItems({ payload: { onNotifyViaSMS } }) {
return (
<Popover
minimal={true}
content={
<Menu>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
</Menu>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
modifiers={{
offset: { offset: '0, 4' },
}}
>
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
</Popover>
);
}
export const EstimateMoreMenuItems = R.compose(withAlertsActions)(
({
// # withAlertsActions,
openAlert,
// # rest
payload: { onNotifyViaSMS },
}) => {
const { estimateId, estimate } = useEstimateDetailDrawerContext();
// Handle cancel/confirm estimate approve.
const handleApproveEstimate = () => {
openAlert('estimate-Approve', { estimateId });
};
// Handle cancel/confirm estimate reject.
const handleRejectEstimate = () => {
openAlert('estimate-reject', { estimateId });
};
return (
<Popover
minimal={true}
content={
<Menu>
<MenuItem
onClick={onNotifyViaSMS}
text={<T id={'notify_via_sms.dialog.notify_via_sms'} />}
/>
<MenuDivider />
<Choose>
<Choose.When
condition={estimate.is_delivered && estimate.is_rejected}
>
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
<MenuItem
className={Classes.MINIMAL}
text={<T id={'mark_as_approved'} />}
onClick={handleApproveEstimate}
/>
</Can>
</Choose.When>
<Choose.When
condition={estimate.is_delivered && estimate.is_approved}
>
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
<MenuItem
className={Classes.MINIMAL}
text={<T id={'mark_as_rejected'} />}
onClick={handleRejectEstimate}
/>
</Can>
</Choose.When>
<Choose.When condition={estimate.is_delivered}>
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
<MenuItem
className={Classes.MINIMAL}
text={<T id={'mark_as_approved'} />}
onClick={handleApproveEstimate}
/>
</Can>
<Can I={SaleEstimateAction.Edit} a={AbilitySubject.Estimate}>
<MenuItem
className={Classes.MINIMAL}
text={<T id={'mark_as_rejected'} />}
onClick={handleRejectEstimate}
/>
</Can>
</Choose.When>
</Choose>
</Menu>
}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
modifiers={{
offset: { offset: '0, 4' },
}}
>
<Button icon={<Icon icon="more-vert" iconSize={16} />} minimal={true} />
</Popover>
);
},
);

View File

@@ -0,0 +1,52 @@
import { useExchangeRate } from '@/hooks/query';
import { useCurrentOrganization } from '@/hooks/state';
import React from 'react';
interface AutoExchangeRateProviderProps {
children: React.ReactNode;
}
interface AutoExchangeRateProviderValue {
autoExRateCurrency: string;
isAutoExchangeRateLoading: boolean;
}
const AutoExchangeRateContext = React.createContext(
{} as AutoExchangeRateProviderValue,
);
function AutoExchangeRateProvider({ children }: AutoExchangeRateProviderProps) {
const [autoExRateCurrency, setAutoExRateCurrency] =
React.useState<string>('');
const currentOrganization = useCurrentOrganization();
// Retrieves the exchange rate.
const { data: autoExchangeRate, isLoading: isAutoExchangeRateLoading } =
useExchangeRate(autoExRateCurrency, currentOrganization.base_currency, {
enabled: Boolean(currentOrganization.base_currency && autoExRateCurrency),
refetchOnWindowFocus: false,
staleTime: 0,
cacheTime: 0,
});
const value = {
autoExRateCurrency,
setAutoExRateCurrency,
isAutoExchangeRateLoading,
autoExchangeRate,
};
return (
<AutoExchangeRateContext.Provider value={value}>
{children}
</AutoExchangeRateContext.Provider>
);
}
const useAutoExRateContext = () => React.useContext(AutoExchangeRateContext);
export {
useAutoExRateContext,
AutoExchangeRateContext,
AutoExchangeRateProvider,
};

View File

@@ -0,0 +1,88 @@
// @ts-nocheck
import React from 'react';
import { useFormikContext } from 'formik';
import { round } from 'lodash';
import * as R from 'ramda';
import { updateItemsEntriesTotal } from './utils';
/**
* Convert the given rate to the local currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const convertToForeignCurrency = (
rate: number,
exchangeRate: number,
) => {
return rate * exchangeRate;
};
/**
* Converts the given rate to the base currency.
* @param {number} rate
* @param {number} exchangeRate
* @returns {number}
*/
export const covertToBaseCurrency = (rate: number, exchangeRate: number) => {
return rate / exchangeRate;
};
/**
* Reverts the given rate from the old exchange rate and covert it to the new
* currency based on the given new exchange rate.
* @param {number} rate -
* @param {number} oldExchangeRate - Old exchange rate.
* @param {number} newExchangeRate - New exchange rate.
* @returns {number}
*/
const revertAndConvertExchangeRate = (
rate: number,
oldExchangeRate: number,
newExchangeRate: number,
) => {
const oldValue = convertToForeignCurrency(rate, oldExchangeRate);
const newValue = covertToBaseCurrency(oldValue, newExchangeRate);
return round(newValue, 3);
};
/**
* Assign the new item entry rate after converting to the new exchange rate.
* @params {number} oldExchangeRate -
* @params {number} newExchangeRate -
* @params {IItemEntry} entries -
*/
const assignRateRevertAndCovertExchangeRate = R.curry(
(oldExchangeRate: number, newExchangeRate: number, entries: IITemEntry[]) => {
return entries.map((entry) => ({
...entry,
rate: revertAndConvertExchangeRate(
entry.rate,
oldExchangeRate,
newExchangeRate,
),
}));
},
);
/**
* Updates items entries on exchange rate change.
* @returns {(oldExchangeRate: number, newExchangeRate: number) => IItemEntry[]}
*/
export const useUpdateEntriesOnExchangeRateChange = () => {
const {
values: { entries },
} = useFormikContext();
return React.useMemo(() => {
return R.curry((oldExchangeRate: number, newExchangeRate: number) => {
return R.compose(
// Updates entries total.
updateItemsEntriesTotal,
// Assign a new rate of the given new exchange rate from the old exchange rate.
assignRateRevertAndCovertExchangeRate(oldExchangeRate, newExchangeRate),
)(entries);
});
}, [entries]);
};

View File

@@ -0,0 +1,121 @@
// @ts-nocheck
import { useFormikContext } from 'formik';
import { useUpdateEntriesOnExchangeRateChange } from './useUpdateEntriesOnExchangeRateChange';
import { useAutoExRateContext } from './AutoExchangeProvider';
import { useCallback, useEffect } from 'react';
import { useCurrentOrganization } from '@/hooks/state';
/**
* Re-calculate the item entries prices based on the old exchange rate.
* @param {InvoiceExchangeRateInputFieldRoot} Component
* @returns {JSX.Element}
*/
export const withExchangeRateItemEntriesPriceRecalc =
(Component) => (props) => {
const { setFieldValue } = useFormikContext();
const updateChangeExRate = useUpdateEntriesOnExchangeRateChange();
return (
<Component
onRecalcConfirm={({ exchangeRate, oldExchangeRate }) => {
setFieldValue(
'entries',
updateChangeExRate(oldExchangeRate, exchangeRate),
);
}}
{...props}
/>
);
};
/**
* Injects the loading props to the exchange rate field.
* @param Component
* @returns {}
*/
export const withExchangeRateFetchingLoading = (Component) => (props) => {
const { isAutoExchangeRateLoading } = useAutoExRateContext();
return (
<Component
isLoading={isAutoExchangeRateLoading}
inputGroupProps={{
disabled: isAutoExchangeRateLoading,
}}
/>
);
};
/**
* Updates the customer currency code and exchange rate once you update the customer
* then change the state to fetch the realtime exchange rate of the new selected currency.
*/
export const useCustomerUpdateExRate = () => {
const { setFieldValue, values } = useFormikContext();
const { setAutoExRateCurrency } = useAutoExRateContext();
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
const currentCompany = useCurrentOrganization();
const DEFAULT_EX_RATE = 1;
return useCallback(
(customer) => {
// Reset the auto exchange rate currency cycle.
setAutoExRateCurrency(null);
// If the customer's currency code equals the same base currency.
if (customer.currency_code === currentCompany.base_currency) {
setFieldValue('exchange_rate', DEFAULT_EX_RATE + '');
setFieldValue(
'entries',
updateEntriesOnExChange(values.exchange_rate, DEFAULT_EX_RATE),
);
} else {
// Sets the currency code to fetch exchange rate of the given currency code.
setAutoExRateCurrency(customer?.currency_code);
}
},
[
currentCompany.base_currency,
setAutoExRateCurrency,
setFieldValue,
updateEntriesOnExChange,
values.exchange_rate,
],
);
};
interface UseSyncExRateToFormProps {
onSynced?: () => void;
}
/**
* Syncs the realtime exchange rate to the Formik form and then re-calculates
* the entries rate based on the given new and old ex. rate.
* @param {UseSyncExRateToFormProps} props -
* @returns {React.ReactNode}
*/
export const useSyncExRateToForm = ({ onSynced }: UseSyncExRateToFormProps) => {
const { setFieldValue, values } = useFormikContext();
const { autoExRateCurrency, autoExchangeRate } = useAutoExRateContext();
const updateEntriesOnExChange = useUpdateEntriesOnExchangeRateChange();
// Sync the fetched real-time exchanage rate to the form.
useEffect(() => {
if (autoExchangeRate?.exchange_rate && autoExRateCurrency) {
setFieldValue('exchange_rate', autoExchangeRate?.exchange_rate + '');
setFieldValue(
'entries',
updateEntriesOnExChange(
values.exchange_rate,
autoExchangeRate?.exchange_rate,
),
);
onSynced?.();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoExchangeRate?.exchange_rate, autoExRateCurrency]);
return null;
};

View File

@@ -1,147 +0,0 @@
// @ts-nocheck
import React, { useCallback, useState, useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Intent,
Popover,
Position,
PopoverInteractionKind,
Alignment,
} from '@blueprintjs/core';
import {
Icon,
If,
DashboardActionsBar,
FormattedMessage as T,
} from '@/components';
import { connect } from 'react-redux';
import { useRefreshExchangeRate } from '@/hooks/query/exchangeRates';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withResourceDetail from '@/containers/Resources/withResourceDetails';
import withExchangeRatesActions from './withExchangeRatesActions';
import { compose } from '@/utils';
/**
* Exchange rate actions bar.
*/
function ExchangeRateActionsBar({
// #withDialogActions.
openDialog,
// #withResourceDetail
resourceFields,
//#withExchangeRatesActions
addExchangeRatesTableQueries,
// #ownProps
selectedRows = [],
onDeleteExchangeRate,
onFilterChanged,
onBulkDelete,
}) {
const [filterCount, setFilterCount] = useState(0);
const onClickNewExchangeRate = () => {
openDialog('exchangeRate-form', {});
};
// Exchange rates refresh action.
const { refresh } = useRefreshExchangeRate();
// Handle click a refresh sale estimates
const handleRefreshBtnClick = () => {
refresh();
};
const hasSelectedRows = useMemo(
() => selectedRows.length > 0,
[selectedRows],
);
const handelBulkDelete = useCallback(() => {
onBulkDelete && onBulkDelete(selectedRows.map((r) => r.id));
}, [onBulkDelete, selectedRows]);
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="plus" />}
text={<T id={'new_exchange_rate'} />}
onClick={onClickNewExchangeRate}
/>
<NavbarDivider />
<Popover
minimal={true}
// content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={
filterCount <= 0 ? (
<T id={'filter'} />
) : (
`${filterCount} ${intl.get('filters_applied')}`
)
}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<If condition={hasSelectedRows}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="trash-16" iconSize={16} />}
text={<T id={'delete'} />}
intent={Intent.DANGER}
onClick={handelBulkDelete}
/>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
<NavbarGroup align={Alignment.RIGHT}>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="refresh-16" iconSize={14} />}
onClick={handleRefreshBtnClick}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
const mapStateToProps = (state, props) => ({
resourceName: '',
});
const withExchangeRateActionBar = connect(mapStateToProps);
export default compose(
withExchangeRateActionBar,
withDialogActions,
withResourceDetail(({ resourceFields }) => ({
resourceFields,
})),
withExchangeRatesActions,
)(ExchangeRateActionsBar);

View File

@@ -1,110 +0,0 @@
// @ts-nocheck
import React, { useCallback } from 'react';
import {
DataTable,
TableSkeletonRows,
TableSkeletonHeader,
} from '@/components';
import { useExchangeRatesContext } from './ExchangeRatesProvider';
import { useExchangeRatesTableColumns, ActionMenuList } from './components';
import withExchangeRates from './withExchangeRates';
import withExchangeRatesActions from './withExchangeRatesActions';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import withAlertActions from '@/containers/Alert/withAlertActions';
import { compose } from '@/utils';
/**
* Exchange rates table.
*/
function ExchangeRateTable({
// #ownProps
tableProps,
// #withDialogActions.
openDialog,
// #withAlertActions
openAlert,
// #withExchangeRatesActions
setExchangeRateTableState,
// #withExchangeRates
exchangeRatesTableState,
}) {
const {
isExchangeRatesFetching,
isExchangeRatesLoading,
exchangesRates,
pagination,
} = useExchangeRatesContext();
// Table columns.
const columns = useExchangeRatesTableColumns();
// Handle delete exchange rate.
const handleDeleteExchangeRate = ({ id }) => {
openAlert('exchange-rate-delete', { exchangeRateId: id });
};
// Handle Edit exchange rate.
const handelEditExchangeRate = (exchangeRate) => {
openDialog('exchangeRate-form', {
action: 'edit',
exchangeRate: exchangeRate,
});
};
const handleFetchData = useCallback(
({ pageSize, pageIndex, sortBy }) => {
setExchangeRateTableState({
pageIndex,
pageSize,
sortBy,
});
},
[setExchangeRateTableState],
);
return (
<DataTable
noInitialFetch={true}
columns={columns}
data={exchangesRates}
initialState={exchangeRatesTableState}
loading={isExchangeRatesLoading}
headerLoading={isExchangeRatesLoading}
progressBarLoading={isExchangeRatesFetching}
selectionColumn={true}
expandable={true}
sticky={true}
manualSortBy={true}
onFetchData={handleFetchData}
pagination={true}
manualPagination={true}
pagesCount={pagination.pagesCount}
TableLoadingRenderer={TableSkeletonRows}
TableHeaderSkeletonRenderer={TableSkeletonHeader}
ContextMenu={ActionMenuList}
payload={{
onDeleteExchangeRate: handleDeleteExchangeRate,
onEditExchangeRate: handelEditExchangeRate,
}}
{...tableProps}
/>
);
}
export default compose(
withDialogActions,
withAlertActions,
withExchangeRates(({ exchangeRatesTableState }) => ({
exchangeRatesTableState,
})),
withExchangeRatesActions,
)(ExchangeRateTable);

View File

@@ -1,10 +0,0 @@
// @ts-nocheck
import React from 'react';
const ExchangeRateDeleteAlert = React.lazy(
() => import('@/containers/Alerts/ExchangeRates/ExchangeRateDeleteAlert'),
);
export default [
{ name: 'exchange-rate-delete', component: ExchangeRateDeleteAlert },
];

View File

@@ -1,38 +0,0 @@
// @ts-nocheck
import React from 'react';
import { DashboardContentTable, DashboardPageContent } from '@/components';
import ExchangeRateTable from './ExchangeRateTable';
import ExchangeRateActionsBar from './ExchangeRateActionsBar';
import { ExchangeRatesProvider } from './ExchangeRatesProvider';
import { transformTableStateToQuery, compose } from '@/utils';
import withExchangeRates from './withExchangeRates';
/**
* Exchange Rates list.
*/
function ExchangeRatesList({
// #withExchangeRates
exchangeRatesTableState,
}) {
return (
<ExchangeRatesProvider
query={transformTableStateToQuery(exchangeRatesTableState)}
>
<ExchangeRateActionsBar />
<DashboardPageContent>
<DashboardContentTable>
<ExchangeRateTable />
</DashboardContentTable>
</DashboardPageContent>
</ExchangeRatesProvider>
);
}
export default compose(
withExchangeRates(({ exchangeRatesTableState }) => ({
exchangeRatesTableState,
})),
)(ExchangeRatesList);

View File

@@ -1,42 +0,0 @@
// @ts-nocheck
import React, { createContext } from 'react';
import { transformTableQueryToParams } from '@/utils';
import { DashboardInsider } from '@/components';
import { useExchangeRates } from '@/hooks/query';
const ExchangesRatesContext = createContext();
/**
* Exchanges rates list provider.
*/
function ExchangeRatesProvider({ query, ...props }) {
const {
data: { exchangesRates, pagination, filterMeta },
isFetching: isExchangeRatesFetching,
isLoading: isExchangeRatesLoading,
} = useExchangeRates(
{
...transformTableQueryToParams(query),
},
{ keepPreviousData: true },
);
const state = {
isExchangeRatesFetching,
isExchangeRatesLoading,
exchangesRates,
pagination,
};
return (
<DashboardInsider name={'exchange-rate'}>
<ExchangesRatesContext.Provider value={state} {...props} />
</DashboardInsider>
);
}
const useExchangeRatesContext = () => React.useContext(ExchangesRatesContext);
export { ExchangeRatesProvider, useExchangeRatesContext };

View File

@@ -1,91 +0,0 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import moment from 'moment';
import intl from 'react-intl-universal';
import {
Menu,
Popover,
Button,
Position,
MenuItem,
MenuDivider,
Intent,
} from '@blueprintjs/core';
import { Icon, Money } from '@/components';
import { safeCallback } from '@/utils';
/**
* Row actions menu list.
*/
export function ActionMenuList({
row: { original },
payload: { onEditExchangeRate, onDeleteExchangeRate },
}) {
return (
<Menu>
<MenuItem
icon={<Icon icon="pen-18" />}
text={intl.get('edit_exchange_rate')}
onClick={safeCallback(onEditExchangeRate, original)}
/>
<MenuDivider />
<MenuItem
text={intl.get('delete_exchange_rate')}
intent={Intent.DANGER}
onClick={safeCallback(onDeleteExchangeRate, original)}
icon={<Icon icon="trash-16" iconSize={16} />}
/>
</Menu>
);
}
/**
* Table actions cell.
*/
export function TableActionsCell(props) {
return (
<Popover
content={<ActionMenuList {...props} />}
position={Position.RIGHT_TOP}
>
<Button icon={<Icon icon="more-h-16" iconSize={16} />} />
</Popover>
);
}
export function useExchangeRatesTableColumns() {
return useMemo(
() => [
{
id: 'date',
Header: intl.get('date'),
accessor: (r) => moment(r.date).format('YYYY MMM DD'),
width: 150,
},
{
id: 'currency_code',
Header: intl.get('currency_code'),
accessor: 'currency_code',
className: 'currency_code',
width: 150,
},
{
id: 'exchange_rate',
Header: intl.get('exchange_rate'),
accessor: (r) => (
<Money amount={r.exchange_rate} currency={r.currency_code} />
),
className: 'exchange_rate',
width: 150,
},
{
id: 'actions',
Header: '',
Cell: TableActionsCell,
className: 'actions',
width: 50,
},
],
[],
);
}

View File

@@ -1,9 +0,0 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { getExchangeRateById } from '@/store/ExchangeRate/exchange.selector';
const mapStateToProps = (state, props) => ({
exchangeRate: getExchangeRateById(state, props),
});
export default connect(mapStateToProps);

View File

@@ -1,16 +0,0 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { getExchangeRatesTableStateFactory } from '@/store/ExchangeRate/exchange.selector';
export default (mapState) => {
const getExchangeRatesTableState = getExchangeRatesTableStateFactory();
const mapStateToProps = (state, props) => {
const mapped = {
exchangeRatesTableState: getExchangeRatesTableState(state, props),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -1,10 +0,0 @@
// @ts-nocheck
import { connect } from 'react-redux';
import { setExchangeRateTableState } from '@/store/ExchangeRate/exchange.actions';
export const mapDispatchToProps = (dispatch) => ({
setExchangeRateTableState: (queries) =>
dispatch(setExchangeRateTableState(queries)),
});
export default connect(null, mapDispatchToProps);

View File

@@ -12,6 +12,7 @@ import {
import classNames from 'classnames';
import { DashboardActionsBar, FormattedMessage as T, Icon } from '@/components';
import { GeneralLedgerSheetExportMenu } from './components';
import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { compose } from '@/utils';
@@ -84,11 +85,18 @@ function GeneralLedgerActionsBar({
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Popover
content={<GeneralLedgerSheetExportMenu />}
interactionKind={PopoverInteractionKind.CLICK}
placement="bottom-start"
minimal
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</Popover>
</NavbarGroup>
</DashboardActionsBar>
);

View File

@@ -10,7 +10,7 @@ const GeneralLedgerHeaderDimensionsPanelContext = React.createContext();
/**
* General Ledger Header Dimensions Panel provider.
* @returns
* @returns {JSX.Element}
*/
function GeneralLedgerHeaderDimensionsPanelProvider({ query, ...props }) {
// Features guard.

View File

@@ -29,6 +29,7 @@ function GeneralLedgerProvider({ query, ...props }) {
sheetRefresh: refetch,
isFetching,
isLoading,
httpRequest: requestQuery
};
return (
<FinancialReportPage name={'general-ledger-sheet'}>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import { useMemo } from 'react';
import intl from 'react-intl-universal';
import styled from 'styled-components';
@@ -13,7 +13,7 @@ import {
} from '@/components';
import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { useGeneralLedgerTableColumns } from './components';
import { useGeneralLedgerTableColumns } from './dynamicColumns';
/**
* General ledger table.
@@ -21,7 +21,7 @@ import { useGeneralLedgerTableColumns } from './components';
export default function GeneralLedgerTable({ companyName }) {
// General ledger context.
const {
generalLedger: { tableRows, query },
generalLedger: { query, table },
isLoading,
} = useGeneralLedgerContext();
@@ -30,8 +30,8 @@ export default function GeneralLedgerTable({ companyName }) {
// Default expanded rows of general ledger table.
const expandedRows = useMemo(
() => defaultExpanderReducer(tableRows, 1),
[tableRows],
() => defaultExpanderReducer(table.rows, 1),
[table.rows],
);
return (
@@ -48,7 +48,7 @@ export default function GeneralLedgerTable({ companyName }) {
'this_report_does_not_contain_any_data_between_date_period',
)}
columns={columns}
data={tableRows}
data={table.rows}
rowClassNames={tableRowTypesToClassnames}
expanded={expandedRows}
virtualizedRows={true}
@@ -79,23 +79,20 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
opacity: 0;
}
}
.tr:not(.no-results) .td:not(:first-of-type) {
border-left: 1px solid #ececec;
}
.tr:last-child .td {
border-bottom: 1px solid #ececec;
}
.tr.row_type {
&--ACCOUNT_ROW {
&--ACCOUNT {
.td {
&.date {
font-weight: 500;
.cell-inner {
white-space: nowrap;
position: relative;
position: absolute;
}
}
}
@@ -103,7 +100,6 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
border-top: 1px solid #ddd;
}
}
&--OPENING_BALANCE,
&--CLOSING_BALANCE {
.amount {

View File

@@ -1,107 +1,31 @@
// @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Button } from '@blueprintjs/core';
import { FormattedMessage as T, Icon, If } from '@/components';
import React, { useRef } from 'react';
import classNames from 'classnames';
import {
Button,
Classes,
Intent,
Menu,
MenuItem,
ProgressBar,
Text,
} from '@blueprintjs/core';
import {
FormattedMessage as T,
Icon,
If,
Stack,
AppToaster,
} from '@/components';
import { getColumnWidth } from '@/utils';
import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import FinancialLoadingBar from '../FinancialLoadingBar';
import { FinancialComputeAlert } from '../FinancialReportPage';
import { Align } from '@/constants';
/**
* Retrieve the general ledger table columns.
*/
export function useGeneralLedgerTableColumns() {
// General ledger context.
const {
generalLedger: { tableRows },
} = useGeneralLedgerContext();
return React.useMemo(
() => [
{
Header: intl.get('date'),
accessor: 'date',
className: 'date',
width: 120,
},
{
Header: intl.get('account_name'),
accessor: 'name',
className: 'name',
textOverview: true,
},
{
Header: intl.get('transaction_type'),
accessor: 'reference_type_formatted',
className: 'transaction_type',
width: 125,
textOverview: true,
},
{
Header: intl.get('transaction_number'),
accessor: 'reference_id',
className: 'transaction_number',
width: 100,
textOverview: true,
},
{
Header: intl.get('description'),
accessor: 'note',
className: 'description',
textOverview: true,
},
{
Header: intl.get('credit'),
accessor: 'formatted_credit',
className: 'credit',
width: getColumnWidth(tableRows, 'formatted_credit', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('debit'),
accessor: 'formatted_debit',
className: 'debit',
width: getColumnWidth(tableRows, 'formatted_debit', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('amount'),
accessor: 'formatted_amount',
className: 'amount',
width: getColumnWidth(tableRows, 'formatted_amount', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
{
Header: intl.get('running_balance'),
accessor: 'formatted_running_balance',
className: 'running_balance',
width: getColumnWidth(tableRows, 'formatted_running_balance', {
minWidth: 100,
magicSpacing: 10,
}),
textOverview: true,
align: Align.Right,
},
],
[tableRows],
);
}
import {
useGeneralLedgerSheetCsvExport,
useGeneralLedgerSheetXlsxExport,
} from '@/hooks/query';
/**
* General ledger sheet alerts.
@@ -144,3 +68,93 @@ export function GeneralLedgerSheetLoadingBar() {
</If>
);
}
/**
* Renders the G/L sheet export menu.
* @returns {JSX.Element}
*/
export const GeneralLedgerSheetExportMenu = () => {
const toastKey = useRef(null);
const commonToastConfig = {
isCloseButtonShown: true,
timeout: 2000,
};
const { httpRequest } = useGeneralLedgerContext();
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 } = useGeneralLedgerSheetXlsxExport(
httpRequest,
{
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 } = useGeneralLedgerSheetCsvExport(
httpRequest,
{
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,117 @@
// @ts-nocheck
import { getColumnWidth } from '@/utils';
import * as R from 'ramda';
import { useGeneralLedgerContext } from './GeneralLedgerProvider';
import { Align } from '@/constants';
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,
};
});
/**
* Date column accessor.
*/
const dateColumnAccessor = R.curry((column) => {
return {
...column,
width: 120,
};
});
/**
* Transaction type column accessor.
*/
const transactionTypeColumnAccessor = (column) => {
return {
...column,
width: 125,
};
};
/**
* Transaction number column accessor.
*/
const transactionIdColumnAccessor = (column) => {
return {
...column,
width: 80,
};
};
const dynamiColumnMapper = R.curry((data, column) => {
const _numericColumnAccessor = numericColumnAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'date'), dateColumnAccessor),
R.when(
R.pathEq(['key'], 'reference_type'),
transactionTypeColumnAccessor,
),
R.when(
R.pathEq(['key'], 'reference_number'),
transactionIdColumnAccessor,
),
R.when(R.pathEq(['key'], 'credit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'debit'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'amount'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'running_balance'), _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 G/L sheet table columns for table component.
*/
export const useGeneralLedgerTableColumns = () => {
const { generalLedger } = useGeneralLedgerContext();
if (!generalLedger) {
throw new Error('asdfadsf');
}
const { table } = generalLedger;
return dynamicColumns(table.rows, table.columns);
};

View File

@@ -19,6 +19,7 @@ import withInventoryValuationActions from './withInventoryValuationActions';
import { useInventoryValuationContext } from './InventoryValuationProvider';
import { compose, saveInvoke } from '@/utils';
import { InventoryValuationExportMenu } from './components';
function InventoryValuationActionsBar({
// #withInventoryValuation
@@ -109,11 +110,18 @@ function InventoryValuationActionsBar({
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Popover
content={<InventoryValuationExportMenu />}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
minimal
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</Popover>
</NavbarGroup>
</DashboardActionsBar>
);

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import FinancialReportPage from '../FinancialReportPage';
import { useInventoryValuation } from '@/hooks/query';
import { useInventoryValuationTable } from '@/hooks/query';
import { transformFilterFormToQuery } from '../common';
const InventoryValuationContext = React.createContext();
@@ -21,7 +21,7 @@ function InventoryValuationProvider({ query, ...props }) {
isFetching,
isLoading,
refetch,
} = useInventoryValuation(requestQuery, {
} = useInventoryValuationTable(requestQuery, {
keepPreviousData: true,
});

View File

@@ -8,23 +8,23 @@ import { ReportDataTable, FinancialSheet } from '@/components';
import { tableRowTypesToClassnames } from '@/utils';
import { useInventoryValuationContext } from './InventoryValuationProvider';
import { useInventoryValuationTableColumns } from './components';
import { useInventoryValuationColumns } from './dynamicColumns';
/**
* inventory valuation data table.
* Inventory valuation data table.
*/
export default function InventoryValuationTable({
//#ownProps
// #ownProps
companyName,
}) {
// inventory valuation context.
// Inventory valuation context.
const {
inventoryValuation: { tableRows, query },
inventoryValuation: { table, query },
isLoading,
} = useInventoryValuationContext();
// inventory valuation table columns.
const columns = useInventoryValuationTableColumns();
// Inventory valuation table columns.
const columns = useInventoryValuationColumns();
return (
<InventoryValuationSheet
@@ -35,7 +35,7 @@ export default function InventoryValuationTable({
>
<InventoryValuationDataTable
columns={columns}
data={tableRows}
data={table.rows}
expandable={true}
expandToggleColumn={1}
expandColumnSpace={1}
@@ -62,7 +62,7 @@ const InventoryValuationDataTable = styled(ReportDataTable)`
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
.tr.row_type--total .td {
.tr.row_type--TOTAL .td {
border-top: 1px solid #bbb;
font-weight: 500;
border-bottom: 3px double #000;

View File

@@ -1,13 +1,25 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import { useMemo, useRef } from 'react';
import intl from 'react-intl-universal';
import { If } from '@/components';
import classNames from 'classnames';
import { AppToaster, If, Stack } from '@/components';
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
import { CellTextSpan } from '@/components/Datatable/Cells';
import { useInventoryValuationContext } from './InventoryValuationProvider';
import FinancialLoadingBar from '../FinancialLoadingBar';
import {
Classes,
Intent,
Menu,
MenuItem,
ProgressBar,
Text,
} from '@blueprintjs/core';
import {
useInventoryValuationCsvExport,
useInventoryValuationXlsxExport,
} from '@/hooks/query';
/**
* Retrieve inventory valuation table columns.
@@ -77,3 +89,87 @@ export function InventoryValuationLoadingBar() {
</If>
);
}
/**
* Retrieves the inventory valuation sheet export menu.
* @returns {JSX.Element}
*/
export const InventoryValuationExportMenu = () => {
const toastKey = useRef(null);
const commonToastConfig = {
isCloseButtonShown: true,
timeout: 2000,
};
const { query } = useInventoryValuationContext();
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 } = useInventoryValuationXlsxExport(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 } = useInventoryValuationCsvExport(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,97 @@
// @ts-nocheck
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
import * as R from 'ramda';
import { useInventoryValuationContext } from './InventoryValuationProvider';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const getReportColWidth = (data, accessor, headerText) => {
return getColumnWidth(
data,
accessor,
{ magicSpacing: 10, minWidth: 100 },
headerText,
);
};
/**
* Common column mapper.
*/
const commonAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
key: column.key,
Header: column.label,
accessor,
className: column.key,
textOverview: true,
align: Align.Left,
};
});
/**
* 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: 240,
}
});
/**
* Dynamic column mapper.
* @param {} data -
* @param {} column -
*/
const dynamicColumnMapper = R.curry((data, column) => {
const _commonAccessor = commonAccessor(data);
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'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'valuation'), _numericColumnAccessor),
R.when(R.pathEq(['key'], 'average'), _numericColumnAccessor),
_commonAccessor,
)(column);
});
/**
* Composes the fetched dynamic columns from the server to the columns to pass it
* to the table component.
*/
export const dynamicColumns = (columns, data) => {
return R.map(dynamicColumnMapper(data), columns);
};
/**
* Retrieves the table columns of inventory valuation sheet.
*/
export const useInventoryValuationColumns = () => {
const { inventoryValuation } = useInventoryValuationContext();
if (!inventoryValuation) {
throw new Error('The inventory valuation is not loaded');
}
const { table } = inventoryValuation;
return dynamicColumns(table.columns, table.rows);
};

View File

@@ -18,6 +18,7 @@ import withJournal from './withJournal';
import { compose } from '@/utils';
import { useJournalSheetContext } from './JournalProvider';
import { JournalSheetExportMenu } from './components';
/**
* Journal sheeet - Actions bar.
@@ -85,11 +86,18 @@ function JournalActionsBar({
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
<Popover
content={<JournalSheetExportMenu />}
interactionKind={PopoverInteractionKind.CLICK}
placement="bottom-start"
minimal
>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</Popover>
</NavbarGroup>
</DashboardActionsBar>
);

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