Compare commits

...

174 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
c83e248648 Update README.md 2023-02-06 23:38:35 +02:00
a.bouhuolia
903fdc601a chore: update CHANGELOG.md 2023-02-06 22:06:44 +02:00
a.bouhuolia
d1a9b2aa00 chore: add CHANGLOG.md 2023-02-06 22:05:27 +02:00
a.bouhuolia
ff036b78b3 chore: add supported node version. 2023-02-06 21:50:08 +02:00
a.bouhuolia
b9f730e5b3 Merge branch 'lerna' of https://github.com/bigcapitalhq/client into lerna 2023-02-06 21:45:32 +02:00
a.bouhuolia
ec8b67452d feat: add README. 2023-02-06 21:45:24 +02:00
Ahmed Bouhuolia
37ab05768f Add files via upload 2023-02-05 22:47:21 +02:00
a.bouhuolia
995abbcc29 chore: change mono package scripts. 2023-02-03 15:14:30 +02:00
a.bouhuolia
32543b8762 fix: change root directory. 2023-02-03 15:13:16 +02:00
a.bouhuolia
80b97b5fdc add server to monorepo. 2023-02-03 11:57:50 +02:00
a.bouhuolia
28e309981b fix: move github build configure out. 2023-02-03 01:13:03 +02:00
a.bouhuolia
7a0a13f9d5 re-structure to monorepo. 2023-02-03 01:02:31 +02:00
a.bouhuolia
8242ec64ba dumo changelog. 2023-02-02 20:01:24 +02:00
a.bouhuolia
e900966cb2 fix: customer/vendor drawer new transaction link. 2023-01-26 23:08:19 +02:00
Ahmed Bouhuolia
24407bd744 Merge pull request #72 from bigcapitalhq/BIG-403-labels-of-add-money-in-out-menu-do-not-work
fix(cashflow): labels of Add money in/out don't appear.
2023-01-26 22:39:08 +02:00
Ahmed Bouhuolia
2176a8be1e Merge branch 'develop' into BIG-403-labels-of-add-money-in-out-menu-do-not-work 2023-01-26 22:38:44 +02:00
a.bouhuolia
9ca2aab58f fix(account-drawer): description placeholder. 2023-01-26 22:34:01 +02:00
a.bouhuolia
036180695f fix(financial-statement): BIG-405 reports filter from/to dates out of the range. 2023-01-26 22:25:51 +02:00
a.bouhuolia
dc99b1f128 fix(account-form): BIG-401 Edit account form initial values do not fill up automatically. 2023-01-26 22:11:26 +02:00
a.bouhuolia
21199c45fa fix(projects): disappear the info button in case the feature was not enabled. 2023-01-26 00:45:35 +02:00
Ahmed Bouhuolia
729fce9c70 Merge pull request #73 from bigcapitalhq/BIG-408-hide-the-project-name-entry-if-the-feature-was-not-enabled
fix(projects): hide the project name entry if the feature was not ena…
2023-01-26 00:07:13 +02:00
a.bouhuolia
7689e5bbe7 fix(projects): hide the project name entry if the feature was not enabled. 2023-01-26 00:05:58 +02:00
a.bouhuolia
96ac46ca64 fix(cashflow): labels of Add money in/out don't appear. 2023-01-25 00:14:11 +02:00
a.bouhuolia
806e4fb54c fix(FinancialStatements): Hide filtering by branches/warehouses if the feature is not enabled. 2023-01-23 22:52:45 +02:00
a.bouhuolia
49f3465265 fix(DataTable): text style of table header. 2023-01-23 22:45:59 +02:00
a.bouhuolia
31d665e91e fix(projects): fetch projects if the feature was enabled. 2023-01-23 16:38:36 +02:00
a.bouhuolia
7de66f16ce fix: change the default sidebar width of the dashboard. 2022-11-06 16:08:39 +02:00
Ahmed Bouhuolia
9b9b43d065 Merge pull request #62 from bigcapitalhq/feature/projects
feat: add project.
2022-11-06 16:03:01 +02:00
a.bouhuolia
048fcda9dc fix(projects): optimize style time entry form. 2022-10-05 23:29:13 +02:00
a.bouhuolia
f9a7021f55 feat: Add Box, Group and Stack layout components. 2022-10-04 00:29:48 +02:00
a.bouhuolia
41db96d958 feat(projects): WIP projects service. 2022-10-02 21:33:23 +02:00
elforjani13
900a237a52 feat: project detail 2022-09-26 01:05:28 +02:00
elforjani13
6363576c5e feat: project detail. 2022-09-25 22:57:53 +02:00
elforjani13
3c3f8c6731 fix: project task dialog. 2022-09-25 22:29:51 +02:00
elforjani13
8ac881cfd7 feat: add project billable entries link & alert. 2022-09-25 22:27:40 +02:00
elforjani13
6055184084 feat: add api project billable entries dialog. 2022-09-25 22:24:06 +02:00
elforjani13
b83faef167 feat: add project billable entries cell. 2022-09-21 05:19:21 +02:00
elforjani13
d102f33698 Merge branch 'feature/projects' of https://github.com/bigcapitalhq/client into feature/projects 2022-09-20 18:58:25 +02:00
elforjani13
942644f8d5 feat: project billable entries dialog 2022-09-20 18:57:22 +02:00
elforjani13
20d9a23260 feat: project billable entries dialog. 2022-09-20 18:56:03 +02:00
elforjani13
7ac9f78366 fix: project billable entries dilaog 2022-09-20 01:24:23 +02:00
elforjani13
777f6a11cd fix: add ignore Ts errors to project billable 2022-09-20 01:13:27 +02:00
elforjani13
785c407a73 Merge branch 'feature/projects' of https://github.com/bigcapitalhq/client into feature/projects 2022-09-19 22:38:31 +02:00
elforjani13
860378f00d fix: project task form dialog. 2022-09-19 22:37:48 +02:00
elforjani13
01e2c24387 feat: add project billable entries dialog. 2022-09-19 22:37:11 +02:00
a.bouhuolia
84a903361e fix: ignore TS errors to speed up the TS compiling. 2022-09-19 21:16:02 +02:00
elforjani13
2ae720821e feat: add project invoicing cell. 2022-09-08 21:44:16 +02:00
elforjani13
c75f46d8a4 feat: add Project invoicing form dialog. 2022-09-08 21:43:34 +02:00
elforjani13
d77fcb7c89 feat: add project ability. 2022-08-30 19:18:19 +02:00
elforjani13
7c6a85d1d4 feat: add project to expenses 2022-08-30 18:50:28 +02:00
elforjani13
dd264eed65 feat: add project to journal 2022-08-30 18:49:05 +02:00
elforjani13
6b37dcd8ae feat: add project list field cell 2022-08-30 18:48:08 +02:00
elforjani13
5c3a7effc1 feat: add project profitability summary. 2022-08-22 19:56:27 +02:00
elforjani13
69c4519647 feat: add project select to sales & purchases 2022-08-11 11:20:52 +02:00
elforjani13
95137f4fcd feat: add project select. 2022-08-07 21:31:57 +02:00
elforjani13
7c0aa9b353 fix: project time entry form. 2022-08-06 22:16:03 +02:00
elforjani13
3753097ea6 fix: add projects status 2022-08-06 12:37:40 +02:00
elforjani13
93da3ed41d fix: edit project task form. 2022-08-06 12:37:08 +02:00
elforjani13
c5a6a72fa5 fix: project timesheet table. 2022-08-06 12:35:41 +02:00
elforjani13
4cb7a50a1d fix: edit project time entry. 2022-08-06 12:34:41 +02:00
elforjani13
5c8c3586e0 fix: project task form. 2022-07-31 18:43:32 +02:00
elforjani13
2e7ba59beb feat: add project time entry. 2022-07-31 18:32:50 +02:00
elforjani13
7350fef5c4 feat: add currency code to project task form. 2022-07-30 15:12:25 +02:00
elforjani13
1b13b98899 feat: add api project timesheet. 2022-07-30 15:10:23 +02:00
elforjani13
72c893c255 fix: project tasks. 2022-07-29 23:42:37 +02:00
elforjani13
fcf001a831 feat: add api project tasks. 2022-07-28 21:14:36 +02:00
elforjani13
a17843ddbe feat: add project. 2022-07-16 04:20:17 +02:00
a.bouhuolia
399ba8fb8e chores: remove unnecessary stories. 2022-07-15 23:36:03 +02:00
a.bouhuolia
f00097f6c8 chore: Refactoring all import directories to alias and all .js|.jsx renamed to be .ts|.tsx 2022-07-15 23:25:23 +02:00
elforjani13
cd08d0ee16 feat: add transaction select. 2022-06-23 21:51:34 +02:00
elforjani13
f268b8a95a feat: project status. 2022-06-23 19:41:09 +02:00
elforjani13
6a06950654 feat: project status. 2022-06-23 19:37:29 +02:00
elforjani13
d9de3341fe feat: add timesheet header. 2022-06-23 17:50:14 +02:00
elforjani13
6b6081e32e feat: add project & timesheet table. 2022-06-23 00:15:47 +02:00
elforjani13
7be568b8ac feat: time entry dialog. 2022-06-23 00:14:49 +02:00
elforjani13
50522af72d fix: task form dialog. 2022-06-23 00:13:46 +02:00
elforjani13
0b454d6d4d feat: project table. 2022-06-23 00:12:17 +02:00
elforjani13
4ba64cc4ff feat: add project timesheet. 2022-06-23 00:10:06 +02:00
elforjani13
5128c021b0 fix: project form. 2022-06-23 00:07:21 +02:00
elforjani13
5a8fcc8fb5 feat: add time entry form. 2022-06-15 16:09:21 +02:00
elforjani13
9cf1b993dd feat: add time entry form. 2022-06-15 16:08:50 +02:00
elforjani13
f443a1b106 fix: project detail tabs. 2022-06-15 11:38:20 +02:00
elforjani13
0eb0aee1ef feat: project detail tabs. 2022-06-14 17:19:59 +02:00
elforjani13
4b992c4bb4 fix: project details. 2022-06-13 17:55:52 +02:00
elforjani13
051681e6f3 feat: add timesheet & project details. 2022-06-13 17:33:54 +02:00
Ahmed Bouhuolia
629c790430 Merge pull request #57 from bigcapitalhq/BIG-379-create-a-project
`BIG-379` Add project & task dialog & projects list.
2022-06-12 13:07:29 +02:00
elforjani13
bdadc5d795 fix: remove the inner container. 2022-06-12 12:55:37 +02:00
elforjani13
23bb9c4cc3 fix: rename project & task form dialog. 2022-06-12 12:43:03 +02:00
elforjani13
8136378725 fix: project & task dialog. 2022-06-12 11:56:06 +02:00
elforjani13
4eac2239b1 feat: add projects view tabs. 2022-06-12 09:43:19 +02:00
elforjani13
a44f548ff9 feat: projects actions bar. 2022-06-11 15:30:11 +02:00
elforjani13
327916da4b fix: add FDateInput 2022-06-11 13:58:04 +02:00
elforjani13
bee7896279 fix: project form. 2022-06-11 13:29:33 +02:00
elforjani13
cb0a315ca6 feat: add task form dialog. 2022-06-11 00:45:31 +02:00
elforjani13
d2c907541a feat: add project form dialog. 2022-06-11 00:38:00 +02:00
elforjani13
928d4d3f00 feat: add projects list. 2022-06-11 00:36:18 +02:00
Ahmed Bouhuolia
01038136f2 Update CHANGELOG.md 2022-04-23 00:41:18 +02:00
a.bouhuolia
1172e69d96 chore: add 1.7.4-rc.2 CHANGELOG. 2022-04-23 00:37:18 +02:00
a.bouhuolia
87758bf773 chore(Sidebar): docs. 2022-04-23 00:28:26 +02:00
a.bouhuolia
5cbb3c84e6 Merge branch 'BIG-374-refactoring-sidebar-menu-with-feature-and-permissions-control' into develop 2022-04-22 23:42:45 +02:00
a.bouhuolia
52924383bd feat(Sidebar): filter sidebar items based on subscription state. 2022-04-18 01:24:11 +02:00
a.bouhuolia
8d1825a065 feature(Sidebar): BIG-374 filtering the sidebar items based on each item feature support. 2022-04-18 00:16:37 +02:00
a.bouhuolia
5e4e9c37c3 feat(Sidebar): add the missing sidebar items. 2022-04-17 05:19:23 +02:00
a.bouhuolia
944bc29f4d feat(Sidebar): Refactoring sidebar menu with feature and permissions abilities control. 2022-04-17 05:05:35 +02:00
a.bouhuolia
682b296f7c fix(FlexGrid): BIG-378 Reports drawers columns css conflict. 2022-04-15 22:33:08 +02:00
Ahmed Bouhuolia
e662bf7af9 Update CHANGELOG.md 2022-04-15 06:34:18 +02:00
a.bouhuolia
a829ceb709 Merge branch 'develop' into main 2022-04-15 06:29:34 +02:00
a.bouhuolia
cdce00187b Merge branch 'feature/tooltip-oneline' into develop 2022-04-15 06:26:42 +02:00
a.bouhuolia
91a38b34cc feat: add readonly entriese details as oneline with tooltip for more details. 2022-04-15 06:24:24 +02:00
a.bouhuolia
1b97a162e8 Merge branch 'BIG-entries-columns' of https://github.com/bigcapitalhq/client into develop 2022-04-15 04:36:08 +02:00
elforjani13
e8e12e63ea feat: add tooltip cell to detail. 2022-04-10 13:46:05 +02:00
elforjani13
80feba6005 feat(sales/purchases): add tooltip cell to detail. 2022-04-09 02:04:31 +02:00
elforjani13
cc457e1e43 feat: estimate tooltip. 2022-04-09 00:42:34 +02:00
a.bouhuolia
2ced5dc013 feat(TextOverlayTooltip): WIP 2022-04-08 06:04:12 +02:00
Ahmed Bouhuolia
dd86c2993e Merge pull request #50 from bigcapitalhq/BIG-372-activate-branches-and-warehouses-dialog-reloading-once-activating
BIG-372: activate branches & warehouses reloading.
2022-04-08 05:21:31 +02:00
a.bouhuolia
bd05a4a188 fix(GeneralLedger): BIG-373 Issue general ledger report select specific account. 2022-04-08 05:19:11 +02:00
elforjani13
8160cbe402 feat: add entries-columns. 2022-04-06 16:52:44 +02:00
elforjani13
0ef6bebfb8 BIG-372: activate branches & warehouses reloading. 2022-04-06 15:00:30 +02:00
a.bouhuolia
91ff3fdccb Merge branch 'main' into develop 2022-04-05 06:08:54 +02:00
a.bouhuolia
edd37fff78 fix(PaymaentViaVocher): make the plan is fixed. 2022-04-05 06:06:02 +02:00
a.bouhuolia
679f7ce96c Merge branch 'develop' into main 2022-04-05 05:27:28 +02:00
Ahmed Bouhuolia
79b3ab9ec7 Update CHANGELOG.md 2022-04-05 05:26:48 +02:00
a.bouhuolia
e7158b7ba7 feat(i18n): add the missing arabic localization. 2022-04-05 05:25:26 +02:00
Ahmed Bouhuolia
569bc1c4a4 Merge pull request #49 from bigcapitalhq/billingplans
Billingplans
2022-04-05 05:16:28 +02:00
elforjani13
b880732087 feat: add billing plans. 2022-04-04 23:52:29 +02:00
elforjani13
b1e7720bd9 BIG-378: add Localize to invoice. 2022-04-04 17:58:39 +02:00
a.bouhuolia
5eb9968095 Merge branch 'develop' into main 2022-03-31 15:09:25 +02:00
Ahmed Bouhuolia
b0cf8f723f Merge pull request #47 from bigcapitalhq/BIG-354-validate-the-warehouse-transfer-quantity-should-be-above-zero
BIG-354: Validate the warehouse transfer.
2022-03-31 15:05:34 +02:00
Ahmed Bouhuolia
b0a826e62a Merge branch 'develop' into BIG-354-validate-the-warehouse-transfer-quantity-should-be-above-zero 2022-03-31 15:05:27 +02:00
elforjani13
6daa9f09a5 BIG-354: fix validate the warehouse transfer. 2022-03-31 14:58:36 +02:00
Ahmed Bouhuolia
f828d85880 Merge pull request #45 from bigcapitalhq/BIG-344-add-branch-details-to-manual-journal-and-expense-drawer-details
BIG-344: add branch to manual journal & expense.
2022-03-31 14:56:35 +02:00
elforjani13
46d895bef9 BIG-354: Validate the warehouse transfer. 2022-03-31 14:50:40 +02:00
Ahmed Bouhuolia
1fa26c7cb7 Merge pull request #46 from bigcapitalhq/BIG-278-fix-created-at-in-expense-details
BIG-278: add created_at in expense details.
2022-03-31 14:30:21 +02:00
a.bouhuolia
41d2fc63cb chore(Changelog): add the missing logs. 2022-03-31 14:08:22 +02:00
a.bouhuolia
4814a40fa9 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2022-03-31 14:07:21 +02:00
a.bouhuolia
b903aa3eb2 dump v1.7.2 version with changelogs. 2022-03-31 14:07:17 +02:00
elforjani13
872b98fb0d BIG-278: add created_at in expense details. 2022-03-31 13:57:07 +02:00
elforjani13
b849bfaa95 BIG-344: add branch to manual journal & expense. 2022-03-31 13:39:15 +02:00
Ahmed Bouhuolia
756af3c4d3 Merge pull request #43 from bigcapitalhq/BIG-141-add-inactive-status-to-item-drawer-details
Big 141 add inactive status to item drawer details
2022-03-31 00:52:51 +02:00
Ahmed Bouhuolia
b935d13918 Merge branch 'develop' into BIG-141-add-inactive-status-to-item-drawer-details 2022-03-31 00:52:41 +02:00
Ahmed Bouhuolia
4aea9cb19b Merge pull request #41 from bigcapitalhq/BIG-356-add-localize-remove-line-entries
Big 356 add localize remove line entries
2022-03-31 00:51:38 +02:00
Ahmed Bouhuolia
d22212e6e3 Merge branch 'develop' into BIG-356-add-localize-remove-line-entries 2022-03-31 00:51:28 +02:00
Ahmed Bouhuolia
6a6ff16c48 Merge pull request #44 from bigcapitalhq/BIG-238-validate-bill-due-date-should-be-equal-or-bigger-than-bill-date
Big 238 validate bill due date should be equal or bigger than bill date
2022-03-31 00:49:31 +02:00
Ahmed Bouhuolia
d08894820d Merge pull request #39 from bigcapitalhq/BIG-354-validate-the-warehouse-transfer-quantity-should-be-above-zero
BIG-354: warehouse transfer validate.
2022-03-31 00:49:07 +02:00
elforjani13
2db32b8ee8 BIG-141: add inactive status to item details. 2022-03-30 17:17:23 +02:00
elforjani13
86c6de361b BIG-356: add localize remove line entries. 2022-03-30 16:34:51 +02:00
elforjani13
9bd13b0d46 BIG-355: remove expandable in item categories. 2022-03-30 16:31:34 +02:00
elforjani13
30b17d697f BIG-355: remove expandable in item categories. 2022-03-30 16:28:42 +02:00
elforjani13
ce674466fe BIG-238: Validate bill due date. 2022-03-30 15:09:11 +02:00
elforjani13
373a695c4c BIG-236: Validate estimate expiration date. 2022-03-30 15:06:58 +02:00
elforjani13
3f4ffdc995 BIG-237: Validate invoice due date. 2022-03-30 15:04:55 +02:00
elforjani13
a9c2a5c5f0 BIG-354: warehouse transfer validate. 2022-03-28 21:38:56 +02:00
a.bouhuolia
908d232cb9 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2022-03-28 19:26:09 +02:00
a.bouhuolia
10af33f1dc fix: BIG-352 Add terms and conditions/notes field to a warehouse transfer. 2022-03-28 19:25:06 +02:00
a.bouhuolia
767d807490 fix: BIG-352 Add terms and conditions/notes field to a warehouse transfer. 2022-03-28 19:06:38 +02:00
Ahmed Bouhuolia
ba1d945dca Merge pull request #38 from bigcapitalhq/BIG-284-cash-flow-statement-loading-bar-not-working
BIG-284: cash flow statement loading bar.
2022-03-28 16:23:42 +02:00
elforjani13
03ea8643e7 BIG-284: cash flow statment loading bar. 2022-03-28 16:18:54 +02:00
Ahmed Bouhuolia
3d78e5d397 Merge pull request #36 from bigcapitalhq/BIG-301-fix-navbar-divider-in-action-bar-detail-casl-ability
fix: navbar divider.
2022-03-28 16:07:20 +02:00
Ahmed Bouhuolia
fffd255eb1 Merge pull request #37 from bigcapitalhq/BIG-add-keepPreviousData
BIG: add keepPreviousData option on use query.
2022-03-28 16:05:42 +02:00
Ahmed Bouhuolia
d0f09a0164 Merge pull request #34 from bigcapitalhq/BIG-351-invalid-date-in-the-inventory-adjustment-detail
Big 351 invalid date in the inventory adjustment detail
2022-03-28 16:01:37 +02:00
elforjani13
7774d9f5ab BIG: add keepPreviousData . 2022-03-28 16:01:07 +02:00
Ahmed Bouhuolia
e5ab240dfd Merge pull request #33 from bigcapitalhq/BIG-221-remove-non-inventory-radio-choice-on-item-form
BIG-221: remove non inventory radio.
2022-03-28 15:54:59 +02:00
elforjani13
2b07917399 BIG-352: invalid date. 2022-03-28 15:48:36 +02:00
elforjani13
79099e1abc BIG-280: optimize select. 2022-03-28 15:46:37 +02:00
elforjani13
e87b22801b BIG-221: remove non inventory radio. 2022-03-28 13:46:41 +02:00
elforjani13
e0eaa56b5c fix: navbar divider. 2022-03-28 13:26:21 +02:00
a.bouhuolia
d842722183 fix: add the missed imported warehouse transfers. 2022-03-28 12:08:53 +02:00
a.bouhuolia
9c14f10edf Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2022-03-28 12:01:39 +02:00
a.bouhuolia
6a8137729f fix(Account): BIG-296 Issue when creating a new child account from chart of accounts list. 2022-03-28 12:01:29 +02:00
a.bouhuolia
5c601fcf2d fix(Account): BIG-296 Issue when creating a new child account from chart of accounts list. 2022-03-28 11:58:12 +02:00
elforjani13
86cab7988c feat: handle item error. 2022-03-24 15:03:25 +02:00
Ahmed Bouhuolia
8404fee10a dump v1.7.0-rc.1 2022-03-24 13:12:14 +02:00
elforjani13
99a23889bc feat: add warehouses transfers query. 2022-03-24 12:19:46 +02:00
elforjani13
5e2000d252 feat: add invalidate item warehouses. 2022-03-24 11:50:06 +02:00
13783 changed files with 300293 additions and 154425 deletions

View File

@@ -1 +0,0 @@
APP_VERSION=$npm_package_version

24
.gitignore vendored
View File

@@ -1,23 +1 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
node_modules/

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v14.20

View File

@@ -2,8 +2,97 @@
All notable changes to Bigcapital server-side will be in this file.
## [1.7.4-rc.2] - 20-04-2022
`@bigcapital/webapp`
### Fixed
- `BIG-374` Refactoring sidebar men with ability permissions and feature control on each item.
## [1.7.3-rc.2] - 15-04-2022
`@bigcapital/webapp`
### Fixed
- `BIG-372` Activate branches and warehouses dialog reloading once activating.
- `BIG-373` Issue general ledger report select specific account.
- `BIG-377` Make readonly details entries as oneline with tooltip for more details.
## [1.7.2-rc.2] - 04-04-2022
`@bigcapital/webapp`
### Fixed
- Add the missing Arabic localization.
- Subscription plans modifications.
## [1.7.1-rc.2] - 30-03-2022
`@bigcapital/webapp`
### Added
- `BIG-141` Add inactive status to item drawer details.
- `BIG-278` Add created at date on expense details.
- `BIG-350` Add empty status content of warehouse transfers service.
- `BIG-344` Add branch details to manual journal and expense details.
### Fixed
- `BIG-221` Remove Non-inventory radio choice on item form.
- `BIG-236` Validate estimate expiration date should be equal or bigger than estimate date.
- `BIG-237` Validate invoice due date should be equal or bigger than invoice date.
- `BIG-238` Validate bill due date should be equal or bigger than bill date.
- `BIG-280` Optimize style of multi-select accounts menu.
- `BIG-284` Cashflow statement loading bar.
- `BIG-296` Creating a new child account from accounts list.
- `BIG-301` Navigation bar divider on actions bar hide with permissions control.
- `BIG-304` Adding cash or bank account from cash flow service.
- `BIG-351` Invalid date in the inventory adjustment detail.
- `BIG-352` Fix terms and notes fields on footer of all services.
- `BIG-354` Validate the warehouse transfer quantity should be above zero.
`@bigcapital/server`
### Fixed
- `BIG-354` Validate the warehouse transfer quantity should be above zero.
- `BIG-358` Refactoring customers/vendors services for smaller classes.
- `BIG-341` Refactoring expenses services for smaller classes.
- `BIG-342` Assign default currency as base currency when create customer, vendor or expense transaction.
## [1.7.0-rc.1] - 24-03-2022
`@bigcapital/webapp`
### Added
- Multiply currencies with foreign currencies.
- Multiply warehouses to track inventory items.
- Multiply branches to track organization transactions.
- Transfer orders between warehouses.
- Integrate financial reports with multiply branches.
- Integrate inventory reports with multiply warehouses.
### Changes
- Optimize style of sale invoice form.
- Optimize style of sale receipt form.
- Optimize style of credit note form.
- Optimize style of payment receive form.
- Optimize style of bill form.
- Optimize style of payment made form.
- Optimize style of manual journal form.
- Optimize style of expense form.
`@bigcapital/server`
### Added
- Multiply currencies with foreign currencies.
- Multiply warehouses to track inventory items.
- Multiply branches to track organization transactions.
- Transfer orders between warehouses.
- Integrate financial reports with multiply branches.
- Integrate inventory reports with multiply warehouses.
## [1.6.3] - 21-02-2022
`@bigcapital/webapp`
### Fixed
- `BIG-337` Display billing page once the organization subscription is inactive.
@@ -14,6 +103,8 @@ All notable changes to Bigcapital server-side will be in this file.
## [1.6.0] - 18-02-2022
`@bigcapital/webapp`
### Added
- Balance sheet comparison of previous period (PP).
- Balance sheet comparison of previous year (PY).
@@ -24,20 +115,34 @@ All notable changes to Bigcapital server-side will be in this file.
## [1.5.8] - 13-01-2022
`@bigcapital/webapp`
### Added
- Add payment receive PDF print.
- Add credit note PDF print.
### Fixed
- fix: Payment receive initial loading state depends on request loading state instead fetching.
- fix: Balance sheet report alert positioning.
- fix: Separate customer and vendor inactivate and activate alerts.
- fix: Hide convert to invoice button if the invoice is already converted.
- fix: Customer and vendor balance summary percentage of column option.
- fix: Remove duplicated details in sales and purchases details drawers.
- Payment receive initial loading state depends on request loading state instead fetching.
- Balance sheet report alert positioning.
- Separate customer and vendor inactivate and activate alerts.
- Hide convert to invoice button if the invoice is already converted.
- Customer and vendor balance summary percentage of column option.
- Remove duplicated details in sales and purchases details drawers.
`@bigcapital/server`
### Fixed
- Balance sheet comparison of previous period (PP).
- Balance sheet comparison of previous year (PY).
- Balance sheet percentage analysis columns and rows basis.
- Profit & loss sheet comparison of preivous period (PP).
- Profit & loss sheet comparison of previous year (PY).
- Profit & loss sheet percentage analysis columns, rows, income and expenses basis.
## [1.5.3] - 03-01-2020
`@bigcapital/webapp`
### Fixed
- Localize the global errors.
@@ -45,6 +150,8 @@ All notable changes to Bigcapital server-side will be in this file.
## [1.5.0] - 20-12-2021
`@bigcapital/webapp`
### Added
- Add credit note on sales module.
@@ -65,6 +172,8 @@ All notable changes to Bigcapital server-side will be in this file.
## [1.4.0] - 11-09-2021
`@bigcapital/webapp`
### Added
- Add SMS notification on sale invoice, receipt, customers payments modules.
@@ -84,6 +193,8 @@ fix: BIG-132 AR/AP aging summary report filter by none transactions/zero contact
## [1.2.0-RC] - 03-09-2021
`@bigcapital/webapp`
Here we write upgrading notes for brands. It's a team effort to make them as
straightforward as possible.

View File

@@ -1,68 +1,31 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
<p align="center">
<p align="center">
<a href="https://bigcapital.ly" target="_blank">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/bigcapital.svg" alt="Bigcapital" width="280" height="75">
</a>
</p>
<p align="center">
Simple, smart online accounting software for small and medium businesses.
</p>
</p>
## Available Scripts
# What's Bigcapital?
In the project directory, you can run:
Bigcapital is a smart and open-source accounting and inventory software, Bigcapital keeps all business finances in right place and automates accounting processes to give the business powerful and intelligent financial statements and reports to help in making decisions.
### `npm start`
<p align="center">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/screenshot-2.png" width="270">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/screenshot-1.png" width="270">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/screenshot-3.png" width="270">
</p>
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
# Resources
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
- [Discord](https://discord.gg/s3ARzDcf) - Ask for help.
- [Bug Tracker](https://github.com/bigcapital/bigcapital/issues) - Notify us new bugs.
- [Source Code](https://github.com/bigcapital/bigcapital) - Github repo.
### `npm test`
# Changlog
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
Please see [Releases](https://github.com/bigcapital/bigcapital) for more information what has changed recently.

View File

@@ -1,104 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebook/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
// We support configuring the sockjs pathname during development.
// These settings let a developer run multiple simultaneous projects.
// They are used as the connection `hostname`, `pathname` and `port`
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
// and `sockPort` options in webpack-dev-server.
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
// Application version.
VERSION: paths.appVersion
}
);
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;

View File

@@ -1,66 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const chalk = require('react-dev-utils/chalk');
const paths = require('./paths');
// Ensure the certificate and key provided are valid and if not
// throw an easy to debug error
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
let encrypted;
try {
// publicEncrypt will throw an error with an invalid cert
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
} catch (err) {
throw new Error(
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
);
}
try {
// privateDecrypt will throw an error with an invalid key
crypto.privateDecrypt(key, encrypted);
} catch (err) {
throw new Error(
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
err.message
}`
);
}
}
// Read file and throw an error if it doesn't exist
function readEnvFile(file, type) {
if (!fs.existsSync(file)) {
throw new Error(
`You specified ${chalk.cyan(
type
)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
);
}
return fs.readFileSync(file);
}
// Get the https config
// Return cert files if provided in env, otherwise just true or false
function getHttpsConfig() {
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
const isHttps = HTTPS === 'true';
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
const config = {
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
};
validateKeyAndCerts({ ...config, keyFile, crtFile });
return config;
}
return isHttps;
}
module.exports = getHttpsConfig;

View File

@@ -1,14 +0,0 @@
'use strict';
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

View File

@@ -1,40 +0,0 @@
'use strict';
const path = require('path');
const camelcase = require('camelcase');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
// Based on how SVGR generates a component name:
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
const pascalCaseFilename = camelcase(path.parse(filename).name, {
pascalCase: true,
});
const componentName = `Svg${pascalCaseFilename}`;
return `const React = require('react');
module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
return {
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: ref,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
};
}),
};`;
}
return `module.exports = ${assetFilename};`;
},
};

View File

@@ -1,141 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
const chalk = require('react-dev-utils/chalk');
const resolve = require('resolve');
/**
* Get additional module paths based on the baseUrl of a compilerOptions object.
*
* @param {Object} options
*/
function getAdditionalModulePaths(options = {}) {
const baseUrl = options.baseUrl;
// We need to explicitly check for null and undefined (and not a falsy value) because
// TypeScript treats an empty string as `.`.
if (baseUrl == null) {
// If there's no baseUrl set we respect NODE_PATH
// Note that NODE_PATH is deprecated and will be removed
// in the next major release of create-react-app.
const nodePath = process.env.NODE_PATH || '';
return nodePath.split(path.delimiter).filter(Boolean);
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
// the default behavior.
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
return null;
}
// Allow the user set the `baseUrl` to `appSrc`.
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
return [paths.appSrc];
}
// If the path is equal to the root directory we ignore it here.
// We don't want to allow importing from the root directly as source files are
// not transpiled outside of `src`. We do allow importing them with the
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
// an alias.
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return null;
}
// Otherwise, throw an error.
throw new Error(
chalk.red.bold(
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
' Create React App does not support other values at this time.'
)
);
}
/**
* Get webpack aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
src: paths.appSrc,
};
}
}
/**
* Get jest aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getJestAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
'^src/(.*)$': '<rootDir>/src/$1',
};
}
}
function getModules() {
// Check if TypeScript is setup
const hasTsConfig = fs.existsSync(paths.appTsConfig);
const hasJsConfig = fs.existsSync(paths.appJsConfig);
if (hasTsConfig && hasJsConfig) {
throw new Error(
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
);
}
let config;
// If there's a tsconfig.json we assume it's a
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync('typescript', {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
} else if (hasJsConfig) {
config = require(paths.appJsConfig);
}
config = config || {};
const options = config.compilerOptions || {};
const additionalModulePaths = getAdditionalModulePaths(options);
return {
additionalModulePaths: additionalModulePaths,
webpackAliases: getWebpackAliases(options),
jestAliases: getJestAliases(options),
hasTsConfig,
};
}
module.exports = getModules();

View File

@@ -1,75 +0,0 @@
'use strict';
const path = require('path');
const fs = require('fs');
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
const appVersion = require(resolveApp('package.json')).version;
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
publicUrlOrPath,
appVersion
};
module.exports.moduleFileExtensions = moduleFileExtensions;

View File

@@ -1,35 +0,0 @@
'use strict';
const { resolveModuleName } = require('ts-pnp');
exports.resolveModuleName = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveModuleName
);
};
exports.resolveTypeReferenceDirective = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveTypeReferenceDirective
);
};

View File

@@ -1,691 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const CompressionPlugin = require("compression-webpack-plugin");
const postcssNormalize = require('postcss-normalize');
const { postcssRTLCSS} = require('postcss-rtlcss');
const appPackageJson = require(paths.appPackageJson);
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// We will provide `paths.publicUrlOrPath` to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
// Postcss rtlcss plugin.
// require( 'postcss-rtl' )({
// // options here.
// removeComments: false,
// }),
postcssRTLCSS({
}),
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
sassOptions: {
sourceComments: true,
outputStyle: 'expanded'
}
},
}
);
}
return loaders;
};
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
globalObject: 'this',
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
cssProcessorPluginOptions: {
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
resolve: {
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
// First, run the linter.
// It's important to do this before Babel processes the JS.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
enforce: 'pre',
use: [
{
options: {
cache: true,
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
resolvePluginsRelativeTo: __dirname,
},
loader: require.resolve('eslint-loader'),
},
],
include: paths.appSrc,
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// It will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (currently CSS only):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/],
importWorkboxFrom: 'cdn',
navigateFallback: paths.publicUrlOrPath + 'index.html',
navigateFallbackBlacklist: [
// Exclude URLs starting with /_, as they're likely an API call
new RegExp('^/_'),
// Exclude any URLs whose last part seems to be a file extension
// as they're likely a resource and not a SPA route.
// URLs containing a "?" character won't be blacklisted as they're likely
// a route with query params (e.g. auth callbacks).
new RegExp('/[^/?]+\\.[^/]+$'),
],
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: isEnvDevelopment,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [
'**',
'!**/__tests__/**',
'!**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
silent: true,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
// Gzip compression.
isEnvProduction &&
new CompressionPlugin({
// asset: "[path].gz[query]",
algorithm: "gzip",
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8
})
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell webpack to provide empty mocks for them so importing them works.
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};

View File

@@ -1,130 +0,0 @@
'use strict';
const fs = require('fs');
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
const ignoredFiles = require('react-dev-utils/ignoredFiles');
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
const paths = require('./paths');
const getHttpsConfig = require('./getHttpsConfig');
const host = process.env.HOST || '0.0.0.0';
const sockHost = process.env.WDS_SOCKET_HOST;
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
const sockPort = process.env.WDS_SOCKET_PORT;
module.exports = function(proxy, allowedHost) {
return {
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
// https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a
// compromise. Since our WDS configuration only serves files in the `public`
// folder we won't consider accessing them a vulnerability. However, if you
// use the `proxy` feature, it gets more dangerous because it can expose
// remote code execution vulnerabilities in backends like Django and Rails.
// So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable.
disableHostCheck:
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: 'none',
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files wont automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
contentBasePublicPath: paths.publicUrlOrPath,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// Use 'ws' instead of 'sockjs-node' on server since we're using native
// websockets in `webpackHotDevClient`.
transportMode: 'ws',
// Prevent a WS client from getting injected as we're already including
// `webpackHotDevClient`.
injectClient: false,
// Enable custom sockjs pathname for websocket connection to hot reloading server.
// Enable custom sockjs hostname, pathname and port for websocket connection
// to hot reloading server.
sockHost,
sockPath,
sockPort,
// It is important to tell WebpackDevServer to use the same "publicPath" path as
// we specified in the webpack config. When homepage is '.', default to serving
// from the root.
// remove last slash so user can land on `/test` instead of `/test/`
publicPath: paths.publicUrlOrPath.slice(0, -1),
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebook/create-react-app/issues/293
// src/node_modules is not ignored to support absolute imports
// https://github.com/facebook/create-react-app/issues/1065
watchOptions: {
ignored: ignoredFiles(paths.appSrc),
},
https: getHttpsConfig(),
host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
index: paths.publicUrlOrPath,
},
public: allowedHost,
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
proxy,
before(app, server) {
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
// middlewares before `redirectServedPath` otherwise will not have any effect
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
},
after(app) {
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
app.use(redirectServedPath(paths.publicUrlOrPath));
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
},
};
};

5
lerna.json Normal file
View File

@@ -0,0 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "0.0.0"
}

17880
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,220 +1,22 @@
{
"name": "bigcapital-client",
"version": "1.6.3",
"name": "bigcapital-monorepo",
"private": true,
"dependencies": {
"@babel/core": "7.8.4",
"@blueprintjs-formik/core": "^0.2.1",
"@blueprintjs-formik/select": "^0.1.4",
"@blueprintjs/core": "^3.50.2",
"@blueprintjs/datetime": "^3.23.12",
"@blueprintjs/popover2": "^0.11.1",
"@blueprintjs/select": "^3.11.2",
"@blueprintjs/table": "^3.8.3",
"@blueprintjs/timezone": "^3.6.2",
"@casl/ability": "^5.4.3",
"@casl/react": "^2.3.0",
"@reduxjs/toolkit": "^1.2.5",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@svgr/webpack": "4.3.3",
"@tanem/react-nprogress": "^3.0.24",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"@types/lodash": "^4.14.172",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"accounting": "^0.4.1",
"axios": "^0.21.2",
"babel-eslint": "10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "8.0.6",
"babel-plugin-named-asset-import": "^0.3.6",
"babel-preset-react-app": "^9.1.1",
"basscss": "^8.0.2",
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"cross-env": "^7.0.2",
"css-loader": "3.4.2",
"deep-map-keys": "^2.0.1",
"dependency-graph": "^0.11.0",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.0",
"eslint-loader": "3.0.3",
"eslint-plugin-flowtype": "4.6.0",
"eslint-plugin-import": "2.20.0",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.18.0",
"eslint-plugin-react-hooks": "^1.6.1",
"fast-deep-equal": "^3.1.3",
"file-loader": "4.3.0",
"flow-bin": "^0.123.0",
"formik": "^2.2.5",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "4.0.0-beta.11",
"identity-obj-proxy": "3.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.2",
"js-money": "^0.6.3",
"lodash": "^4.17.15",
"mini-css-extract-plugin": "0.9.0",
"moment": "^2.24.0",
"moment-timezone": "^0.5.33",
"node-sass": "^4.13.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"pnp-webpack-plugin": "1.6.0",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-rtl": "^1.7.3",
"postcss-safe-parser": "4.0.1",
"query-string": "^7.1.1",
"ramda": "^0.27.1",
"react": "^16.12.0",
"react-app-polyfill": "^1.0.6",
"react-body-classname": "^1.3.1",
"react-content-loader": "^6.0.1",
"react-dev-utils": "^11.0.4",
"react-dom": "^16.12.0",
"react-dropzone": "^11.0.1",
"react-error-boundary": "^3.0.2",
"react-hotkeys-hook": "^3.0.3",
"react-intl-universal": "^2.4.7",
"react-loadable": "^5.5.0",
"react-query": "^3.6.0",
"react-redux": "^7.1.3",
"react-router-breadcrumbs-hoc": "^3.2.10",
"react-router-dom": "^5.2.0",
"react-scroll-sync": "^0.7.1",
"react-scrollbars-custom": "^4.0.21",
"react-sortablejs": "^2.0.11",
"react-split-pane": "^0.1.91",
"react-table": "^7.6.3",
"react-table-sticky": "^1.1.3",
"react-transition-group": "^4.4.1",
"react-use": "^13.26.1",
"react-use-context-menu": "^0.1.4",
"react-virtualized": "^9.22.3",
"redux": "^4.0.5",
"redux-persist": "^6.0.0",
"redux-thunk": "^2.3.0",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"rtl-detect": "^1.0.3",
"sass-loader": "8.0.2",
"semver": "6.3.0",
"style-loader": "0.23.1",
"styled-components": "^5.3.1",
"stylis-rtlcss": "^2.1.1",
"terser-webpack-plugin": "2.3.4",
"ts-pnp": "1.1.5",
"url-loader": "2.3.0",
"webpack": "4.41.5",
"webpack-dev-server": "3.10.2",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "4.3.1",
"yup": "^0.28.1"
},
"scripts": {
"start": "PORT=8000 node scripts/start.js",
"start-win": "cross-env PORT=8000 node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js",
"flow": "flow"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"bootstrap": "lerna exec npm install",
"dev": "lerna run dev",
"build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
"build:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\"",
"build:server": "lerna run dev --scope \"@bigcapital/server\""
},
"workspaces": [
"packages/*"
],
"devDependencies": {
"@babel/preset-flow": "^7.9.0",
"@types/jest": "^26.0.15",
"@types/node": "^14.14.9",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.8",
"@types/yup": "^0.29.13",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"compression-webpack-plugin": "^6.1.0",
"http-proxy-middleware": "^1.0.0",
"postcss-rtlcss": "^1.7.2",
"react-query-devtools": "^2.1.1",
"redux-devtools": "^3.5.0",
"typescript": "^4.1.2"
"lerna": "^6.4.1"
},
"jest": {
"roots": [
"<rootDir>/src"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
],
"setupFiles": [
"react-app-polyfill/jsdom"
],
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
],
"testEnvironment": "jest-environment-jsdom-fourteen",
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
"^.+\\.module\\.(css|sass|scss)$"
],
"modulePaths": [
"/Users/ahmed/Documents/Ratteb/client/src"
],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
},
"babel": {
"presets": [
"react-app"
]
},
"proxy": "http://localhost:3000/"
}
"engines": {
"node": ">=14.20"
}
}

8
packages/server/.babelrc Normal file
View File

@@ -0,0 +1,8 @@
{
"presets": ["@babel/preset-env"],
"retainLines": true,
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
]
}

View File

@@ -0,0 +1,41 @@
MAIL_HOST=smtp.mailtrap.io
MAIL_USERNAME=842f331d3dc005
MAIL_PASSWORD=172f97b34f1a17
MAIL_PORT=587
MAIL_SECURE=false
MAIL_FROM_NAME=Bigcapital
MAIL_FROM_ADDRESS=noreply@sender.bigcapital.ly
SYSTEM_DB_CLIENT=mysql
SYSTEM_DB_HOST=127.0.0.1
SYSTEM_DB_USER=root
SYSTEM_DB_PASSWORD=root
SYSTEM_DB_NAME=bigcapital_system
SYSTEM_MIGRATIONS_DIR=./src/system/migrations
SYSTEM_SEEDS_DIR=./src/system/seeds
TENANT_DB_CLIENT=mysql
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
TENANT_DB_HOST=127.0.0.1
TENANT_DB_PASSWORD=root
TENANT_DB_USER=root
TENANT_DB_CHARSET=utf8
TENANT_MIGRATIONS_DIR=src/database/migrations
TENANT_SEEDS_DIR=src/database/seeds/core
DB_MANAGER_SUPER_USER=root
DB_MANAGER_SUPER_PASSWORD=root
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
CONTACT_US_MAIL=support@bigcapital.ly
BASE_URL=https://bigcapital.ly
LICENSES_AUTH_USER=root
LICENSES_AUTH_PASSWORD=root
AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123
BROWSER_WS_ENDPOINT=ws://localhost:4080/

View File

@@ -0,0 +1,34 @@
module.exports = {
env: {
browser: true,
es6: true,
},
extends: ['airbnb-base', 'airbnb-typescript'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
project: 'tsconfig.json',
tsconfigRootDir: './',
},
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
plugins: ['import'],
rules: {
'import/no-unresolved': 'error',
'import/prefer-default-export': 'off',
},
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: 'tsconfig.json',
},
},
},
};

7
packages/server/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
/node_modules/
/.env
/storage
package-lock.json
stdout.log
/dist
/build

View File

@@ -0,0 +1,203 @@
import commander from 'commander';
import color from 'colorette';
import argv from 'getopts'
import config from '../src/config';
import {
initSystemKnex,
getAllSystemTenants,
initTenantKnex,
exit,
success,
log,
} from './utils';
// - bigcapital system:migrate:latest
// - bigcapital system:migrate:rollback
// - bigcapital tenants:migrate:latest
// - bigcapital tenants:migrate:latest --tenant_id=XXX
// - bigcapital tenants:migrate:rollback
// - bigcapital tenants:migrate:rollback --tenant_id=XXX
// - bigcapital tenants:migrate:make
// - bigcapital system:migrate:make
// - bigcapital tenants:list
commander
.command('system:migrate:rollback')
.description('Migrate the system database of the application.')
.action(async () => {
try {
const sysKnex = await initSystemKnex();
const [batchNo, _log] = await sysKnex.migrate.rollback();
if (_log.length === 0) {
success(color.cyan('Already at the base migration'));
}
success(
color.green(
`Batch ${batchNo} rolled back: ${_log.length} migrations`
) + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '')
);
} catch(error) {
exit(error);
}
});
commander
.command('system:migrate:latest')
.description('Rollback latest mgiration of the system database.')
.action(async () => {
try {
const sysKnex = await initSystemKnex();
const [batchNo, log] = await sysKnex.migrate.latest();
if (log.length === 0) {
success(color.cyan('Already up to date'));
}
success(
color.green(`Batch ${batchNo} run: ${log.length} migrations`) +
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
);
} catch(error) {
exit(error);
}
});
commander
.command('system:migrate:make <name>')
.description('Created a named migration file to the system database.')
.action(async (name) => {
const sysKnex = await initSystemKnex();
sysKnex.migrate.make(name).then((name) => {
success(color.green(`Created Migration: ${name}`));
}).catch(exit)
});
commander
.command('tenants:migrate:make <name>')
.description('Created a name migration file to the tenants databases.')
.action(async (name) => {
const sysKnex = await initTenantKnex();
sysKnex.migrate.make(name).then((name) => {
success(color.green(`Created Migration: ${name}`));
}).catch(exit)
});
commander
.command('tenants:list')
.description('Retrieve a list of all system tenants databases.')
.action(async (cmd) => {
try{
const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex);
tenants.forEach((tenant) => {
const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`;
console.log(`ID: ${tenant.id} | Organization ID: ${tenant.organizationId} | DB Name: ${dbName}`);
});
} catch(error) { exit(error); };
success('---');
});
commander
.command('tenants:migrate:rollback')
.description('Rollback the last batch of tenants migrations.')
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
.action(async (cmd) => {
try {
const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
}
const migrateOpers = [];
const migrateTenant = async (organizationId) => {
try {
const tenantKnex = await initTenantKnex(organizationId);
const [batchNo, _log] = await tenantKnex.migrate.rollback();
const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`;
if (_log.length === 0) {
log(color.cyan('Already at the base migration'));
}
log(
color.green(
`Tenant: ${tenantDb} > Batch ${batchNo} rolled back: ${_log.length} migrations`
) + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '')
);
log('---------------');
} catch (error) { exit(error); }
};
if (!cmd.tenant_id) {
tenants.forEach((tenant) => {
const oper = migrateTenant(tenant.organizationId);
migrateOpers.push(oper);
});
} else {
const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
}
Promise.all(migrateOpers).then(() => {
success('All tenants are rollbacked.');
});
} catch (error) { exit(error); }
});
commander
.command('tenants:migrate:latest')
.description('Migrate all tenants or the given tenant id.')
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
.action(async (cmd) => {
try {
const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex);
const tenantsOrgsIds = tenants.map(tenant => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
}
// Validate the tenant id exist first of all.
const migrateOpers = [];
const migrateTenant = async (organizationId) => {
try {
const tenantKnex = await initTenantKnex(organizationId);
const [batchNo, _log] = await tenantKnex.migrate.latest();
const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`;
if (_log.length === 0) {
log(color.cyan('Already up to date'));
}
log(
color.green(`Tenant ${tenantDb} > Batch ${batchNo} run: ${_log.length} migrations`) +
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
);
log('-------------------');
} catch (error) {
log(error);
}
}
if (!cmd.tenant_id) {
tenants.forEach((tenant) => {
const oper = migrateTenant(tenant.organizationId);
migrateOpers.push(oper);
});
} else {
const oper = migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
}
Promise.all(migrateOpers).then(() => {
success('All tenants are migrated.');
});
} catch (error) {
exit(error);
}
});
commander.parse(process.argv);

View File

@@ -0,0 +1,94 @@
import Knex from 'knex';
import { knexSnakeCaseMappers } from 'objection';
import color from 'colorette';
import config from '../src/config';
// import { systemKnexConfig } from '../src/config/knexConfig';
function initSystemKnex() {
return Knex({
client: config.system.db_client,
connection: {
host: config.system.db_host,
user: config.system.db_user,
password: config.system.db_password,
database: config.system.db_name,
charset: 'utf8',
},
migrations: {
directory: config.system.migrations_dir,
},
seeds: {
directory: config.system.seeds_dir,
},
pool: { min: 0, max: 7 },
...knexSnakeCaseMappers({ upperCase: true }),
});
}
function getAllSystemTenants(knex) {
return knex('tenants');
}
function initTenantKnex(organizationId) {
return Knex({
client: config.tenant.db_client,
connection: {
host: config.tenant.db_host,
user: config.tenant.db_user,
password: config.tenant.db_password,
database: `${config.tenant.db_name_prefix}${organizationId}`,
charset: config.tenant.charset,
},
migrations: {
directory: config.tenant.migrations_dir,
},
seeds: {
directory: config.tenant.seeds_dir,
},
pool: { min: 0, max: 5 },
...knexSnakeCaseMappers({ upperCase: true }),
})
}
function exit(text) {
if (text instanceof Error) {
console.error(
color.red(`${text.detail ? `${text.detail}\n` : ''}${text.stack}`)
);
} else {
console.error(color.red(text));
}
process.exit(1);
}
function success(text) {
console.log(text);
process.exit(0);
}
function log(text) {
console.log(text);
}
function getDeepValue(prop, obj) {
if (!Object.keys(obj).length) { return []; }
return Object.entries(obj).reduce((acc, [key, val]) => {
if (key === prop) {
acc.push(val);
} else {
acc.push(Array.isArray(val) ? val.map(getIds).flat() : getIds(val));
}
return acc.flat();
}, []);
}
export {
initTenantKnex,
initSystemKnex,
getAllSystemTenants,
exit,
success,
log,
getDeepValue,
}

View File

@@ -0,0 +1,17 @@
const { knexSnakeCaseMappers } = require('objection');
module.exports = {
client: 'mysql',
connection: {
host: '127.0.0.1',
user: 'root',
password: 'root',
database: 'bigcapital_tenant_hqde5zqkylsho06',
charset: 'utf8',
},
migrations: {
directory: './src/database/migrations',
},
pool: { min: 0, max: 7 },
...knexSnakeCaseMappers({ upperCase: true }),
};

View File

@@ -0,0 +1,144 @@
{
"name": "bigcapital-server",
"version": "1.7.1",
"description": "",
"main": "src/server.ts",
"scripts": {
"inspect": "cross-env NODE_PATH=./src nodemon src/server.ts",
"clear": "rimraf build",
"dev": "cross-env NODE_ENV=development webpack --config scripts/webpack.config.js",
"build:resources": "gulp --gulpfile=scripts/gulpfile.js styles styles-rtl",
"build": "cross-env NODE_ENV=production webpack --config scripts/webpack.config.js",
"lint:fix": "eslint --fix ./**/*.ts"
},
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
"license": "ISC",
"bin": {
"bigcapital": "./bin/bigcapital.js"
},
"dependencies": {
"@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3",
"@types/i18n": "^0.8.7",
"@types/knex": "^0.16.1",
"@types/mathjs": "^6.0.12",
"accepts": "^1.3.7",
"accounting": "^0.4.1",
"agenda": "^4.2.1",
"agendash": "^3.1.0",
"app-root-path": "^3.0.0",
"async": "^3.2.0",
"axios": "^0.20.0",
"babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3",
"bluebird": "^3.7.2",
"compression": "^1.7.4",
"country-codes-list": "^1.6.8",
"cpy": "^8.1.2",
"cpy-cli": "^3.1.1",
"crypto-random-string": "^3.2.0",
"csurf": "^1.10.0",
"deep-map": "^2.0.0",
"deepdash": "^5.3.7",
"dotenv": "^8.1.0",
"errorhandler": "^1.5.1",
"es6-weak-map": "^2.0.3",
"esm": "^3.2.25",
"event-dispatch": "^0.4.1",
"eventemitter2": "^6.4.5",
"express": "^4.17.1",
"express-basic-auth": "^1.2.0",
"express-boom": "^3.0.0",
"express-fileupload": "^1.1.7-alpha.3",
"express-oauth-server": "^2.0.0",
"express-validator": "^6.12.2",
"gulp": "^4.0.2",
"gulp-sass": "^5.0.0",
"helmet": "^3.21.0",
"i18n": "^0.13.3",
"is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3",
"jsonwebtoken": "^8.5.1",
"knex": "^0.95.15",
"knex-cleaner": "^1.3.0",
"knex-db-manager": "^0.6.1",
"libphonenumber-js": "^1.9.6",
"lodash": "^4.17.15",
"lru-cache": "^6.0.0",
"mathjs": "^9.4.0",
"memory-cache": "^0.2.0",
"moment": "^2.24.0",
"moment-range": "^4.0.2",
"mongoose": "^5.10.0",
"mustache": "^3.0.3",
"mysql": "^2.17.1",
"mysql2": "^1.6.5",
"node-cache": "^4.2.1",
"nodemailer": "^6.3.0",
"nodemon": "^1.19.1",
"object-hash": "^2.0.3",
"objection": "^3.0.0",
"objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7",
"objection-unique": "^1.2.2",
"pluralize": "^8.0.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
"qim": "0.0.52",
"ramda": "^0.27.1",
"rate-limiter-flexible": "^2.1.14",
"reflect-metadata": "^0.1.13",
"rtl-detect": "^1.0.4",
"ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0",
"typedi": "^0.8.0",
"uniqid": "^5.2.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@types/lodash": "^4.14.158",
"@types/ramda": "^0.27.64",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"chai-things": "^0.2.0",
"colorette": "^1.2.0",
"commander": "^5.0.0",
"cross-env": "^5.2.0",
"eslint": "^8.33.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-import-resolver-webpack": "^0.11.1",
"eslint-loader": "^2.2.1",
"eslint-plugin-import": "^2.27.5",
"faker": "^4.1.0",
"getopts": "^2.2.5",
"gulp-postcss": "^9.0.0",
"gulp-rename": "^2.0.0",
"knex-factory": "0.0.6",
"merge-stream": "^2.0.0",
"mocha": "^5.2.0",
"npm-run-all": "^4.1.5",
"nyc": "^14.1.1",
"progress-bar-webpack-plugin": "^2.1.0",
"regenerator-runtime": "^0.13.7",
"rimraf": "^3.0.2",
"rtlcss": "^3.3.0",
"run-script-webpack-plugin": "^0.1.1",
"sass": "^1.37.5",
"sinon": "^7.4.2",
"start-server-webpack-plugin": "^2.2.5",
"ts-loader": "^9.4.2",
"ts-node": "^9.0.0",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^3.9.7",
"webpack": "^5.75.0",
"webpack-cli": "^4.10.0",
"webpack-merge": "^5.8.0",
"webpack-node-externals": "^3.0.0",
"webpack-watch-changed": "^1.0.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,640 @@
{
"Petty Cash": "العهدة",
"Cash": "النقدية",
"Bank": "المصرف",
"Other Income": "إيرادات اخري",
"Interest Income": "إيرادات الفوائد",
"Depreciation Expense": "مصاريف الاهلاك",
"Interest Expense": "مصروفات الفوائد",
"Sales of Product Income": "مبيعات دخل المنتجات",
"Inventory Asset": "المخزون",
"Cost of Goods Sold (COGS)": "تكلفة البضائع المباعة (COGS)",
"Cost of Goods Sold": "تكلفة البضاعة المباعة",
"Accounts Payable": "الذمم الدائنة",
"Other Expense": "مصاريف أخرى",
"Payroll Expenses": "مصاريف المرتبات",
"Fixed Asset": "أصول ثابتة",
"Credit Card": "بطاقة إئتمان",
"Non-Current Asset": "أصول غير متداولة",
"Current Asset": "أصول متداولة",
"Other Asset": "أصول اخري",
"Long Term Liability": "التزامات طويلة الاجل",
"Current Liability": "التزامات قصيرة الاجل",
"Other Liability": "التزمات اخري",
"Equity": "حقوق الملكية",
"Expense": "مصروف",
"Income": "إيراد",
"Accounts Receivable (A/R)": "الذمم المدينة",
"Accounts Receivable": "الذمم المدينة",
"Accounts Payable (A/P)": "الذمم الدائنة",
"Inactive": "غير نشط",
"Other Current Asset": "أصول متداولة اخرى",
"Tax Payable": "الضريبة المستحقة",
"Other Current Liability": "التزامات قصيرة الأجر اخرى",
"Non-Current Liability": "التزامات طويلة الأجر",
"Assets": "أصول",
"Liabilities": "الالتزمات",
"Account name": "أسم الحساب",
"Account type": "نوع الحساب",
"Account normal": "حساب عادي",
"Description": "وصف",
"Account code": "رمز الحساب",
"Currency": "عملة",
"Balance": "توازن",
"Active": "نشيط",
"Created at": "أنشئت في",
"fixed_asset": "أصل ثابت",
"Journal": "قيد",
"Reconciliation": "تسوية",
"Credit": "دائن",
"Debit": "مدين",
"Interest": "فائدة",
"Depreciation": "اهلاك",
"Payroll": "كشف رواتب",
"Type": "نوع",
"Name": "الأسم",
"Sellable": "قابل للبيع",
"Purchasable": "قابل للشراء",
"Sell price": "سعر البيع",
"Cost price": "سعر الكلفة",
"User": "المستخدم",
"Category": "تصنيف",
"Note": "ملحوظة",
"Quantity on hand": "كمية في اليد",
"Purchase description": "وصف الشراء",
"Sell description": "وصف البيع",
"Sell account": "حساب البيع",
"Cost account": "حساب التكلفة",
"Inventory account": "حساب المخزون",
"Payment date": "تاريخ الدفع",
"Payment account": "حساب الدفع",
"Amount": "كمية",
"Reference No.": "رقم المرجع.",
"Published": "نشرت",
"Journal number": "رقم القيد",
"Status": "حالة",
"Journal type": "نوع القيد",
"Date": "تاريخ",
"Asset": "أصل",
"Liability": "التزام",
"First-in first-out (FIFO)": "الوارد أولاً يصرف أولاً (FIFO)",
"Last-in first-out (LIFO)": "الوارد أخيرًا يصرف أولاً (LIFO)",
"Average rate": "المعدل المتوسط",
"Total": "الإجمالي",
"Transaction type": "نوع المعاملة",
"Transaction #": "عملية #",
"Running Value": "القيمة الجارية",
"Running quantity": "الكمية الجارية",
"Profit Margin": "هامش الربح",
"Value": "القيمة",
"Rate": "السعر",
"OPERATING ACTIVITIES": "الأنشطة التشغيلية",
"FINANCIAL ACTIVITIES": "الأنشطة التمويلية",
"INVESTMENT ACTIVITIES": "الانشطة الاستثمارية",
"Net income": "صافي الدخل",
"Adjustments net income by operating activities.": "تسويات صافي الدخل من الأنشطة التشغيلية.",
"Net cash provided by operating activities": "صافي التدفقات النقدية من أنشطة التشغيل",
"Net cash provided by investing activities": "صافي التدفقات النقدية من أنشطة الاستثمار",
"Net cash provided by financing activities": "صافي التدفقات النقدية من أنشطة التمويلية",
"Cash at beginning of period": "التدفقات النقدية في بداية الفترة",
"NET CASH INCREASE FOR PERIOD": "زيادة التدفقات النقدية للفترة",
"CASH AT END OF PERIOD": "صافي التدفقات النقدية في نهاية الفترة",
"Expenses": "مصاريف",
"Services": "خدمات",
"Inventory": "المخزون",
"Non Inventory": "غير المخزون",
"Draft": "مسودة",
"Delivered": "تم التوصيل",
"Overdue": "متأخر",
"Partially paid": "المدفوعة جزئيا",
"Paid": "مدفوع",
"Opened": "افتتح",
"Unpaid": "غير مدفوعة",
"Approved": "وافق",
"Rejected": "مرفوض",
"Invoiced": "مفوترة",
"Expired": "منتهي الصلاحية",
"Closed": "مغلق",
"Manual journal": "قيد اليدوي",
"Owner contribution": "زيادة رأس المال",
"Transfer to account": "تحويل إلى الحساب",
"Transfer from account": "تحويل من الحساب",
"Other income": "إيراد اخر",
"Other expense": "مصاريف أخرى",
"Owner drawing": "سحب رأس المال",
"Inventory adjustment": "تسوية المخزون",
"Customer opening balance": "الرصيد الافتتاحي للزبون",
"Vendor opening balance": "رصيد افتتاحي للمورد",
"Payment made": "سند الزبون",
"Bill": "فاتورة الشراء",
"Payment receive": "استلام الدفع",
"Sale receipt": "إيصال البيع",
"Sale invoice": "فاتورة البيع",
"Quantity": "الكمية",
"Bank Account": "حساب البنك",
"Saving Bank Account": "حساب التوفير البنكي",
"Undeposited Funds": "الأموال غير المودعة",
"Computer Equipment": "معدات كمبيوتر",
"Office Equipment": "معدات مكتبية",
"Uncategorized Income": "الدخل غير مصنف",
"Sales of Service Income": "دخل مبيعات الخدمات",
"Bank Fees and Charges": "رسوم المصرفية",
"Exchange Gain or Loss": "ربح أو خسارة فروقات الصرف",
"Rent": "إيجار",
"Office expenses": "مصاريف المكتب",
"Other Expenses": "مصاريف اخري",
"Drawings": "السحوبات",
"Owner's Equity": "حقوق الملكية",
"Opening Balance Equity": "الارصدة الافتتاحية ",
"Retained Earnings": "الأرباح المحتجزة",
"Sales Tax Payable": "ضريبة المبيعات المستحقة",
"Revenue Received in Advance": "الإيرادات المقبوضة مقدما",
"Opening Balance Liabilities": "رصيد الالتزامات الافتتاحي",
"Loan": "اقراض",
"Owner A Drawings": "مسحوبات المالك",
"An account that holds valuation of products or goods that availiable for sale.": "حساب يحمل قيم مخزون البضاعة أو السلع المتاحة للبيع.",
"Tracks the gain and losses of the exchange differences.": "يسجل مكاسب وخسائر فروق الصرف.",
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "يتم تسجيل أي رسوم مصرفية يتم فرضها في حساب الرسوم والمصروفات البنكية. ومن الأمثلة على ذلك رسوم صيانة الحساب المصرفي ورسوم المعاملات ورسوم الدفع المتأخر.",
"The income activities are not associated to the core business.": "لا ترتبط انشطة الدخل إلى الأعمال الأساسية.",
"Cash and cash equivalents": "النقد والنقد المكافئ",
"Inventories": "مخزون البضاعة",
"Other current assets": "الأصول متداولة الأخرى",
"Non-Current Assets": "أصول غير المتداولة",
"Current Liabilties": "التزامات متداولة",
"Long-Term Liabilities": "التزامات طويلة الاجل",
"Non-Current Liabilities": "التزامات غير متداولة",
"Liabilities and Equity": "التزامات وحقوق الملكية",
"Closing balance": "الرصيد الختامي",
"Opening balance": "الرصيد الفتاحي",
"Total {{accountName}}": "إجمالي {{accountName}}",
"invoice.paper.invoice": "فاتورة",
"invoice.paper.due_amount": "القيمة المستحقة",
"invoice.paper.billed_to": "فاتورة إلي",
"invoice.paper.invoice_date": "تاريخ الفاتورة",
"invoice.paper.invoice_number": "رقم الفاتورة",
"invoice.paper.due_date": "تاريخ الاستحقاق",
"invoice.paper.conditions_title": "الشروط والأحكام",
"invoice.paper.notes_title": "ملاحظات",
"invoice.paper.total": "المجموع",
"invoice.paper.balance_due": "مبلغ المستحق",
"invoice.paper.payment_amount": "مبلغ المدفوع",
"invoice.paper.invoice_amount": "قيمة الفاتورة",
"item_entry.paper.item_name": "اسم الصنف",
"item_entry.paper.rate": "السعر",
"item_entry.paper.quantity": "الكمية",
"item_entry.paper.total": "إجمالي",
"estimate.paper.estimate": "عرض أسعار",
"estimate.paper.billed_to": "عرض أسعار إلي",
"estimate.paper.estimate_date": "تاريخ العرض",
"estimate.paper.estimate_number": "رقم العرض",
"estimate.paper.expiration_date": "تاريخ انتهاء الصلاحية",
"estimate.paper.conditions_title": "الشروط والأحكام",
"estimate.paper.notes_title": "ملاحظات",
"estimate.paper.amount": "قيمة العرض",
"estimate.paper.subtotal": "المجموع",
"estimate.paper.total": "إجمالي",
"estimate.paper.estimate_amount": "قيمة العرض",
"receipt.paper.receipt": "إيصال",
"receipt.paper.billed_to": "الإيصال إلي",
"receipt.paper.receipt_date": "تاريخ الإيصال",
"receipt.paper.receipt_number": "رقم الإيصال",
"receipt.paper.conditions_title": "الشروط والأحكام",
"receipt.paper.notes_title": "ملاحظات",
"receipt.paper.receipt_amount": "قيمة الإيصال",
"receipt.paper.total": "إجمالي",
"receipt.paper.payment_amount": "مبلغ المدفوع",
"receipt.paper.balance_due": "مبلغ المستحق",
"receipt.paper.statement": "البيان",
"receipt.paper.notes": "ملاحظات",
"payment.paper.payment_receipt": "إيصال قبض",
"payment.paper.amount_received": "القيمة المستلمه",
"payment.paper.billed_to": "إيصال إلي",
"payment.paper.payment_date": "تاريخ الدفع",
"payment.paper.invoice_number": "رقم الفاتورة",
"payment.paper.invoice_date": "تاريخ الفاتورة",
"payment.paper.invoice_amount": "قيمة الفاتورة",
"payment.paper.payment_amount": "قيمة الدفع",
"payment.paper.balance_due": "المبلغ المستحق",
"payment.paper.statement": "البيان",
"credit.paper.credit_note": "اشعار دائن",
"credit.paper.amount": "قيمة الاشعار",
"credit.paper.remaining": "رصيد المتبقي",
"credit.paper.billed_to": "إيصال إلي",
"credit.paper.credit_date": "تاريخ الاشعار",
"credit.paper.terms_conditions": "الشروط والاحكام",
"credit.paper.notes": "ملاحظات",
"credit.paper.total": "إجمالي",
"credit.paper.credits_used": "قيمة المستخدمه",
"credit.paper.credits_remaining": "قيمة المتبقية",
"account.field.name": "إسم الحساب",
"account.field.description": "الوصف",
"account.field.slug": "Account slug",
"account.field.code": "رقم الحساب",
"account.field.root_type": "جذر الحساب",
"account.field.normal": "طبيعة الحساب",
"account.field.normal.credit": "دائن",
"account.field.normal.debit": "مدين",
"account.field.type": "نوع الحساب",
"account.field.active": "Activity",
"account.field.balance": "الرصيد",
"account.field.created_at": "أنشئت في",
"item.field.type": "نوع الصنف",
"item.field.type.inventory": "مخزون",
"item.field.type.service": "خدمة",
"item.field.type.non-inventory": "غير مخزون",
"item.field.name": "اسم الصنف",
"item.field.code": "رمز الصنف",
"item.field.sellable": "قابل للبيع",
"item.field.purchasable": "قابل للشراء",
"item.field.cost_price": "سعر التكلفة",
"item.field.cost_account": "حساب التكلفة",
"item.field.sell_account": "حساب البيع",
"item.field.sell_description": "وصف البيع",
"item.field.inventory_account": "حساب المخزون",
"item.field.purchase_description": "وصف الشراء",
"item.field.quantity_on_hand": "الكمية",
"item.field.note": "ملاحظة",
"item.field.category": "التصنيف",
"item.field.active": "Active",
"item.field.created_at": "أنشئت في",
"item_category.field.name": "الاسم",
"item_category.field.description": "الوصف",
"item_category.field.count": "العدد",
"item_category.field.created_at": "أنشئت في",
"invoice.field.customer": "الزبون",
"invoice.field.invoice_date": "تاريخ الفاتورة",
"invoice.field.due_date": "تاريخ الاستحقاق",
"invoice.field.invoice_no": "رقم الفاتورة",
"invoice.field.reference_no": "رقم الإشاري",
"invoice.field.invoice_message": "رسالة الفاتورة",
"invoice.field.terms_conditions": "الشروط والأحكام",
"invoice.field.amount": "القيمة",
"invoice.field.payment_amount": "القيمة المدفوعة",
"invoice.field.due_amount": "القيمة المستحقة",
"invoice.field.status": "الحالة",
"invoice.field.status.paid": "مدفوعة",
"invoice.field.status.partially-paid": "المدفوعة جزئيا",
"invoice.field.status.overdue": "متأخرة",
"invoice.field.status.unpaid": "غير مدفوعة",
"invoice.field.status.delivered": "تم تسليمها",
"invoice.field.status.draft": "مسودة",
"invoice.field.created_at": "أنشئت في",
"estimate.field.amount": "القيمة",
"estimate.field.estimate_number": "رقم العرض",
"estimate.field.customer": "الزبون",
"estimate.field.estimate_date": "تاريخ العرض",
"estimate.field.expiration_date": "تاريخ انتهاء الصلاحية",
"estimate.field.reference_no": "رقم الإشاري",
"estimate.field.note": "ملاحظة",
"estimate.field.terms_conditions": "الشروط والأحكام",
"estimate.field.status": "الحالة",
"estimate.field.status.delivered": "تم تسليمها",
"estimate.field.status.rejected": "مرفوضة",
"estimate.field.status.approved": "تم الموافقة",
"estimate.field.status.draft": "مسودة",
"estimate.field.created_at": "أنشئت في",
"payment_receive.field.customer": "الزبون",
"payment_receive.field.payment_date": "تاريخ الدفع",
"payment_receive.field.amount": "القيمة",
"payment_receive.field.reference_no": "رقم الإشاري",
"payment_receive.field.deposit_account": "حساب الإيداع",
"payment_receive.field.payment_receive_no": "رقم عملية الدفع",
"payment_receive.field.statement": "البيان",
"payment_receive.field.created_at": "أنشئت في",
"bill_payment.field.vendor": "المورد",
"bill_payment.field.amount": "القيمة",
"bill_payment.field.due_amount": "قيمة المستحقة",
"bill_payment.field.payment_account": "حساب الدفع",
"bill_payment.field.payment_number": "قيمة الدفع",
"bill_payment.field.payment_date": "تاريخ الدفع",
"bill_payment.field.reference_no": "رقم الإشاري",
"bill_payment.field.description": "الوصف",
"bill_payment.field.created_at": "أنشئت في",
"bill.field.vendor": "المورد",
"bill.field.bill_number": "رقم الفاتورة",
"bill.field.bill_date": "تاريخ الفاتورة",
"bill.field.due_date": "تاريخ الاستحقاق",
"bill.field.reference_no": "رقم الإشاري",
"bill.field.status": "الحالة",
"bill.field.status.paid": "مدفوعة",
"bill.field.status.partially-paid": "مدفوعة جزئيا",
"bill.field.status.unpaid": "غير مدفوعة",
"bill.field.status.opened": "مفتوحة",
"bill.field.status.draft": "مسودة",
"bill.field.status.overdue": "متأخرة",
"bill.field.amount": "القيمة",
"bill.field.payment_amount": "قيم الدفع",
"bill.field.note": "ملاحظة",
"bill.field.created_at": "أنشئت في",
"inventory_adjustment.field.date": "التاريخ",
"inventory_adjustment.field.type": "النوع",
"inventory_adjustment.field.type.increment": "زيادة",
"inventory_adjustment.field.type.decrement": "نقصان",
"inventory_adjustment.field.adjustment_account": "حساب التسوية",
"inventory_adjustment.field.reason": "السبب",
"inventory_adjustment.field.reference_no": "رقم الإشاري",
"inventory_adjustment.field.description": "الوصف",
"inventory_adjustment.field.published_at": "نشرت في",
"inventory_adjustment.field.created_at": "أنشئت في",
"expense.field.payment_date": "تاريخ الدفع",
"expense.field.payment_account": "حساب الدفع",
"expense.field.amount": "القيمة",
"expense.field.reference_no": "رقم الإشاري",
"expense.field.description": "الوصف",
"expense.field.published": "Published",
"expense.field.status": "الحالة",
"expense.field.status.draft": "مسودة",
"expense.field.status.published": "نشرت",
"expense.field.created_at": "أنشئت في",
"manual_journal.field.date": "التاريخ",
"manual_journal.field.journal_number": "رقم القيد",
"manual_journal.field.reference": "رقم الإشاري",
"manual_journal.field.journal_type": "نوع القيد",
"manual_journal.field.amount": "القيمة",
"manual_journal.field.description": "الوصف",
"manual_journal.field.status": "الحالة",
"manual_journal.field.created_at": "أنشئت في",
"receipt.field.amount": "القيمة",
"receipt.field.deposit_account": "حساب الإيداع",
"receipt.field.customer": "الزبون",
"receipt.field.receipt_date": "تاريخ الإيصال",
"receipt.field.receipt_number": "رقم الإيصال",
"receipt.field.reference_no": "رقم الإشاري",
"receipt.field.receipt_message": "رسالة الإيصال",
"receipt.field.statement": "البيان",
"receipt.field.created_at": "أنشئت في",
"receipt.field.status": "الحالة",
"receipt.field.status.draft": "مسودة",
"receipt.field.status.closed": "مغلقة",
"customer.field.first_name": "الاسم الأول",
"customer.field.last_name": "الاسم الاخير",
"customer.field.display_name": "اسم العرض",
"customer.field.email": "بريد الالكتروني",
"customer.field.work_phone": "هاتف عمل",
"customer.field.personal_phone": "هاتف شخصي",
"customer.field.company_name": "اسم الشركة",
"customer.field.website": "موقع الكتروني",
"customer.field.opening_balance_at": "الرصيد الافتتاحي في",
"customer.field.opening_balance": "الرصيد الافتتاحي",
"customer.field.created_at": "أنشئت في",
"customer.field.balance": "الرصيد",
"customer.field.status": "الحالة",
"customer.field.currency": "العملة",
"customer.field.status.active": "مفعل",
"customer.field.status.inactive": "غير مفعل",
"customer.field.status.overdue": "متأخر",
"customer.field.status.unpaid": "غير دافع",
"vendor.field.first_name": "الاسم الأول",
"vendor.field.last_name": "الاسم الاخير",
"vendor.field.display_name": "اسم العرض",
"vendor.field.email": "بريد الالكتروني",
"vendor.field.work_phone": "هاتف عمل",
"vendor.field.personal_phone": "هاتف شخصي",
"vendor.field.company_name": "اسم الشركة",
"vendor.field.website": "موقع الكتروني",
"vendor.field.opening_balance_at": "الرصيد الافتتاحي في",
"vendor.field.opening_balance": "الرصيد الافتتاحي",
"vendor.field.created_at": "أنشئت في",
"vendor.field.balance": "الرصيد",
"vendor.field.status": "الحالة",
"vendor.field.currency": "العملة",
"vendor.field.status.active": "مفعل",
"vendor.field.status.inactive": "غير مفعل",
"vendor.field.status.overdue": "متأخر",
"vendor.field.status.unpaid": "غير دافع",
"Invoice write-off": "شطب فاتورة",
"transaction_type.credit_note": "اشعار دائن",
"transaction_type.refund_credit_note": "استرجاع اموال اشعار دائن",
"transaction_type.vendor_credit": "اشعار مدين",
"transaction_type.refund_vendor_credit": "استرجاع اموال اشعار مدين",
"transaction_type.landed_cost": "تحميل تكلفة",
"sms_notification.invoice_details.label": "تفاصيل فاتورة البيع ",
"sms_notification.invoice_reminder.label": "تذكير بفاتورة البيع ",
"sms_notification.receipt_details.label": "تفاصيل إيصال البيع ",
"sms_notification.sale_estimate_details.label": "تفاصيل فاتورة عرض اسعار ",
"sms_notification.payment_receive_details.label": "تفاصيل سند الزبون",
"sms_notification.customer_balance.label": "رصيد الزبون",
"sms_notification.invoice_details.description": "سيتم إرسال إشعار عبر الرسائل القصيرة إلى العميل بمجرد إنشاء الفاتورة ونشرها أو عند إشعار العميل عبر رسالة نصية قصيرة بالفاتورة. ",
"sms_notification.payment_receive.description": "سيتم إرسال إشعار رسالة شكر للدفع إلى العميل بمجرد إنشاء الدفعة ونشرها أو إشعار العميل بالدفع يدويًا. ",
"sms_notification.receipt_details.description": "سيتم إرسال إشعار عبر الرسائل القصيرة إلى العميل بمجرد إنشاء ونشر الإيصال أو عند إشعار العميل بالإيصال يدويًا.",
"sms_notification.customer_balance.description": "إرسال رسالة نصية قصيرة إشعار العملاء برصيدهم الحالي المستحق. ",
"sms_notification.estimate_details.description": "سيتم إرسال إشعار عبر الرسائل القصيرة إلى عميلك بمجرد نشر العرض أو إشعار العميل بالعرض يدويًا.",
"sms_notification.invoice_reminder.description": "سيتم ارسال إشعار SMS لتذكير الزبون بالدفع باكراً ، سواء ارسال بشكل تلقائي او يدوي.",
"sms_notification.customer_balance.default_message": "عزيزي {CustomerName} ، هذا تذكير بشأن رصيد الحالي المستحق {Balance} ، يُرجى الدفع في أقرب وقت ممكن. - {CompanyName}",
"sms_notification.payment_receive.default_message": "مرحبًا {CustomerName} ، تم القبض بقيمة {Amount} للفاتورة - {InvoiceNumber}. نحن نتطلع إلى خدمتك مرة أخرى. شكرا لك. - {CompanyName}",
"sms_notification.estimate.default_message": "مرحبًا , {CustomerName} ، تم أنشاء فاتورة عرض اسعار - {EstimateNumber} لك. يرجى إلقاء نظرة وقبوله للمضي قدما. بانتظار ردك. - {CompanyName}",
"sms_notification.invoice_details.default_message": "مرحبًا {CustomerName}, لديك مبلغ مستحق قدره {DueAmount} للفاتورة {InvoiceNumber}. - {CompanyName}",
"sms_notification.receipt_details.default_message": "مرحبًا {CustomerName} ، لقد تم إنشاء إيصال - {ReceiptNumber} من أجلك. نتطلع إلى خدمتك مرة أخرى. شكرًا لك - {CompanyName}",
"sms_notification.invoice_reminder.default_message": "عزيزي {CustomerName} ، يرجي سداد فاتورة - {InvoiceNumber} المستحقة. يرجى الدفع قبل تاريخ {DueDate}. شكرا لك. - {CompanyName}",
"module.sale_invoices.label": "فواتير البيع",
"module.sale_receipts.label": "إيصالات البيع",
"module.sale_estimates.label": "فاتورة عرض اسعار ",
"module.payment_receives.label": "سندات الزبائن ",
"module.customers.label": "العملاء",
"sms_notification.invoice.var.invoice_number": "يشير إلى رقم الفاتورة.",
"sms_notification.invoice.var.reference_number": "يشير إلى رقم إشاري للفاتورة.",
"sms_notification.invoice.var.customer_name": "يشير إلى اسم العميل الفاتورة",
"sms_notification.invoice.var.due_amount": "يشير إلى مبلغ الفاتورة المستحق",
"sms_notification.invoice.var.amount": "يشير إلى مبلغ الفاتورة.",
"sms_notification.invoice.var.company_name": "يشير إلي اسم الشركة.",
"sms_notification.invoice.var.due_date": "يشير إلي تاريخ استحقاق الفاتورة.",
"sms_notification.receipt.var.receipt_number": "يشير إلى رقم الإيصال.",
"sms_notification.receipt.var.reference_number": "يشير إلى رقم الإشاري للإيصال.",
"sms_notification.receipt.var.customer_name": "يشير إلى اسم العميل الإيصال.",
"sms_notification.receipt.var.amount": "يشير إلى مبلغ الإيصال. ",
"sms_notification.receipt.var.company_name": "يشير إلي اسم الشركة.",
"sms_notification.payment.var.payment_number": "يشير إلى رقم معاملة الدفع.",
"sms_notification.payment.var.reference_number": "يشير إلى رقم الإشاري لعملية الدفع ",
"sms_notification.payment.var.customer_name": "يشير إلى اسم العميل الدفع",
"sms_notification.payment.var.amount": "يشير إلى مبلغ معاملة الدفع.",
"sms_notification.payment.company_name": "يشير إلي اسم الشركة.",
"sms_notification.payment.var.invoice_number": "يشير إلي رقم فاتورة التي تم دفعها.",
"sms_notification.estimate.var.estimate_number": "يشير إلى رقم فاتورة عرض اسعار.",
"sms_notification.estimate.var.reference_number": "يشير إلى رقم الإشاري لفاتورة عرض اسعار.",
"sms_notification.estimate.var.customer_name": "يشير إلى اسم العميل الفاتورة",
"sms_notification.estimate.var.amount": "يشير إلى قيمة الفاتورة",
"sms_notification.estimate.var.company_name": "يشير إلي اسم الشركة.",
"sms_notification.estimate.var.expiration_date": "يشير إلي تاريخ الصلاحية الفاتورة.",
"sms_notification.estimate.var.estimate_date": "يشير إلي تاريخ الفاتورة.",
"sms_notification.customer.var.customer_name": "يشير إلي اسم الزبون",
"sms_notification.customer.var.balance": "يشير إلي رصيد زبون المستحق.",
"sms_notification.customer.var.company_name": "يشير إلي اسم الشركة.",
"ability.accounts": "شجرة الحسابات",
"ability.manual_journal": "القيود اليدوية",
"ability.cashflow": "التدفقات النقدية",
"ability.inventory_adjustment": "تسويات المخزون",
"ability.customers": "الزبائن",
"ability.vendors": "الموردين",
"ability.sale_estimates": "فواتير عرض الاسعار",
"ability.sale_invoices": "فواتير البيع",
"ability.sale_receipts": "إيصالات البيع",
"ability.expenses": "المصاريف",
"ability.payments_receive": "سندات الزبائن",
"ability.purchase_invoices": "فواتير الشراء",
"ability.all_reports": "كل التقارير",
"ability.payments_made": "سندات الموردين",
"ability.preferences": "التفضيلات",
"ability.mutate_system_preferences": "تعديل تفضيلات النظام.",
"ability.items": "الأصناف",
"ability.view": "عرض",
"ability.create": "إضافة",
"ability.edit": "تعديل",
"ability.delete": "حذف",
"ability.transactions_locking": "إمكانية اغلاق المعاملات.",
"ability.balance_sheet_report": "ميزانية العمومية",
"ability.profit_loss_sheet": "قائمة الدخل",
"ability.journal": "اليومية العامة",
"ability.general_ledger": "دفتر الأستاذ العام",
"ability.cashflow_report": "تقرير التدفقات النقدية",
"ability.AR_aging_summary_report": "ملخص اعمار الديون للذمم المدينة",
"ability.AP_aging_summary_report": "ملخص اعمار الديون للذمم الدائنة",
"ability.purchases_by_items": "المشتريات حسب المنتجات",
"ability.sales_by_items_report": "المبيعات حسب المنتجات",
"ability.customers_transactions_report": "معاملات الزبائن",
"ability.vendors_transactions_report": "معاملات الموردين",
"ability.customers_summary_balance_report": "ملخص أرصدة الزبائن",
"ability.vendors_summary_balance_report": "ملخص أرصدة الموردين",
"ability.inventory_valuation_summary": "ملخص تقييم المخزون",
"ability.inventory_items_details": "تفاصيل منتج المخزون",
"vendor_credit.field.vendor": "المورد",
"vendor_credit.field.amount": "القيمة",
"vendor_credit.field.currency_code": "العملة",
"vendor_credit.field.credit_date": "تاريخ الاشعار",
"vendor_credit.field.credit_number": "رقم الاشعار",
"vendor_credit.field.note": "ملاحظة",
"vendor_credit.field.created_at": "أنشئت في",
"vendor_credit.field.reference_no": "رقم الإشاري",
"vendor_credit.field.status": "الحالة",
"vendor_credit.field.status.draft": "مسودة",
"vendor_credit.field.status.published": "تم نشرها",
"vendor_credit.field.status.open": "مفتوحة",
"vendor_credit.field.status.closed": "مغلقة",
"credit_note.field.terms_conditions": "الشروط والاحكام",
"credit_note.field.note": "ملاحظة",
"credit_note.field.currency_code": "العملة",
"credit_note.field.created_at": "أنشئت في",
"credit_note.field.amount": "القيمة",
"credit_note.field.credit_note_number": "رقم الاشعار",
"credit_note.field.credit_note_date": "تاريخ الاشعار",
"credit_note.field.customer": "الزبون",
"credit_note.field.reference_no": "رقم الإشاري",
"credit_note.field.status": "الحالة",
"credit_note.field.status.draft": "مسودة",
"credit_note.field.status.published": "تم نشرها",
"credit_note.field.status.open": "مفتوحة",
"credit_note.field.status.closed": "مغلقة",
"transactions_locking.module.sales.label": "المبيعات",
"transactions_locking.module.purchases.label": "المشتريات",
"transactions_locking.module.financial.label": "المالية",
"transactions_locking.module.all_transactions": "كل المعاملات",
"transactions_locking.module.sales.desc": "فواتير البيع ، والإيصالات ، والإشعارات الدائنة ، واستلام مدفوعات الزبائن ، والأرصدة الافتتاحية للزبائن.",
"transactions_locking.module.purchases.desc": "فواتير الشراء ومدفوعات الموردين وإشعارات المدينة والأرصدة الافتتاحية للموردين.",
"transactions_locking.module.financial.desc": "القيود اليدوية والمصروفات وتسويات المخزون.",
"inventory_adjustment.type.increment": "زيادة",
"inventory_adjustment.type.decrement": "نقصان",
"customer.type.individual": "فرد",
"customer.type.business": "اعمال",
"credit_note.view.draft": "مسودة",
"credit_note.view.closed": "مغلقة",
"credit_note.view.open": "مفتوحة",
"credit_note.view.published": "نشرت",
"vendor_credit.view.draft": "مسودة",
"vendor_credit.view.closed": "مغلقة",
"vendor_credit.view.open": "مفتوحة",
"vendor_credit.view.published": "نشرت",
"allocation_method.value.label": "القيمة",
"allocation_method.quantity.label": "الكمية",
"balance_sheet.assets": "الأصول",
"balance_sheet.current_asset": "الأصول المتداولة",
"balance_sheet.cash_and_cash_equivalents": "النقدية وما يعادلها",
"balance_sheet.accounts_receivable": "الذمم المدينة",
"balance_sheet.inventory": "المخزون",
"balance_sheet.other_current_assets": "اصول متداولة اخرى",
"balance_sheet.fixed_asset": "الأصول الثابتة",
"balance_sheet.non_current_assets": "الاصول غير المتداولة",
"balance_sheet.liabilities_and_equity": "الالتزامات وحقوق الملكية",
"balance_sheet.liabilities": "الإلتزامات",
"balance_sheet.current_liabilties": "الالتزامات المتداولة",
"balance_sheet.long_term_liabilities": "الالتزامات طويلة الاجل",
"balance_sheet.non_current_liabilities": "الالتزامات غير المتداولة",
"balance_sheet.equity": "حقوق الملكية",
"balance_sheet.account_name": "اسم الحساب",
"balance_sheet.total": "إجمالي",
"balance_sheet.percentage_of_column": "٪ التغير العمودي",
"balance_sheet.percentage_of_row": "٪ التغير الأفقي",
"financial_sheet.previoud_period_date": "(ف.س) {{date}}",
"fianncial_sheet.previous_period_change": "التغيرات (ف.س)",
"financial_sheet.previous_period_percentage": "٪ التغير (ف.س)",
"financial_sheet.previous_year_date": "(س.س) {{date}}",
"financial_sheet.previous_year_change": "التغيرات (س.س)",
"financial_sheet.previous_year_percentage": "٪ التغير (س.س)",
"financial_sheet.total_row": "إجمالي {{value}}",
"profit_loss_sheet.income": "الإيرادات",
"profit_loss_sheet.cost_of_sales": "تكلفة المبيعات",
"profit_loss_sheet.gross_profit": "إجمالي الدخل",
"profit_loss_sheet.expenses": "المصروفات",
"profit_loss_sheet.net_operating_income": "صافي الدخل التشغيلي",
"profit_loss_sheet.other_income": "إيرادات اخري",
"profit_loss_sheet.other_expenses": "مصاريف اخري",
"profit_loss_sheet.net_income": "صافي الدخل",
"profit_loss_sheet.account_name": "اسم الحساب",
"profit_loss_sheet.total": "إجمالي",
"profit_loss_sheet.percentage_of_income": "٪ التغير في الإيرادات",
"profit_loss_sheet.percentage_of_expenses": "٪ التغير في المصاريف",
"profit_loss_sheet.percentage_of_column": "٪ التغير العمودي",
"profit_loss_sheet.percentage_of_row": "٪ التغير الأفقي",
"warehouses.primary_warehouse": "المستودع الرئيسي",
"branches.head_branch": "الفرع الرئيسي",
"account.accounts_payable.currency": "الذمم الدائنة - {{currency}}",
"account.accounts_receivable.currency": "الذمم المدينة - {{currency}}",
"role.admin.name": "الادارة",
"role.admin.desc": "وصول غير مقيد لجميع الوحدات.",
"role.staff.name": "العاملين",
"role.staff.desc": "الوصول إلى جميع الوحدات باستثناء التقارير والإعدادات والمحاسبة.",
"warehouse_transfer.view.draft.name": "مسودة",
"warehouse_transfer.view.in_transit.name": "في النقل",
"warehouse_transfer.view.transferred.name": "تم النقل"
}

View File

@@ -0,0 +1,641 @@
{
"Petty Cash": "Petty Cash",
"Cash": "Cash",
"Bank": "Bank",
"Other Income": "Other Income",
"Interest Income": "Interest Income",
"Depreciation Expense": "Depreciation Expense",
"Interest Expense": "Interest Expense",
"Sales of Product Income": "Sales of Product Income",
"Inventory Asset": "Inventory Asset",
"Cost of Goods Sold (COGS)": "Cost of Goods Sold (COGS)",
"Cost of Goods Sold": "Cost of Goods Sold",
"Accounts Payable": "Accounts Payable",
"Other Expense": "Other Expense",
"Payroll Expenses": "Payroll Expenses",
"Fixed Asset": "Fixed Asset",
"Credit Card": "Credit Card",
"Non-Current Asset": "Non-Current Asset",
"Current Asset": "Current Asset",
"Other Asset": "Other Asset",
"Long Term Liability": "Long Term Liability",
"Current Liability": "Current Liability",
"Other Liability": "Other Liability",
"Equity": "Equity",
"Expense": "Expense",
"Income": "Income",
"Accounts Receivable (A/R)": "Accounts Receivable (A/R)",
"Accounts Receivable": "Accounts Receivable",
"Accounts Payable (A/P)": "Accounts Payable (A/P)",
"Inactive": "Inactive",
"Other Current Asset": "Other Current Asset",
"Tax Payable": "Tax Payable",
"Other Current Liability": "Other Current Liability",
"Non-Current Liability": "Non-Current Liability",
"Assets": "Assets",
"Liabilities": "Liabilities",
"Account name": "Account name",
"Account type": "Account type",
"Account normal": "Account normal",
"Description": "Description",
"Account code": "Account code",
"Currency": "Currency",
"Balance": "Balance",
"Active": "Active",
"Created at": "Created at",
"fixed_asset": "Fixed asset",
"Journal": "Journal",
"Reconciliation": "Reconciliation",
"Credit": "Credit",
"Debit": "Debit",
"Interest": "Interest",
"Depreciation": "Depreciation",
"Payroll": "Payroll",
"Type": "Type",
"Name": "Name",
"Sellable": "Sellable",
"Purchasable": "Purchasable",
"Sell price": "Sell price",
"Cost price": "Cost price",
"User": "User",
"Category": "Category",
"Note": "Note",
"Quantity on hand": "Quantity on hand",
"Quantity": "Quantity",
"Purchase description": "Purchase description",
"Sell description": "Sell description",
"Sell account": "Sell account",
"Cost account": "Cost account",
"Inventory account": "Inventory account",
"Payment date": "Payment date",
"Payment account": "Payment account",
"Amount": "Amount",
"Reference No.": "Reference No.",
"Journal number": "Journal number",
"Status": "Status",
"Journal type": "Journal type",
"Date": "Date",
"Asset": "Asset",
"Liability": "Liability",
"First-in first-out (FIFO)": "First-in first-out (FIFO)",
"Last-in first-out (LIFO)": "Last-in first-out (LIFO)",
"Average rate": "Average rate",
"Total": "Total",
"Transaction type": "Transaction type",
"Transaction #": "Transaction #",
"Running Value": "Running Value",
"Running quantity": "Running quantity",
"Profit Margin": "Profit Margin",
"Value": "Value",
"Rate": "Rate",
"OPERATING ACTIVITIES": "OPERATING ACTIVITIES",
"FINANCIAL ACTIVITIES": "FINANCIAL ACTIVITIES",
"Net income": "Net income",
"Adjustments net income by operating activities.": "Adjustments net income by operating activities.",
"Net cash provided by operating activities": "Net cash provided by operating activities",
"Net cash provided by investing activities": "Net cash provided by investing activities",
"Net cash provided by financing activities": "Net cash provided by financing activities",
"Cash at beginning of period": "Cash at beginning of period",
"NET CASH INCREASE FOR PERIOD": "NET CASH INCREASE FOR PERIOD",
"CASH AT END OF PERIOD": "CASH AT END OF PERIOD",
"Expenses": "Expenses",
"Services": "Services",
"Inventory": "Inventory",
"Non Inventory": "Non Inventory",
"Draft": "Draft",
"Published": "Published",
"Delivered": "Delivered",
"Overdue": "Overdue",
"Partially paid": "Partially paid",
"Paid": "Paid",
"Opened": "Opened",
"Unpaid": "Unpaid",
"Approved": "Approved",
"Rejected": "Rejected",
"Invoiced": "Invoiced",
"Expired": "Expired",
"Closed": "Closed",
"Manual journal": "Manual journal",
"Owner contribution": "Owner contribution",
"Transfer to account": "Transfer to account",
"Transfer from account": "Transfer from account",
"Other income": "Other income",
"Other expense": "Other expense",
"Owner drawing": "Owner drawing",
"Inventory adjustment": "Inventory adjustment",
"Customer opening balance": "Customer opening balance",
"Vendor opening balance": "Vendor opening balance",
"Payment made": "Payment made",
"Bill": "Bill",
"Payment receive": "Payment receive",
"Sale receipt": "Sale receipt",
"Sale invoice": "Sale invoice",
"Bank Account": "Bank Account",
"Saving Bank Account": "Saving Bank Account",
"Undeposited Funds": "Undeposited Funds",
"Computer Equipment": "Computer Equipment",
"Office Equipment": "Office Equipment",
"Uncategorized Income": "Uncategorized Income",
"Sales of Service Income": "Sales of Service Income",
"Bank Fees and Charges": "Bank Fees and Charges",
"Exchange Gain or Loss": "Exchange Gain or Loss",
"Rent": "Rent",
"Office expenses": "Office expenses",
"Other Expenses": "Other Expenses",
"Drawings": "Drawings",
"Owner's Equity": "Owner's Equity",
"Opening Balance Equity": "Opening Balance Equity",
"Retained Earnings": "Retained Earnings",
"Sales Tax Payable": "Sales Tax Payable",
"Revenue Received in Advance": "Revenue Received in Advance",
"Opening Balance Liabilities": "Opening Balance Liabilities",
"Loan": "Loan",
"Owner A Drawings": "Owner A Drawings",
"An account that holds valuation of products or goods that availiable for sale.": "An account that holds valuation of products or goods that availiable for sale.",
"Tracks the gain and losses of the exchange differences.": "Tracks the gain and losses of the exchange differences.",
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.",
"The income activities are not associated to the core business.": "The income activities are not associated to the core business.",
"Cash and cash equivalents": "Cash and cash equivalents",
"Inventories": "Inventories",
"Other current assets": "Other current assets",
"Non-Current Assets": "Non-Current Assets",
"Current Liabilties": "Current Liabilties",
"Long-Term Liabilities": "Long-Term Liabilities",
"Non-Current Liabilities": "Non-Current Liabilities",
"Liabilities and Equity": "Liabilities and Equity",
"Closing balance": "Closing balance",
"Opening Balance": "Opening balance",
"Total {{accountName}}": "Total {{accountName}}",
"invoice.paper.invoice": "Invoice",
"invoice.paper.invoice_amount": "Invoice amount",
"invoice.paper.due_amount": "Due amount",
"invoice.paper.billed_to": "Billed to",
"invoice.paper.invoice_date": "Invoice date",
"invoice.paper.invoice_number": "Invoice No.",
"invoice.paper.due_date": "Due date",
"invoice.paper.conditions_title": "Conditions & terms",
"invoice.paper.notes_title": "Notes",
"invoice.paper.total": "Total",
"invoice.paper.payment_amount": "Payment Amount",
"invoice.paper.balance_due": "Balance Due",
"item_entry.paper.item_name": "Item name",
"item_entry.paper.rate": "Rate",
"item_entry.paper.quantity": "Quantity",
"item_entry.paper.total": "Total",
"estimate.paper.estimate": "Estimate",
"estimate.paper.estimate_amount": "Estimate amount",
"estimate.paper.billed_to": "Billed to",
"estimate.paper.estimate_date": "Estimate date",
"estimate.paper.estimate_number": "Estimate number",
"estimate.paper.expiration_date": "Expiration date",
"estimate.paper.conditions_title": "Conditions & terms",
"estimate.paper.notes_title": "Notes",
"estimate.paper.amount": "Estimate amount",
"estimate.paper.subtotal": "Subtotal",
"estimate.paper.total": "Total",
"receipt.paper.receipt": "Receipt",
"receipt.paper.billed_to": "Billed to",
"receipt.paper.receipt_date": "Receipt date",
"receipt.paper.receipt_number": "Receipt number",
"receipt.paper.expiration_date": "Expiration date",
"receipt.paper.conditions_title": "Conditions & terms",
"receipt.paper.notes": "Notes",
"receipt.paper.statement": "Statement",
"receipt.paper.receipt_amount": "Receipt amount",
"receipt.paper.total": "Total",
"receipt.paper.balance_due": "Balance Due",
"receipt.paper.payment_amount": "Payment Amount",
"credit.paper.credit_note": "Credit Note",
"credit.paper.remaining": "Credit remaining",
"credit.paper.amount": "Credit amount",
"credit.paper.billed_to": "Bill to",
"credit.paper.credit_date": "Credit date",
"credit.paper.total": "Total",
"credit.paper.credits_used": "Credits used",
"credit.paper.credits_remaining": "Credits remaining",
"credit.paper.conditions_title": "Conditions & terms",
"credit.paper.notes": "Notes",
"payment.paper.payment_receipt": "Payment Receipt",
"payment.paper.amount_received": "Amount received",
"payment.paper.billed_to": "Billed to",
"payment.paper.payment_date": "Payment date",
"payment.paper.invoice_number": "Invoice number",
"payment.paper.invoice_date": "Invoice date",
"payment.paper.invoice_amount": "Invoice amount",
"payment.paper.payment_amount": "Payment amount",
"payment.paper.balance_due": "Balance Due",
"payment.paper.statement": "Statement",
"account.field.name": "Account name",
"account.field.description": "Description",
"account.field.slug": "Account slug",
"account.field.code": "Account code",
"account.field.root_type": "Root type",
"account.field.normal": "Account normal",
"account.field.normal.credit": "Credit",
"account.field.normal.debit": "Debit",
"account.field.type": "Type",
"account.field.active": "Activity",
"account.field.balance": "Balance",
"account.field.created_at": "Created at",
"item.field.type": "Item type",
"item.field.type.inventory": "Inventory",
"item.field.type.service": "Service",
"item.field.type.non-inventory": "Non inventory",
"item.field.name": "Name",
"item.field.code": "Code",
"item.field.sellable": "Sellable",
"item.field.purchasable": "Purchasable",
"item.field.cost_price": "Cost price",
"item.field.cost_account": "Cost account",
"item.field.sell_account": "Sell account",
"item.field.sell_description": "Sell description",
"item.field.inventory_account": "Inventory account",
"item.field.purchase_description": "Purchase description",
"item.field.quantity_on_hand": "Quantity on hand",
"item.field.note": "Note",
"item.field.category": "Category",
"item.field.active": "Active",
"item.field.created_at": "Created at",
"item_category.field.name": "Name",
"item_category.field.description": "Description",
"item_category.field.count": "Count",
"item_category.field.created_at": "Created at",
"invoice.field.customer": "Customer",
"invoice.field.invoice_date": "Invoice date",
"invoice.field.due_date": "Due date",
"invoice.field.invoice_no": "Invoice No.",
"invoice.field.reference_no": "Reference No.",
"invoice.field.invoice_message": "Invoice message",
"invoice.field.terms_conditions": "Terms & conditions",
"invoice.field.amount": "Amount",
"invoice.field.payment_amount": "Payment amount",
"invoice.field.due_amount": "Due amount",
"invoice.field.status": "Status",
"invoice.field.status.paid": "Paid",
"invoice.field.status.partially-paid": "Partially paid",
"invoice.field.status.overdue": "Overdue",
"invoice.field.status.unpaid": "Unpaid",
"invoice.field.status.delivered": "Delivered",
"invoice.field.status.draft": "Draft",
"invoice.field.created_at": "Created at",
"estimate.field.amount": "Amount",
"estimate.field.estimate_number": "Estimate number",
"estimate.field.customer": "Customer",
"estimate.field.estimate_date": "Estimate date",
"estimate.field.expiration_date": "Expiration date",
"estimate.field.reference_no": "Reference No.",
"estimate.field.note": "Note",
"estimate.field.terms_conditions": "Terms & conditions",
"estimate.field.status": "Status",
"estimate.field.status.delivered": "Delivered",
"estimate.field.status.rejected": "Rejected",
"estimate.field.status.approved": "Approved",
"estimate.field.status.draft": "Draft",
"estimate.field.created_at": "Created at",
"payment_receive.field.customer": "Customer",
"payment_receive.field.payment_date": "Payment date",
"payment_receive.field.amount": "Amount",
"payment_receive.field.reference_no": "Reference No.",
"payment_receive.field.deposit_account": "Deposit account",
"payment_receive.field.payment_receive_no": "Payment receive No.",
"payment_receive.field.statement": "Statement",
"payment_receive.field.created_at": "Created at",
"bill_payment.field.vendor": "Vendor",
"bill_payment.field.amount": "Amount",
"bill_payment.field.due_amount": "Due amount",
"bill_payment.field.payment_account": "Payment account",
"bill_payment.field.payment_number": "Payment number",
"bill_payment.field.payment_date": "Payment date",
"bill_payment.field.reference_no": "Reference No.",
"bill_payment.field.description": "Description",
"bill_payment.field.created_at": "Created at",
"bill.field.vendor": "Vendor",
"bill.field.bill_number": "Bill number",
"bill.field.bill_date": "Bill date",
"bill.field.due_date": "Due date",
"bill.field.reference_no": "Reference No.",
"bill.field.status": "Status",
"bill.field.status.paid": "Paid",
"bill.field.status.partially-paid": "Partially paid",
"bill.field.status.unpaid": "Unpaid",
"bill.field.status.opened": "Opened",
"bill.field.status.draft": "Draft",
"bill.field.status.overdue": "overdue",
"bill.field.amount": "Amount",
"bill.field.payment_amount": "Payment amount",
"bill.field.note": "Note",
"bill.field.created_at": "Created at",
"inventory_adjustment.field.date": "Date",
"inventory_adjustment.field.type": "Type",
"inventory_adjustment.field.type.increment": "Increment",
"inventory_adjustment.field.type.decrement": "Decrement",
"inventory_adjustment.field.adjustment_account": "Adjustment account",
"inventory_adjustment.field.reason": "Reason",
"inventory_adjustment.field.reference_no": "Reference No.",
"inventory_adjustment.field.description": "Description",
"inventory_adjustment.field.published_at": "Published at",
"inventory_adjustment.field.created_at": "Created at",
"expense.field.payment_date": "Payment date",
"expense.field.payment_account": "Payment account",
"expense.field.amount": "Amount",
"expense.field.reference_no": "Reference No.",
"expense.field.description": "Description",
"expense.field.published": "Published",
"expense.field.status": "Status",
"expense.field.status.draft": "Draft",
"expense.field.status.published": "Published",
"expense.field.created_at": "Created at",
"manual_journal.field.date": "Date",
"manual_journal.field.journal_number": "Journal number",
"manual_journal.field.reference": "Reference No.",
"manual_journal.field.journal_type": "Journal type",
"manual_journal.field.amount": "Amount",
"manual_journal.field.description": "Description",
"manual_journal.field.status": "Status",
"manual_journal.field.created_at": "Created at",
"receipt.field.amount": "Amount",
"receipt.field.deposit_account": "Deposit account",
"receipt.field.customer": "Customer",
"receipt.field.receipt_date": "Receipt date",
"receipt.field.receipt_number": "Receipt number",
"receipt.field.reference_no": "Reference No.",
"receipt.field.receipt_message": "Receipt message",
"receipt.field.statement": "Statement",
"receipt.field.created_at": "Created at",
"receipt.field.status": "Status",
"receipt.field.status.draft": "Draft",
"receipt.field.status.closed": "Closed",
"customer.field.first_name": "First name",
"customer.field.last_name": "Last name",
"customer.field.display_name": "Display name",
"customer.field.email": "Email",
"customer.field.work_phone": "Work phone",
"customer.field.personal_phone": "Personal phone",
"customer.field.company_name": "Company name",
"customer.field.website": "Website",
"customer.field.opening_balance_at": "Opening balance at",
"customer.field.opening_balance": "Opening balance",
"customer.field.created_at": "Created at",
"customer.field.balance": "Balance",
"customer.field.status": "Status",
"customer.field.currency": "Curreny",
"customer.field.status.active": "Active",
"customer.field.status.inactive": "Inactive",
"customer.field.status.overdue": "Overdue",
"customer.field.status.unpaid": "Unpaid",
"vendor.field.first_name": "First name",
"vendor.field.last_name": "Last name",
"vendor.field.display_name": "Display name",
"vendor.field.email": "Email",
"vendor.field.work_phone": "Work phone",
"vendor.field.personal_phone": "Personal phone",
"vendor.field.company_name": "Company name",
"vendor.field.website": "Website",
"vendor.field.opening_balance_at": "Opening balance at",
"vendor.field.opening_balance": "Opening balance",
"vendor.field.created_at": "Created at",
"vendor.field.balance": "Balance",
"vendor.field.status": "Status",
"vendor.field.currency": "Curreny",
"vendor.field.status.active": "Active",
"vendor.field.status.inactive": "Inactive",
"vendor.field.status.overdue": "Overdue",
"vendor.field.status.unpaid": "Unpaid",
"Invoice write-off": "Invoice write-off",
"transaction_type.credit_note": "Credit note",
"transaction_type.refund_credit_note": "Refund credit note",
"transaction_type.vendor_credit": "Vendor credit",
"transaction_type.refund_vendor_credit": "Refund vendor credit",
"transaction_type.landed_cost": "Landed cost",
"sms_notification.invoice_details.label": "Sale invoice details",
"sms_notification.invoice_reminder.label": "Sale invoice reminder",
"sms_notification.receipt_details.label": "Sale receipt details",
"sms_notification.sale_estimate_details.label": "Sale estimate details",
"sms_notification.payment_receive_details.label": "Payment receive details",
"sms_notification.customer_balance.label": "Customer balance",
"sms_notification.invoice_details.description": "SMS notification will be sent to your customer once invoice created and published or when notify customer via SMS about the invoice.",
"sms_notification.payment_receive.description": "Payment thank you message notification will be sent to customer once the payment created and published or notify customer about payment manually.",
"sms_notification.receipt_details.description": "SMS notification will be sent to your cusotmer once receipt created and published or when notify customer about the receipt manually.",
"sms_notification.customer_balance.description": "Send SMS to notify customers about their current outstanding balance.",
"sms_notification.estimate_details.description": "SMS notification will be sent to your customer once estimate publish or notify customer about estimate manually.",
"sms_notification.invoice_reminder.description": "SMS notification will be sent to remind the customer to pay earliest, either automatically or manually.",
"sms_notification.customer_balance.default_message": "Dear {CustomerName}, This is reminder about your current outstanding balance of {Balance}, Please pay at the earliest. - {CompanyName}",
"sms_notification.payment_receive.default_message": "'Hi, {CustomerName}, We have received your payment for the invoice - {InvoiceNumber}. We look forward to serving you again. Thank you. - {CompanyName}'",
"sms_notification.estimate.default_message": "Hi, {CustomerName}, We have created an estimate - {EstimateNumber} for you. Please take a look and accept it to proceed further. Looking forward to hearing from you. - {CompanyName}",
"sms_notification.invoice_details.default_message": "Hi, {CustomerName}, You have an outstanding amount of {DueAmount} for the invoice {InvoiceNumber}. - {CompanyName}",
"sms_notification.receipt_details.default_message": "Hi, {CustomerName}, We have created receipt - {ReceiptNumber} for you. we look forward to serveing you again. Thank your - {CompanyName}",
"sms_notification.invoice_reminder.default_message": "Dear {CustomerName}, The payment towards the invoice - {InvoiceNumber} is due. Please pay before {DueDate}. Thank you. - {CompanyName}",
"module.sale_invoices.label": "Sale invoices",
"module.sale_receipts.label": "Sale receipts",
"module.sale_estimates.label": "Sale estimates",
"module.payment_receives.label": "Payment receive",
"module.customers.label": "Customers",
"sms_notification.invoice.var.invoice_number": "References to invoice number.",
"sms_notification.invoice.var.reference_number": "References to invoice reference number.",
"sms_notification.invoice.var.customer_name": "References to invoice customer name.",
"sms_notification.invoice.var.due_amount": "References to invoice due amount.",
"sms_notification.invoice.var.amount": "References to invoice amount.",
"sms_notification.invoice.var.company_name": "References to company name.",
"sms_notification.invoice.var.due_date": "References to invoice due date.",
"sms_notification.receipt.var.receipt_number": "References to receipt number.",
"sms_notification.receipt.var.reference_number": "References to receipt reference number.",
"sms_notification.receipt.var.customer_name": "References to receipt customer name.",
"sms_notification.receipt.var.amount": "References to receipt amount.",
"sms_notification.receipt.var.company_name": "References to company name.",
"sms_notification.payment.var.payment_number": "References to payment transaction number.",
"sms_notification.payment.var.reference_number": "References to payment reference number",
"sms_notification.payment.var.customer_name": "References to payment customer name.",
"sms_notification.payment.var.amount": "References to payment transaction amount.",
"sms_notification.payment.company_name": "References to company name",
"sms_notification.payment.var.invoice_number": "Reference to payment invoice number.",
"sms_notification.estimate.var.estimate_number": "References to estimate number.",
"sms_notification.estimate.var.reference_number": "References to estimate reference number.",
"sms_notification.estimate.var.customer_name": "References to estimate customer name.",
"sms_notification.estimate.var.amount": "References to estimate amount.",
"sms_notification.estimate.var.company_name": "References to company name.",
"sms_notification.estimate.var.expiration_date": "References to estimate expirtaion date.",
"sms_notification.estimate.var.estimate_date": "References to estimate date.",
"sms_notification.customer.var.customer_name": "References to customer name.",
"sms_notification.customer.var.balance": "References to customer outstanding balance.",
"sms_notification.customer.var.company_name": "References to company name.",
"ability.accounts": "Chart of accounts",
"ability.manual_journal": "Manual journals",
"ability.cashflow": "Cash flow",
"ability.inventory_adjustment": "Inventory adjustments",
"ability.customers": "Customers",
"ability.vendors": "vendors",
"ability.sale_estimates": "Sale estimates",
"ability.sale_invoices": "Sale invoices",
"ability.sale_receipts": "Sale receipts",
"ability.expenses": "Expenses",
"ability.payments_receive": "Payments receive",
"ability.purchase_invoices": "Purchase invoices",
"ability.all_reports": "All reports",
"ability.payments_made": "Payments made",
"ability.preferences": "Preferences",
"ability.mutate_system_preferences": "Mutate the system preferences.",
"ability.items": "Items",
"ability.view": "View",
"ability.create": "Create",
"ability.edit": "Edit",
"ability.delete": "Delete",
"ability.transactions_locking": "Ability to transactions locking.",
"ability.balance_sheet_report": "Balance sheet.",
"ability.profit_loss_sheet": "Profit/loss sheet",
"ability.journal": "Journal",
"ability.general_ledger": "General ledger",
"ability.cashflow_report": "Cashflow",
"ability.AR_aging_summary_report": "A/R aging summary",
"ability.AP_aging_summary_report": "A/P aging summary",
"ability.purchases_by_items": "Purchases by items",
"ability.sales_by_items_report": "Sales by items",
"ability.customers_transactions_report": "Customers transactions",
"ability.vendors_transactions_report": "Vendors transactions",
"ability.customers_summary_balance_report": "Customers summary balance",
"ability.vendors_summary_balance_report": "Vendors summary balance",
"ability.inventory_valuation_summary": "Inventory valuation summary",
"ability.inventory_items_details": "Inventory items details",
"vendor_credit.field.vendor": "Vendor name",
"vendor_credit.field.amount": "Amount",
"vendor_credit.field.currency_code": "Currency code",
"vendor_credit.field.credit_date": "Credit date",
"vendor_credit.field.credit_number": "Credit number",
"vendor_credit.field.note": "Note",
"vendor_credit.field.created_at": "Created at",
"vendor_credit.field.reference_no": "Reference No.",
"credit_note.field.terms_conditions": "Terms and conditions",
"credit_note.field.note": "Note",
"credit_note.field.currency_code": "Currency code",
"credit_note.field.created_at": "Created at",
"credit_note.field.amount": "Amount",
"credit_note.field.credit_note_number": "Credit note number",
"credit_note.field.credit_note_date": "Credit date",
"credit_note.field.customer": "Customer",
"credit_note.field.reference_no": "Reference No.",
"Credit note": "Credit note",
"Vendor credit": "Vendor credit",
"Refund credit note": "Refund credit note",
"Refund vendor credit": "Refund vendor credit",
"credit_note.field.status": "Status",
"credit_note.field.status.draft": "Draft",
"credit_note.field.status.published": "Published",
"credit_note.field.status.open": "Open",
"credit_note.field.status.closed": "Closed",
"transactions_locking.module.sales.label": "Sales",
"transactions_locking.module.purchases.label": "Purchases",
"transactions_locking.module.financial.label": "Financial",
"transactions_locking.module.all_transactions": "All transactions",
"transactions_locking.module.sales.desc": "Sale invoices, Receipts, credit notes, customers payment receive and customers opening balances.",
"transactions_locking.module.purchases.desc": "Purchase invoices, vendors payments, vendor credit notes and vendors opening balances.",
"transactions_locking.module.financial.desc": "Manual journal, expenses and inventory adjustments.",
"inventory_adjustment.type.increment": "Increment",
"inventory_adjustment.type.decrement": "Decrement",
"customer.type.individual": "Individual",
"customer.type.business": "Business",
"credit_note.view.draft": "Draft",
"credit_note.view.closed": "Closed",
"credit_note.view.open": "Open",
"credit_note.view.published": "Published",
"vendor_credit.view.draft": "Draft",
"vendor_credit.view.closed": "Closed",
"vendor_credit.view.open": "Open",
"vendor_credit.view.published": "Published",
"allocation_method.value.label": "Value",
"allocation_method.quantity.label": "Quantity",
"balance_sheet.assets": "Assets",
"balance_sheet.current_asset": "Current Asset",
"balance_sheet.cash_and_cash_equivalents": "Cash and cash equivalents",
"balance_sheet.accounts_receivable": "Accounts Receivable",
"balance_sheet.inventory": "Inventory",
"balance_sheet.other_current_assets": "Other current assets",
"balance_sheet.fixed_asset": "Fixed Asset",
"balance_sheet.non_current_assets": "Non-Current Assets",
"balance_sheet.liabilities_and_equity": "Liabilities and Equity",
"balance_sheet.liabilities": "Liabilities",
"balance_sheet.current_liabilties": "Current Liabilties",
"balance_sheet.long_term_liabilities": "Long-Term Liabilities",
"balance_sheet.non_current_liabilities": "Non-Current Liabilities",
"balance_sheet.equity": "Equity",
"balance_sheet.account_name": "Account name",
"balance_sheet.total": "Total",
"balance_sheet.percentage_of_column": "% of Column",
"balance_sheet.percentage_of_row": "% of Row",
"financial_sheet.previoud_period_date": "{{date}} (PP)",
"fianncial_sheet.previous_period_change": "Change (PP)",
"financial_sheet.previous_period_percentage": "% Change (PP)",
"financial_sheet.previous_year_date": "{{date}} (PY)",
"financial_sheet.previous_year_change": "Change (PY)",
"financial_sheet.previous_year_percentage": "% Change (PY)",
"financial_sheet.total_row": "Total {{value}}",
"profit_loss_sheet.income": "Income",
"profit_loss_sheet.cost_of_sales": "Cost of sales",
"profit_loss_sheet.gross_profit": "GROSS PROFIT",
"profit_loss_sheet.expenses": "Expenses",
"profit_loss_sheet.net_operating_income": "NET OPERATING INCOME",
"profit_loss_sheet.other_income": "Other income",
"profit_loss_sheet.other_expenses": "Other expenses",
"profit_loss_sheet.net_income": "NET INCOME",
"profit_loss_sheet.account_name": "Account name",
"profit_loss_sheet.total": "Total",
"profit_loss_sheet.percentage_of_income": "% of Income",
"profit_loss_sheet.percentage_of_expenses": "% of Expenses",
"profit_loss_sheet.percentage_of_column": "% of Column",
"profit_loss_sheet.percentage_of_row": "% of Row",
"contact_summary_balance.account_name": "Account name",
"contact_summary_balance.total": "Total",
"contact_summary_balance.percentage_column": "% of Column",
"warehouses.primary_warehouse": "Primary warehouse",
"branches.head_branch": "Head Branch",
"account.accounts_payable.currency": "Accounts Payable (A/P) - {{currency}}",
"account.accounts_receivable.currency": "Accounts Receivable (A/R) - {{currency}}",
"role.admin.name": "Admin",
"role.admin.desc": "Unrestricted access to all modules.",
"role.staff.name": "Staff",
"role.staff.desc": "Access to all modules except reports, settings and accountant.",
"warehouse_transfer.view.draft.name": "Draft",
"warehouse_transfer.view.in_transit.name": "In Transit",
"warehouse_transfer.view.transferred.name": "Transferred"
}

View File

@@ -0,0 +1,35 @@
@import "./normalize.scss";
*,
*::before,
*::after {
box-sizing: border-box;
}
th {
text-align: inherit; // 2
text-align: -webkit-match-parent; // 3
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
body{
margin: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
direction: ltr;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
@import "../base.scss";
@import "../fonts.scss";
body {
background: #f8f9fa;
text-align: left;
-webkit-print-color-adjust: exact;
html[lang^='ar'] & {
font-family: "Segoe UI";
}
html[lang^='en'] & {
font-family: "Noto Sans";
}
@media print {
background: #fff;
}
}

View File

@@ -0,0 +1,193 @@
@import "../layouts/paper-layout.scss";
.credit {
text-align: left;
padding: 45px 40px;
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 0 0 30px;
.organization {
.title {
margin: 0 0 4px;
}
.creditNumber {
font-size: 12px;
}
}
.paper {
.title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 2px;
font-size: 32px;
line-height: 1;
}
}
}
&__full-amount {
margin-bottom: 18px;
.label {
font-size: 12px;
}
.amount {
font-size: 18px;
font-weight: 800;
}
}
&__meta {
display: flex;
flex-direction: column;
margin-bottom: 20px;
font-size: 13px;
&-item {
padding-right: 10px;
font-weight: 400;
margin-bottom: 10px;
display: flex;
flex-direction: row;
.value {
color: #000;
}
.label {
color: #444;
margin-bottom: 2px;
width: 180px;
}
}
}
&__table {
display: flex;
flex-direction: column;
table {
font-size: 12px;
color: #000;
text-align: left;
border-spacing: 0;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead th {
font-weight: 400;
border-bottom: none;
padding: 8px;
color: #fff;
background-color: #333;
}
tbody tr td {
padding: 8px;
border-bottom: 1px solid #cecbcb;
}
thead tr th,
tbody tr td {
&.item {
width: 45%;
}
&.rate {
width: 18%;
text-align: right;
}
&.quantity {
width: 16%;
text-align: right;
}
&.total {
width: 21%;
text-align: right;
}
}
.description {
color: #666;
}
}
}
&__table-after {
display: flex;
}
&__table-total {
margin-bottom: 20px;
width: 50%;
float: right;
margin-left: auto;
table {
border-spacing: 0;
width: 100%;
font-size: 12px;
tbody tr td {
padding: 8px 10px 8px 0;
border-top: 1px solid #d5d5d5;
&:last-child {
width: 140px;
text-align: right;
}
}
tbody tr:first-child td {
border-top: 0;
}
tbody tr.payment-amount td:last-child {
color: red
}
tbody tr.blanace-due td {
border-top: 3px double #666;
font-weight: bold;
}
}
}
&__footer {
font-size: 12px;
}
&__conditions,
&__notes {
h3 {
color: #666;
font-size: 12px;
margin-top: 0;
margin-bottom: 10px;
}
p {
margin: 0;
}
}
&__conditions+&__notes {
margin-top: 20px;
}
}

View File

@@ -0,0 +1,174 @@
@import "../layouts/paper-layout.scss";
.estimate {
text-align: left;
padding: 45px;
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 0 0 30px;
.organization {
.title {
margin: 0 0 4px;
}
}
.paper {
.title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 2px;
font-size: 32px;
line-height: 1;
}
}
}
&__estimate-amount {
margin-bottom: 18px;
.label {
font-size: 12px;
}
.amount {
font-size: 18px;
font-weight: 800;
}
}
&__meta {
display: flex;
flex-direction: column;
margin-bottom: 20px;
font-size: 13px;
&-item {
padding-right: 10px;
margin-bottom: 10px;
display: flex;
flex-direction: row;
.value {
color: #000;
}
.label {
color: #444;
margin-bottom: 2px;
width: 180px;
}
}
}
&__table {
display: flex;
flex-direction: column;
table {
font-size: 12px;
color: #000;
text-align: left;
border-spacing: 0;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead th {
font-weight: 400;
border-bottom: none;
padding: 8px;
color: #fff;
background-color: #333;
}
tbody tr td {
padding: 8px;
border-bottom: 1px solid #cecbcb;
}
thead tr th,
tbody tr td {
&.item {
width: 45%;
}
&.rate {
width: 18%;
text-align: right;
}
&.quantity {
width: 16%;
text-align: right;
}
&.total {
width: 21%;
text-align: right;
}
.description {
color: #666;
}
}
}
}
&__table-after {
display: flex;
}
&__table-total {
margin-bottom: 20px;
width: 50%;
float: right;
margin-left: auto;
table {
border-spacing: 0;
width: 100%;
font-size: 12px;
tbody tr td {
padding: 8px 10px 8px 0;
border-top: 1px solid #d5d5d5;
&:last-child {
width: 140px;
text-align: right;
}
}
tbody tr:first-child td {
border-top: 0;
}
tbody tr.total td {
border-top: 3px double #666;
font-weight: bold;
}
}
}
&__footer{
font-size: 12px;
}
&__conditions,
&__notes {
h3 {
color: #666;
font-size: 12px;
margin-top: 0;
margin-bottom: 10px;
}
p {
margin: 0 0 20px;
}
}
}

View File

@@ -0,0 +1,179 @@
@import "../layouts/paper-layout.scss";
.invoice {
text-align: left;
padding: 45px 40px;
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 0 0 30px;
.organization {
.title {
margin: 0 0 4px;
}
.invoiceNo {
font-size: 12px;
}
}
.paper {
.title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 2px;
font-size: 32px;
line-height: 1;
}
}
}
&__meta {
display: flex;
flex-direction: column;
margin-bottom: 20px;
font-size: 13px;
&-item {
padding-right: 10px;
font-weight: 400;
margin-bottom: 10px;
display: flex;
flex-direction: row;
.value {
color: #000;
}
.label {
color: #444;
margin-bottom: 2px;
width: 180px;
}
}
}
&__table {
display: flex;
flex-direction: column;
table {
font-size: 12px;
color: #000;
text-align: left;
border-spacing: 0;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead th {
font-weight: 400;
border-bottom: none;
padding: 8px;
color: #fff;
background-color: #333;
}
tbody tr td {
padding: 8px;
border-bottom: 1px solid #cecbcb;
}
thead tr th,
tbody tr td {
&.item {
width: 45%;
}
&.rate {
width: 18%;
text-align: right;
}
&.quantity {
width: 16%;
text-align: right;
}
&.total {
width: 21%;
text-align: right;
}
}
.description {
color: #666;
}
}
}
&__table-after{
display: flex;
}
&__table-total {
margin-bottom: 20px;
width: 50%;
float: right;
margin-left: auto;
table {
border-spacing: 0;
width: 100%;
font-size: 12px;
tbody tr td {
padding: 8px 10px 8px 0;
border-top: 1px solid #d5d5d5;
&:last-child {
width: 140px;
text-align: right;
}
}
tbody tr:first-child td {
border-top: 0;
}
tbody tr.payment-amount td:last-child {
color: red
}
tbody tr.blanace-due td {
border-top: 3px double #666;
font-weight: bold;
}
}
}
&__due-amount {
margin-bottom: 18px;
.label {
font-size: 12px;
}
.amount {
font-size: 18px;
font-weight: 800;
}
}
&__footer{
font-size: 12px;
}
&__conditions,
&__notes {
h3 {
color: #666;
font-size: 12px;
margin-top: 0;
margin-bottom: 10px;
}
p{
margin: 0;
}
}
&__conditions + &__notes{
margin-top: 20px;
}
}

View File

@@ -0,0 +1,178 @@
@import "../layouts/paper-layout.scss";
.payment {
text-align: left;
padding: 45px 40px;
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 0 0 30px;
.organization {
.title {
margin: 0 0 4px;
}
.paymentNumber {
font-size: 12px;
}
}
.paper {
.title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 2px;
font-size: 32px;
line-height: 1;
}
}
}
&__meta {
display: flex;
flex-direction: column;
margin-bottom: 20px;
font-size: 13px;
&-item {
padding-right: 10px;
font-weight: 400;
margin-bottom: 10px;
display: flex;
flex-direction: row;
.value {
color: #000;
}
.label {
color: #444;
margin-bottom: 2px;
width: 180px;
}
}
}
&__table {
display: flex;
flex-direction: column;
table {
font-size: 12px;
color: #000;
text-align: left;
border-spacing: 0;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead th {
font-weight: 400;
border-bottom: none;
padding: 8px;
color: #fff;
background-color: #333;
}
tbody tr td {
padding: 8px;
border-bottom: 1px solid #cecbcb;
}
thead tr th,
tbody tr td {
&.item {
width: 34%;
}
&.date {
width: 22%;
text-align: right;
}
&.invoiceAmount {
width: 22%;
text-align: right;
}
&.paymentAmount {
width: 22%;
text-align: right;
}
}
.description {
color: #666;
}
}
}
&__table-after{
display: flex;
}
&__table-total {
margin-bottom: 20px;
width: 50%;
float: right;
margin-left: auto;
table {
border-spacing: 0;
width: 100%;
font-size: 12px;
tbody tr td {
padding: 8px 10px 8px 0;
border-top: 1px solid #d5d5d5;
&:last-child {
width: 140px;
text-align: right;
}
}
tbody tr:first-child td {
border-top: 0;
}
tbody tr.payment-amount td:last-child {
color: red
}
tbody tr.blanace-due td {
border-top: 3px double #666;
font-weight: bold;
}
}
}
&__received-amount {
margin-bottom: 18px;
.label {
font-size: 12px;
}
.amount {
font-size: 18px;
font-weight: 800;
}
}
&__footer{
font-size: 12px;
}
&__conditions,
&__notes {
h3 {
color: #666;
font-size: 12px;
margin-top: 0;
margin-bottom: 10px;
}
p{
margin: 0;
}
}
&__conditions + &__notes{
margin-top: 20px;
}
}

View File

@@ -0,0 +1,185 @@
@import "../layouts/paper-layout.scss";
.receipt {
text-align: left;
padding: 45px;
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin: 0 0 30px;
.organization {
.title {
margin: 0 0 4px;
}
.receiptNumber {
margin: 0 0 12px;
}
}
.paper {
.title {
font-weight: 400;
text-transform: uppercase;
margin: 0 0 2px;
font-size: 32px;
line-height: 1;
}
}
}
&__receipt-amount {
margin-bottom: 18px;
.label {
font-size: 12px;
}
.amount {
font-size: 18px;
font-weight: 800;
}
}
&__meta {
display: flex;
flex-direction: column;
margin-bottom: 20px;
font-size: 13px;
&-item {
padding-right: 10px;
margin-bottom: 10px;
display: flex;
flex-direction: row;
.value {
color: #000;
}
.label {
color: #444;
margin-bottom: 2px;
width: 180px;
}
}
}
&__table {
display: flex;
flex-direction: column;
table {
font-size: 12px;
color: #000;
text-align: left;
border-spacing: 0;
thead th,
tbody tr td {
margin-bottom: 15px;
background: transparent;
}
thead th {
font-weight: 400;
border-bottom: none;
padding: 8px;
color: #fff;
background-color: #333;
}
tbody tr td {
padding: 10px;
border-bottom: 1px solid #cecbcb;
}
thead tr th,
tbody tr td {
&.item {
width: 45%;
}
&.rate {
width: 18%;
text-align: right;
}
&.quantity {
width: 16%;
text-align: right;
}
&.total {
width: 21%;
text-align: right;
}
}
}
}
&__table-after {
display: flex;
}
&__table-total {
margin-bottom: 20px;
width: 50%;
float: right;
margin-left: auto;
table {
border-spacing: 0;
width: 100%;
font-size: 12px;
tbody tr td {
padding: 8px 10px 8px 0;
border-top: 1px solid #d5d5d5;
&:last-child {
width: 140px;
text-align: right;
}
}
tbody tr:first-child td {
border-top: 0;
}
tbody tr.payment-amount td:last-child {
color: red
}
tbody tr.blanace-due td {
border-top: 3px double #666;
font-weight: bold;
}
}
}
&__footer {
font-size: 12px;
}
&__conditions,
&__notes {
h3 {
color: #666;
font-size: 12px;
margin-top: 0;
margin-bottom: 10px;
}
p {
margin: 0 0 20px;
}
}
}

View File

@@ -0,0 +1,379 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box;
/* 1 */
height: 0;
/* 1 */
overflow: visible;
/* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none;
/* 1 */
text-decoration: underline;
/* 2 */
text-decoration: underline dotted;
/* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box;
/* 1 */
color: inherit;
/* 2 */
display: table;
/* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
/* 1 */
padding: 0;
/* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@@ -0,0 +1,7 @@
html(lang=locale)
head
title My Site - #{title}
block head
body
div.paper-template
block content

View File

@@ -0,0 +1,81 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/credit-rtl.css
else
include ../../css/modules/credit.css
block content
div.credit
div.credit__header
div.paper
h1.title #{__('credit.paper.credit_note')}
if creditNote.creditNoteNumber
span.creditNoteNumber #{creditNote.creditNoteNumber}
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.credit__full-amount
div.label #{__('credit.paper.amount')}
div.amount #{creditNote.formattedAmount}
div.credit__meta
div.credit__meta-item.credit__meta-item--amount
span.label #{__('credit.paper.remaining')}
span.value #{creditNote.formattedCreditsRemaining}
div.credit__meta-item.credit__meta-item--billed-to
span.label #{__("credit.paper.billed_to")}
span.value #{creditNote.customer.displayName}
div.credit__meta-item.credit__meta-item--credit-date
span.label #{__("credit.paper.credit_date")}
span.value #{creditNote.formattedCreditNoteDate}
div.credit__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in creditNote.entries
tr
td.item
div.title=entry.item.name
span.description=entry.description
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.credit__table-after
div.credit__table-total
table
tbody
tr.total
td #{__('credit.paper.total')}
td #{creditNote.formattedAmount}
tr.payment-amount
td #{__('credit.paper.credits_used')}
td #{creditNote.formattedCreditsUsed}
tr.blanace-due
td #{__('credit.paper.credits_remaining')}
td #{creditNote.formattedCreditsRemaining}
div.credit__footer
if creditNote.termsConditions
div.credit__conditions
h3 #{__("credit.paper.terms_conditions")}
p #{creditNote.termsConditions}
if creditNote.note
div.credit__notes
h3 #{__("credit.paper.notes")}
p #{creditNote.note}

View File

@@ -0,0 +1,82 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/estimate-rtl.css
else
include ../../css/modules/estimate.css
block content
div.estimate
div.estimate__header
div.paper
h1.title #{__("estimate.paper.estimate")}
span.email #{saleEstimate.estimateNumber}
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.estimate__estimate-amount
div.label #{__('estimate.paper.estimate_amount')}
div.amount #{saleEstimate.formattedAmount}
div.estimate__meta
if saleEstimate.estimateNumber
div.estimate__meta-item.estimate__meta-item--estimate-number
span.label #{__("estimate.paper.estimate_number")}
span.value #{saleEstimate.estimateNumber}
div.estimate__meta-item.estimate__meta-item--billed-to
span.label #{__("estimate.paper.billed_to")}
span.value #{saleEstimate.customer.displayName}
div.estimate__meta-item.estimate__meta-item--estimate-date
span.label #{__("estimate.paper.estimate_date")}
span.value #{saleEstimate.formattedEstimateDate}
div.estimate__meta-item.estimate__meta-item--due-date
span.label #{__("estimate.paper.expiration_date")}
span.value #{saleEstimate.formattedExpirationDate}
div.estimate__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in saleEstimate.entries
tr
td.item
div.title=entry.item.name
span.description=entry.description
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.estimate__table-after
div.estimate__table-total
table
tbody
tr.subtotal
td #{__('estimate.paper.subtotal')}
td #{saleEstimate.formattedAmount}
tr.total
td #{__('estimate.paper.total')}
td #{saleEstimate.formattedAmount}
div.estimate__footer
if saleEstimate.termsConditions
div.estimate__conditions
h3 #{__("estimate.paper.conditions_title")}
p #{saleEstimate.termsConditions}
if saleEstimate.note
div.estimate__notes
h3 #{__("estimate.paper.notes_title")}
p #{saleEstimate.note}

View File

@@ -0,0 +1,85 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/invoice-rtl.css
else
include ../../css/modules/invoice.css
block content
div.invoice
div.invoice__header
div.paper
h1.title #{__("invoice.paper.invoice")}
if saleInvoice.invoiceNo
span.invoiceNo #{saleInvoice.invoiceNo}
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.invoice__due-amount
div.label #{__('invoice.paper.invoice_amount')}
div.amount #{saleInvoice.formattedAmount}
div.invoice__meta
div.invoice__meta-item.invoice__meta-item--amount
span.label #{__('invoice.paper.due_amount')}
span.value #{saleInvoice.formattedDueAmount}
div.invoice__meta-item.invoice__meta-item--billed-to
span.label #{__("invoice.paper.billed_to")}
span.value #{saleInvoice.customer.displayName}
div.invoice__meta-item.invoice__meta-item--invoice-date
span.label #{__("invoice.paper.invoice_date")}
span.value #{saleInvoice.formattedInvoiceDate}
div.invoice__meta-item.invoice__meta-item--due-date
span.label #{__("invoice.paper.due_date")}
span.value #{saleInvoice.formattedDueDate}
div.invoice__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in saleInvoice.entries
tr
td.item
div.title=entry.item.name
span.description=entry.description
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.invoice__table-after
div.invoice__table-total
table
tbody
tr.total
td #{__('invoice.paper.total')}
td #{saleInvoice.formattedAmount}
tr.payment-amount
td #{__('invoice.paper.payment_amount')}
td #{saleInvoice.formattedPaymentAmount}
tr.blanace-due
td #{__('invoice.paper.balance_due')}
td #{saleInvoice.formattedDueAmount}
div.invoice__footer
if saleInvoice.termsConditions
div.invoice__conditions
h3 #{__("invoice.paper.conditions_title")}
p #{saleInvoice.termsConditions}
if saleInvoice.invoiceMessage
div.invoice__notes
h3 #{__("invoice.paper.notes_title")}
p #{saleInvoice.invoiceMessage}

View File

@@ -0,0 +1,67 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/payment-rtl.css
else
include ../../css/modules/payment.css
block content
div.payment
div.payment__header
div.paper
h1.title #{__("payment.paper.payment_receipt")}
if paymentReceive.paymentReceiveNo
span.paymentNumber #{paymentReceive.paymentReceiveNo}
div.organization
h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
div.payment__received-amount
div.label #{__('payment.paper.amount_received')}
div.amount #{paymentReceive.formattedAmount}
div.payment__meta
div.payment__meta-item.payment__meta-item--billed-to
span.label #{__("payment.paper.billed_to")}
span.value #{paymentReceive.customer.displayName}
div.payment__meta-item.payment__meta-item--payment-date
span.label #{__("payment.paper.payment_date")}
span.value #{paymentReceive.formattedPaymentDate}
div.payment__table
table
thead
tr
th.item #{__("payment.paper.invoice_number")}
th.date #{__("payment.paper.invoice_date")}
th.invoiceAmount #{__("payment.paper.invoice_amount")}
th.paymentAmount #{__("payment.paper.payment_amount")}
tbody
each entry in paymentReceive.entries
tr
td.item=entry.invoice.invoiceNo
td.date=entry.invoice.formattedInvoiceDate
td.invoiceAmount=entry.invoice.formattedAmount
td.paymentAmount=entry.invoice.formattedPaymentAmount
div.payment__table-after
div.payment__table-total
table
tbody
tr.payment-amount
td #{__('payment.paper.payment_amount')}
td #{paymentReceive.formattedAmount}
tr.blanace-due
td #{__('payment.paper.balance_due')}
td #{paymentReceive.customer.closingBalance}
div.payment__footer
if paymentReceive.statement
div.payment__notes
h3 #{__("payment.paper.statement")}
p #{paymentReceive.statement}

View File

@@ -0,0 +1,77 @@
extends ../PaperTemplateLayout.pug
block head
style
if (isRtl)
include ../../css/modules/receipt-rtl.css
else
include ../../css/modules/receipt.css
block content
div.receipt
div.receipt__header
div.paper
h1.title #{__("receipt.paper.receipt")}
span.receiptNumber #{saleReceipt.receiptNumber}
div.organization
h3.title #{organizationName}
div.receipt__receipt-amount
div.label #{__('receipt.paper.receipt_amount')}
div.amount #{saleReceipt.formattedAmount}
div.receipt__meta
div.receipt__meta-item.receipt__meta-item--billed-to
span.label #{__("receipt.paper.billed_to")}
span.value #{saleReceipt.customer.displayName}
div.receipt__meta-item.receipt__meta-item--invoice-date
span.label #{__("receipt.paper.receipt_date")}
span.value #{saleReceipt.formattedReceiptDate}
if saleReceipt.receiptNumber
div.receipt__meta-item.receipt__meta-item--invoice-number
span.label #{__("receipt.paper.receipt_number")}
span.value #{saleReceipt.receiptNumber}
div.receipt__table
table
thead
tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in saleReceipt.entries
tr
td.item=entry.item.name
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.receipt__table-after
div.receipt__table-total
table
tbody
tr.total
td #{__('receipt.paper.total')}
td #{saleReceipt.formattedAmount}
tr.payment-amount
td #{__('receipt.paper.payment_amount')}
td #{saleReceipt.formattedAmount}
tr.blanace-due
td #{__('receipt.paper.balance_due')}
td #{'$0'}
div.receipt__footer
if saleReceipt.statement
div.receipt__conditions
h3 #{__("receipt.paper.statement")}
p #{saleReceipt.statement}
if saleReceipt.receiptMessage
div.receipt__notes
h3 #{__("receipt.paper.notes")}
p #{saleReceipt.receiptMessage}

View File

@@ -0,0 +1,145 @@
/**
* # Gulp Configuration.
* ------------------------------------------------------------------
*/
const RESOURCES_PATH = '../resources/';
module.exports = {
banner: [
'/**',
' * <%= pkg.name %> - <%= pkg.description %>',
' * @version v<%= pkg.version %>',
' * @link <%= pkg.homepage %>',
' * @author <%= pkg.author %>',
' * @license <%= pkg.license %>',
'**/',
'',
].join('\n'),
// Browser Sync
browsersync: {
files: ['**/*', '!**.map', '!**.css'], // Exclude map files.
notify: false, //
open: true, // Set it to false if you don't like the broser window opening automatically.
port: 8080, //
proxy: 'localhost/customatic', //
watchOptions: {
debounceDelay: 2000, // This introduces a small delay when watching for file change events to avoid triggering too many reloads
},
snippetOptions: {
whitelist: ['/wp-admin/admin-ajax.php'],
blacklist: ['/wp-admin/**'],
},
},
// Style Related.
style: {
clean: ['style.css', 'style.min.css', 'style-rtl.css', 'style-rtl.min.css'],
build: [
{
src: `${RESOURCES_PATH}/scss/modules/invoice.scss`,
dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
{
src: `${RESOURCES_PATH}/scss/modules/estimate.scss`,
dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
{
src: `${RESOURCES_PATH}/scss/modules/receipt.scss`,
dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
{
src: `${RESOURCES_PATH}/scss/modules/credit.scss`,
dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
{
src: `${RESOURCES_PATH}/scss/modules/payment.scss`,
dest: `${RESOURCES_PATH}/css/modules`,
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source.
},
// {
// src: './assets/sass/editor-style.scss',
// dest: './assets/css',
// sourcemaps: true,
// minify: true,
// },
],
// RTL builds.
rtl: [
{
src: `${RESOURCES_PATH}/css/modules/invoice.css`,
dest: `${RESOURCES_PATH}/css/modules`,
},
{
src: `${RESOURCES_PATH}/css/modules/estimate.css`,
dest: `${RESOURCES_PATH}/css/modules`,
},
{
src: `${RESOURCES_PATH}/css/modules/receipt.css`,
dest: `${RESOURCES_PATH}/css/modules`,
},
{
src: `${RESOURCES_PATH}/css/modules/credit.css`,
dest: `${RESOURCES_PATH}/css/modules`,
},
{
src: `${RESOURCES_PATH}/css/modules/payment.css`,
dest: `${RESOURCES_PATH}/css/modules`,
},
],
// Browsers you care about for auto-prefixing.
autoprefixer: {
browsers: [
'Android 2.3',
'Android >= 4',
'Chrome >= 20',
'Firefox >= 24',
'Explorer >= 9',
'iOS >= 6',
'Opera >= 12',
'Safari >= 6',
],
},
// SASS Configuration for all builds.
sass: {
errLogToConsole: true,
// outputStyle: 'compact',
},
// CSS MQ Packer configuration for all builds and style tasks.
cssMqpacker: {},
// CSS nano configuration for all builds.
cssnano: {},
// rtlcss configuration for all builds.
rtlcss: {},
},
// Clean specific files.
clean: [
'**/.DS_Store',
'./assets/js/**/*.min.js',
'**/*.map',
'**/*.min.css',
'assets/js/hypernews.js',
],
// Watch related.
watch: {
css: ['./assets/sass/**/*'],
js: ['assets/js/**/*.js', '!assets/js/**/*.min.js'],
images: ['./assets/images/**/*'],
},
};

View File

@@ -0,0 +1,50 @@
const gulp = require('gulp');
const sass = require('sass');
const gulpSass = require('gulp-sass')(sass); // Gulp pluign for Sass compilation.
const mergeStream = require('merge-stream');
const rename = require('gulp-rename'); // Renames files E.g. style.css -> style.min.css
// Style related.
const postcss = require('gulp-postcss'); // Transforming styles with JS plugins
const rtlcss = require('rtlcss'); // Convert LTR CSS to RTL.
const config = require('./gulpConfig');
gulp.task('styles', () => {
const builds = config.style.build.map((build) => {
return gulp
.src(build.src)
.pipe(gulpSass(config.style.sass))
.pipe(gulp.dest(build.dest));
});
return mergeStream(builds);
});
/**
* Task: `styles-rtl`
*
* This task does the following.
* 1. Gets the source css files.
* 2. Covert LTR CSS to RTL.
* 3. Suffix all CSS files to `-rtl`.
* 4. Reloads css files via browser sync stream.
* 5. Combine matching media queries for `.min.css` version.
* 6. Minify all CSS files.
* 7. Reload minified css files via browser sync stream.
*/
gulp.task('styles-rtl', () => {
const builds = config.style.rtl.map((build) => {
return gulp
.src(build.src)
.pipe(
postcss([
rtlcss(config.style.rtlcss), // Convert LTR CSS to RTL.
]),
)
.pipe(rename({ suffix: '-rtl' })) // Append "-rtl" to the filename.
.pipe(gulp.dest(build.dest));
});
return mergeStream(builds);
});

View File

@@ -0,0 +1,4 @@
npm install
npm run build
npm run copy-i18n

View File

@@ -0,0 +1,31 @@
MYSQL_USER="ratteb"
MYSQL_DATABASE="ratteb"
MYSQL_CONTAINER_NAME="ratteb_test"
MYSQL_ROOT_PASSWORD="root"
MYSQL_PASSWORD="root"
echo "Start the testing MySql database..."
docker \
run \
--detach \
--env MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \
--env MYSQL_USER=${MYSQL_USER} \
--env MYSQL_PASSWORD=${MYSQL_PASSWORD} \
--env MYSQL_DATABASE=${MYSQL_DATABASE} \
--name ${MYSQL_CONTAINER_NAME} \
--publish 3306:3306 \
--tmpfs /var/lib/mysql:rw \
mysql:5.7;
echo "Sleeping for 10 seconds to allow time for the DB to be provisioned:"
for i in `seq 1 10`;
do
echo "."
sleep 1
done
echo "Database '${MYSQL_DATABASE}' running."
echo " Username: ${MYSQL_USER}"
echo " Password: ${MYSQL_PASSWORD}"

View File

@@ -0,0 +1,74 @@
const path = require('path');
const { NormalModuleReplacementPlugin } = require('webpack');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const isDev = process.env.NODE_ENV === 'development';
const outputDir = '../build';
const outputFilename = 'index.js';
const inputEntry = './src/server.ts';
const webpackOptions = {
entry: ['regenerator-runtime/runtime', inputEntry],
target: 'node',
mode: isDev ? 'development' : 'production',
watch: isDev,
watchOptions: {
aggregateTimeout: 200,
poll: 1000,
},
output: {
path: path.resolve(__dirname, outputDir),
filename: outputFilename,
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
extensionAlias: {
'.ts': ['.js', '.ts'],
'.cts': ['.cjs', '.cts'],
'.mts': ['.mjs', '.mts'],
},
plugins: [
new TsconfigPathsPlugin({
configFile: './tsconfig.json',
extensions: ['.ts', '.tsx', '.js'],
}),
],
},
plugins: [
// Ignore knex dynamic required dialects that we don't use
new NormalModuleReplacementPlugin(
/m[sy]sql2?|oracle(db)?|sqlite3|pg-(native|query)/,
'noop2'
),
new ProgressBarPlugin(),
],
externals: [nodeExternals(), 'aws-sdk', 'prettier'],
module: {
rules: [
{
test: /\.([cm]?ts|tsx|js)$/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
configFile: 'tsconfig.json',
},
},
],
exclude: /(node_modules)/,
},
],
},
};
if (isDev) {
webpackOptions.plugins.push(
new RunScriptWebpackPlugin({ name: outputFilename })
);
}
module.exports = webpackOptions;

View File

@@ -0,0 +1,52 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import BaseController from '@/api/controllers/BaseController';
import AuthenticatedAccount from '@/services/AuthenticatedAccount';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import JWTAuth from '@/api/middleware/jwtAuth';
@Service()
export default class AccountController extends BaseController {
@Inject()
accountService: AuthenticatedAccount;
/**
* Router constructor method.
*/
public router() {
const router = Router();
// Should before build tenant database the user be authorized and
// most important than that, should be subscribed to any plan.
router.use(JWTAuth);
router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware);
router.get('/', this.getAccount);
return router;
}
/**
* Creates a new account.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private getAccount = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId, user } = req;
try {
const account = await this.accountService.getAccount(tenantId, user);
return res.status(200).send({ data: account });
} catch (error) {
next(error);
}
};
}

View File

@@ -0,0 +1,42 @@
import { Service, Inject } from 'typedi';
import { Request, Response, Router, NextFunction } from 'express';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import AccountsTypesService from '@/services/Accounts/AccountsTypesServices';
@Service()
export default class AccountsTypesController extends BaseController {
@Inject()
accountsTypesService: AccountsTypesService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get('/', asyncMiddleware(this.getAccountTypesList.bind(this)));
return router;
}
/**
* Retrieve accounts types list.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @return {Response}
*/
getAccountTypesList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const accountTypes = this.accountsTypesService.getAccountsTypes(tenantId);
return res.status(200).send({
account_types: this.transfromToResponse(accountTypes, ['label'], req),
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,500 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
@Service()
export default class AccountsController extends BaseController {
@Inject()
private accountsApplication: AccountsApplication;
@Inject()
private dynamicListService: DynamicListingService;
/**
* Router constructor method.
*/
router() {
const router = Router();
router.get(
'/transactions',
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
[query('account_id').optional().isInt().toInt()],
this.asyncMiddleware(this.accountTransactions.bind(this)),
this.catchServiceErrors
);
router.post(
'/:id/activate',
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
[...this.accountParamSchema],
asyncMiddleware(this.activateAccount.bind(this)),
this.catchServiceErrors
);
router.post(
'/:id/inactivate',
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
[...this.accountParamSchema],
asyncMiddleware(this.inactivateAccount.bind(this)),
this.catchServiceErrors
);
router.post(
'/:id',
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
[...this.editAccountDTOSchema, ...this.accountParamSchema],
this.validationResult,
asyncMiddleware(this.editAccount.bind(this)),
this.catchServiceErrors
);
router.post(
'/',
CheckPolicies(AccountAction.CREATE, AbilitySubject.Account),
[...this.createAccountDTOSchema],
this.validationResult,
asyncMiddleware(this.newAccount.bind(this)),
this.catchServiceErrors
);
router.get(
'/:id',
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
[...this.accountParamSchema],
this.validationResult,
asyncMiddleware(this.getAccount.bind(this)),
this.catchServiceErrors
);
router.get(
'/',
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
[...this.accountsListSchema],
this.validationResult,
asyncMiddleware(this.getAccountsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors
);
router.delete(
'/:id',
CheckPolicies(AccountAction.DELETE, AbilitySubject.Account),
[...this.accountParamSchema],
this.validationResult,
asyncMiddleware(this.deleteAccount.bind(this)),
this.catchServiceErrors
);
return router;
}
/**
* Create account DTO Schema validation.
*/
get createAccountDTOSchema() {
return [
check('name')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim()
.escape(),
check('code')
.optional({ nullable: true })
.isLength({ min: 3, max: 6 })
.trim()
.escape(),
check('currency_code').optional(),
check('account_type')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim()
.escape(),
check('description')
.optional({ nullable: true })
.isLength({ max: DATATYPES_LENGTH.TEXT })
.trim()
.escape(),
check('parent_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt(),
];
}
/**
* Account DTO Schema validation.
*/
get editAccountDTOSchema() {
return [
check('name')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim()
.escape(),
check('code')
.optional({ nullable: true })
.isLength({ min: 3, max: 6 })
.trim()
.escape(),
check('account_type')
.exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim()
.escape(),
check('description')
.optional({ nullable: true })
.isLength({ max: DATATYPES_LENGTH.TEXT })
.trim()
.escape(),
check('parent_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt(),
];
}
get accountParamSchema() {
return [param('id').exists().isNumeric().toInt()];
}
/**
* Accounts list validation schema.
*/
get accountsListSchema() {
return [
query('view_slug').optional({ nullable: true }).isString().trim(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
get closingAccountSchema() {
return [
check('to_account_id').exists().isNumeric().toInt(),
check('delete_after_closing').exists().isBoolean(),
];
}
/**
* Creates a new account.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async newAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const accountDTO: IAccountDTO = this.matchedBodyData(req);
try {
const account = await this.accountsApplication.createAccount(
tenantId,
accountDTO
);
return res.status(200).send({
id: account.id,
message: 'The account has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Edit account details.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async editAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: accountId } = req.params;
const accountDTO: IAccountDTO = this.matchedBodyData(req);
try {
const account = await this.accountsApplication.editAccount(
tenantId,
accountId,
accountDTO
);
return res.status(200).send({
id: account.id,
message: 'The account has been edited successfully',
});
} catch (error) {
next(error);
}
}
/**
* Get details of the given account.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async getAccount(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: accountId } = req.params;
try {
const account = await this.accountsApplication.getAccount(
tenantId,
accountId
);
return res
.status(200)
.send({ account: this.transfromToResponse(account) });
} catch (error) {
next(error);
}
}
/**
* Delete the given account.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async deleteAccount(req: Request, res: Response, next: NextFunction) {
const { id: accountId } = req.params;
const { tenantId } = req;
try {
await this.accountsApplication.deleteAccount(tenantId, accountId);
return res.status(200).send({
id: accountId,
message: 'The deleted account has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Activate the given account.
* @param {Response} res -
* @param {Request} req -
* @return {Response}
*/
async activateAccount(req: Request, res: Response, next: Function) {
const { id: accountId } = req.params;
const { tenantId } = req;
try {
await this.accountsApplication.activateAccount(tenantId, accountId);
return res.status(200).send({
id: accountId,
message: 'The account has been activated successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Inactive the given account.
* @param {Response} res -
* @param {Request} req -
* @return {Response}
*/
async inactivateAccount(req: Request, res: Response, next: Function) {
const { id: accountId } = req.params;
const { tenantId } = req;
try {
await this.accountsApplication.inactivateAccount(tenantId, accountId);
return res.status(200).send({
id: accountId,
message: 'The account has been inactivated successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieve accounts datatable list.
* @param {Request} req
* @param {Response} res
* @param {Response}
*/
public async getAccountsList(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
// Filter query.
const filter = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...this.matchedQueryData(req),
};
try {
const { accounts, filterMeta } =
await this.accountsApplication.getAccounts(tenantId, filter);
return res.status(200).send({
accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
}
}
/**
* Retrieve accounts transactions list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async accountTransactions(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const transactionsFilter = this.matchedQueryData(req);
try {
const transactions =
await this.accountsApplication.getAccountsTransactions(
tenantId,
transactionsFilter
);
return res.status(200).send({
transactions: this.transfromToResponse(transactions),
});
} catch (error) {
next(error);
}
}
/**
* Transforms service errors to response.
* @param {Error}
* @param {Request} req
* @param {Response} res
* @param {ServiceError} error
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'account_not_found') {
return res.boom.notFound('The given account not found.', {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'account_name_not_unqiue') {
return res.boom.badRequest('The given account not unique.', {
errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }],
});
}
if (error.errorType === 'account_type_not_found') {
return res.boom.badRequest('The given account type not found.', {
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }],
});
}
if (error.errorType === 'account_type_not_allowed_to_changed') {
return res.boom.badRequest(
'Not allowed to change account type of the account.',
{
errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }],
}
);
}
if (error.errorType === 'parent_account_not_found') {
return res.boom.badRequest('The parent account not found.', {
errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }],
});
}
if (error.errorType === 'parent_has_different_type') {
return res.boom.badRequest('The parent account has different type.', {
errors: [
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 },
],
});
}
if (error.errorType === 'account_code_not_unique') {
return res.boom.badRequest('The given account code is not unique.', {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }],
});
}
if (error.errorType === 'account_has_associated_transactions') {
return res.boom.badRequest(
'You could not delete account has associated transactions.',
{
errors: [
{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 },
],
}
);
}
if (error.errorType === 'account_predefined') {
return res.boom.badRequest('You could not delete predefined account', {
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }],
});
}
if (error.errorType === 'accounts_not_found') {
return res.boom.notFound('Some of the given accounts not found.', {
errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }],
});
}
if (error.errorType === 'predefined_accounts') {
return res.boom.badRequest(
'Some of the given accounts are predefined.',
{ errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] }
);
}
if (error.errorType === 'close_account_and_to_account_not_same_type') {
return res.boom.badRequest(
'The close account has different root type with to account.',
{
errors: [
{
type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE',
code: 1200,
},
],
}
);
}
if (error.errorType === 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY') {
return res.boom.badRequest(
'The given account type does not support multi-currency.',
{
errors: [
{ type: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY', code: 1300 },
],
}
);
}
if (error.errorType === 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT') {
return res.boom.badRequest(
'You could not add account has currency different on the parent account.',
{
errors: [
{ type: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT', code: 1400 },
],
}
);
}
}
next(error);
}
}

View File

@@ -0,0 +1,24 @@
import { Router } from 'express';
import basicAuth from 'express-basic-auth';
import agendash from 'agendash';
import { Container } from 'typedi';
import config from '@/config';
export default class AgendashController {
static router() {
const router = Router();
const agendaInstance = Container.get('agenda');
router.use(
'/dash',
basicAuth({
users: {
[config.agendash.user]: config.agendash.password,
},
challenge: true,
}),
agendash(agendaInstance)
);
return router;
}
}

View File

@@ -0,0 +1,314 @@
import { Request, Response, Router } from 'express';
import { check, ValidationChain } from 'express-validator';
import { Service, Inject } from 'typedi';
import countries from 'country-codes-list';
import parsePhoneNumber from 'libphonenumber-js';
import BaseController from '@/api/controllers/BaseController';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import AuthenticationService from '@/services/Authentication';
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
import { ServiceError, ServiceErrors } from '@/exceptions';
import { DATATYPES_LENGTH } from '@/data/DataTypes';
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
import config from '@/config';
@Service()
export default class AuthenticationController extends BaseController {
@Inject()
authService: AuthenticationService;
/**
* Constructor method.
*/
router() {
const router = Router();
router.post(
'/login',
this.loginSchema,
this.validationResult,
LoginThrottlerMiddleware,
asyncMiddleware(this.login.bind(this)),
this.handlerErrors
);
router.post(
'/register',
this.registerSchema,
this.validationResult,
asyncMiddleware(this.register.bind(this)),
this.handlerErrors
);
router.post(
'/send_reset_password',
this.sendResetPasswordSchema,
this.validationResult,
asyncMiddleware(this.sendResetPassword.bind(this)),
this.handlerErrors
);
router.post(
'/reset/:token',
this.resetPasswordSchema,
this.validationResult,
asyncMiddleware(this.resetPassword.bind(this)),
this.handlerErrors
);
return router;
}
/**
* Login schema.
*/
get loginSchema(): ValidationChain[] {
return [
check('crediential').exists().isEmail(),
check('password').exists().isLength({ min: 5 }),
];
}
/**
* Register schema.
*/
get registerSchema(): ValidationChain[] {
return [
check('first_name')
.exists()
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('last_name')
.exists()
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('email')
.exists()
.isString()
.isEmail()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('phone_number')
.exists()
.isString()
.trim()
.escape()
.custom(this.phoneNumberValidator)
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('password')
.exists()
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('country')
.exists()
.isString()
.trim()
.escape()
.custom(this.countryValidator)
.isLength({ max: DATATYPES_LENGTH.STRING }),
];
}
/**
* Country validator.
*/
countryValidator(value, { req }) {
const {
countries: { whitelist, blacklist },
} = config.registration;
const foundCountry = countries.findOne('countryCode', value);
if (!foundCountry) {
throw new Error('The country code is invalid.');
}
if (
// Focus with me! In case whitelist is not empty and the given coutry is not
// in whitelist throw the error.
//
// Or in case the blacklist is not empty and the given country exists
// in the blacklist throw the goddamn error.
(whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
(blacklist.length > 0 && blacklist.indexOf(value) !== -1)
) {
throw new Error('The country code is not supported yet.');
}
return true;
}
/**
* Phone number validator.
*/
phoneNumberValidator(value, { req }) {
const phoneNumber = parsePhoneNumber(value, req.body.country);
if (!phoneNumber || !phoneNumber.isValid()) {
throw new Error('Phone number is invalid with the given country code.');
}
return true;
}
/**
* Reset password schema.
*/
get resetPasswordSchema(): ValidationChain[] {
return [
check('password')
.exists()
.isLength({ min: 5 })
.custom((value, { req }) => {
if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match");
} else {
return value;
}
}),
];
}
/**
* Send reset password validation schema.
*/
get sendResetPasswordSchema(): ValidationChain[] {
return [check('email').exists().isEmail().trim().escape()];
}
/**
* Handle user login.
* @param {Request} req
* @param {Response} res
*/
async login(req: Request, res: Response, next: Function): Response {
const userDTO: ILoginDTO = this.matchedBodyData(req);
try {
const { token, user, tenant } = await this.authService.signIn(
userDTO.crediential,
userDTO.password
);
return res.status(200).send({ token, user, tenant });
} catch (error) {
next(error);
}
}
/**
* Organization register handler.
* @param {Request} req
* @param {Response} res
*/
async register(req: Request, res: Response, next: Function) {
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
try {
const registeredUser: ISystemUser = await this.authService.register(
registerDTO
);
return res.status(200).send({
type: 'success',
code: 'REGISTER.SUCCESS',
message: 'Register organization has been success.',
});
} catch (error) {
next(error);
}
}
/**
* Send reset password handler
* @param {Request} req
* @param {Response} res
*/
async sendResetPassword(req: Request, res: Response, next: Function) {
const { email } = this.matchedBodyData(req);
try {
await this.authService.sendResetPassword(email);
return res.status(200).send({
code: 'SEND_RESET_PASSWORD_SUCCESS',
message: 'The reset password message has been sent successfully.',
});
} catch (error) {
if (error instanceof ServiceError) {
}
next(error);
}
}
/**
* Reset password handler
* @param {Request} req
* @param {Response} res
*/
async resetPassword(req: Request, res: Response, next: Function) {
const { token } = req.params;
const { password } = req.body;
try {
await this.authService.resetPassword(token, password);
return res.status(200).send({
type: 'RESET_PASSWORD_SUCCESS',
message: 'The password has been reset successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Handles the service errors.
*/
handlerErrors(error, req: Request, res: Response, next: Function) {
if (error instanceof ServiceError) {
if (
['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
) {
return res.boom.badRequest(null, {
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
});
}
if (error.errorType === 'USER_INACTIVE') {
return res.boom.badRequest(null, {
errors: [{ type: 'USER_INACTIVE', code: 200 }],
});
}
if (
error.errorType === 'TOKEN_INVALID' ||
error.errorType === 'TOKEN_EXPIRED'
) {
return res.boom.badRequest(null, {
errors: [{ type: 'TOKEN_INVALID', code: 300 }],
});
}
if (error.errorType === 'USER_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'USER_NOT_FOUND', code: 400 }],
});
}
if (error.errorType === 'EMAIL_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }],
});
}
}
if (error instanceof ServiceErrors) {
const errorReasons = [];
if (error.hasType('PHONE_NUMBER_EXISTS')) {
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
}
if (error.hasType('EMAIL_EXISTS')) {
errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
}
next(error);
}
}

View File

@@ -0,0 +1,140 @@
import { Response, Request, NextFunction } from 'express';
import { matchedData, validationResult } from 'express-validator';
import accepts from 'accepts';
import { isArray, drop, first, camelCase, snakeCase, omit, set, get } from 'lodash';
import { mapKeysDeep } from 'utils';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
export default class BaseController {
/**
* Converts plain object keys to cameCase style.
* @param {Object} data
*/
protected dataToCamelCase(data) {
return mapKeysDeep(data, (v, k) => camelCase(k));
}
/**
* Matches the body data from validation schema.
* @param {Request} req
* @param options
*/
protected matchedBodyData(req: Request, options: any = {}) {
const data = matchedData(req, {
locations: ['body'],
includeOptionals: true,
...omit(options, ['locations']), // override any propery except locations.
});
return this.dataToCamelCase(data);
}
/**
* Matches the query data from validation schema.
* @param {Request} req
*/
protected matchedQueryData(req: Request) {
const data = matchedData(req, {
locations: ['query'],
});
return this.dataToCamelCase(data);
}
/**
* Validate validation schema middleware.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
protected validationResult(req: Request, res: Response, next: NextFunction) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error',
...validationErrors,
});
}
next();
}
/**
* Sets localization to response object by the given path.
* @param {Response} response -
* @param {string} path -
* @param {Request} req -
*/
private setLocalizationByPath(
response: any,
path: string,
req: Request,
) {
const DOT = '.';
if (isArray(response)) {
response.forEach((va) => {
const currentPath = first(path.split(DOT));
const value = get(va, currentPath);
if (isArray(value)) {
const nextPath = drop(path.split(DOT)).join(DOT);
this.setLocalizationByPath(value, nextPath, req);
} else {
set(va, path, req.__(value));
}
})
} else {
const value = get(response, path);
set(response, path, req.__(value));
}
}
/**
* Transform the given data to response.
* @param {any} data
*/
protected transfromToResponse(
data: any,
translatable?: string | string[],
req?: Request
) {
const response = mapKeysDeep(data, (v, k) => snakeCase(k));
if (translatable) {
const translatables = Array.isArray(translatable)
? translatable
: [translatable];
translatables.forEach((path) => {
this.setLocalizationByPath(response, path, req);
});
}
return response;
}
/**
* Async middleware.
* @param {function} callback
*/
protected asyncMiddleware(callback) {
return asyncMiddleware(callback);
}
/**
*
* @param {Request} req
* @returns
*/
protected accepts(req) {
return accepts(req);
}
/**
*
* @param {Request} req
* @param {string[]} types
* @returns {string}
*/
protected acceptTypes(req: Request, types: string[]) {
return this.accepts(req).types(types);
}
}

View File

@@ -0,0 +1,335 @@
import { Service, Inject } from 'typedi';
import { Request, Response, Router, NextFunction } from 'express';
import { check, param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { Features, ICreateBranchDTO, IEditBranchDTO } from '@/interfaces';
import { BranchesApplication } from '@/services/Branches/BranchesApplication';
import { ServiceError } from '@/exceptions';
import { FeatureActivationGuard } from '@/api/middleware/FeatureActivationGuard';
@Service()
export class BranchesController extends BaseController {
@Inject()
branchesApplication: BranchesApplication;
/**
* Branches routes.
* @returns {Router}
*/
router() {
const router = Router();
router.post(
'/activate',
[],
this.validationResult,
this.asyncMiddleware(this.activateBranches),
this.handlerServiceErrors
);
router.post(
'/',
FeatureActivationGuard(Features.BRANCHES),
[
check('name').exists(),
check('code').optional({ nullable: true }),
check('address').optional({ nullable: true }),
check('city').optional({ nullable: true }),
check('country').optional({ nullable: true }),
check('phone_number').optional({ nullable: true }),
check('email').optional({ nullable: true }).isEmail(),
check('website').optional({ nullable: true }).isURL(),
],
this.validationResult,
this.asyncMiddleware(this.createBranch),
this.handlerServiceErrors
);
router.post(
'/:id',
FeatureActivationGuard(Features.BRANCHES),
[
param('id').exists().isInt().toInt(),
check('name').exists(),
check('code').optional({ nullable: true }),
check('address').optional({ nullable: true }),
check('city').optional({ nullable: true }),
check('country').optional({ nullable: true }),
check('phone_number').optional({ nullable: true }),
check('email').optional({ nullable: true }).isEmail(),
check('website').optional({ nullable: true }).isURL(),
],
this.validationResult,
this.asyncMiddleware(this.editBranch),
this.handlerServiceErrors
);
router.post(
'/:id/mark-primary',
FeatureActivationGuard(Features.BRANCHES),
[],
this.validationResult,
this.asyncMiddleware(this.markBranchAsPrimary),
this.handlerServiceErrors
);
router.delete(
'/:id',
FeatureActivationGuard(Features.BRANCHES),
[param('id').exists().isInt().toInt()],
this.validationResult,
this.asyncMiddleware(this.deleteBranch),
this.handlerServiceErrors
);
router.get(
'/:id',
FeatureActivationGuard(Features.BRANCHES),
[param('id').exists().isInt().toInt()],
this.validationResult,
this.asyncMiddleware(this.getBranch),
this.handlerServiceErrors
);
router.get(
'/',
FeatureActivationGuard(Features.BRANCHES),
[],
this.validationResult,
this.asyncMiddleware(this.getBranches),
this.handlerServiceErrors
);
return router;
}
/**
* Creates a new branch.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public createBranch = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const createBranchDTO: ICreateBranchDTO = this.matchedBodyData(req);
try {
const branch = await this.branchesApplication.createBranch(
tenantId,
createBranchDTO
);
return res.status(200).send({
id: branch.id,
message: 'The branch has been created successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Edits the given branch.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public editBranch = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: branchId } = req.params;
const editBranchDTO: IEditBranchDTO = this.matchedBodyData(req);
try {
const branch = await this.branchesApplication.editBranch(
tenantId,
branchId,
editBranchDTO
);
return res.status(200).send({
id: branch.id,
message: 'The branch has been edited successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Deletes the given branch.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public deleteBranch = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: branchId } = req.params;
try {
await this.branchesApplication.deleteBranch(tenantId, branchId);
return res.status(200).send({
id: branchId,
message: 'The branch has been deleted successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Retrieves specific branch.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public getBranch = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: branchId } = req.params;
try {
const branch = await this.branchesApplication.getBranch(
tenantId,
branchId
);
return res.status(200).send({ branch });
} catch (error) {
next(error);
}
};
/**
* Retrieves branches list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public getBranches = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
try {
const branches = await this.branchesApplication.getBranches(tenantId);
return res.status(200).send({ branches });
} catch (error) {
next(error);
}
};
/**
* Activates the multi-branches feature.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public activateBranches = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
try {
await this.branchesApplication.activateBranches(tenantId);
return res.status(200).send({
message: 'Multi-branches feature has been activated successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Marks the given branch as primary.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
public markBranchAsPrimary = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: branchId } = req.params;
try {
await this.branchesApplication.markBranchAsPrimary(tenantId, branchId);
return res.status(200).send({
id: branchId,
message: 'The branch has been marked as primary.',
});
} catch (error) {
next(error);
}
};
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private handlerServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'BRANCH_NOT_FOUND') {
return res.status(400).send({
errors: [{ type: 'BRANCH_NOT_FOUND', code: 100 }],
});
}
if (error.errorType === 'MUTLI_BRANCHES_ALREADY_ACTIVATED') {
return res.status(400).send({
errors: [{ type: 'MUTLI_BRANCHES_ALREADY_ACTIVATED', code: 100 }],
});
}
if (error.errorType === 'COULD_NOT_DELETE_ONLY_BRANCH') {
return res.status(400).send({
errors: [{ type: 'COULD_NOT_DELETE_ONLY_BRANCH', code: 300 }],
});
}
if (error.errorType === 'BRANCH_CODE_NOT_UNIQUE') {
return res.status(400).send({
errors: [{ type: 'BRANCH_CODE_NOT_UNIQUE', code: 400 }],
});
}
if (error.errorType === 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS') {
return res.status(400).send({
errors: [
{ type: 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS', code: 500 },
],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,23 @@
import { Service, Inject, Container } from 'typedi';
import { Router } from 'express';
import CommandCashflowTransaction from './NewCashflowTransaction';
import DeleteCashflowTransaction from './DeleteCashflowTransaction';
import GetCashflowTransaction from './GetCashflowTransaction';
import GetCashflowAccounts from './GetCashflowAccounts';
@Service()
export default class CashflowController {
/**
* Constructor method.
*/
router() {
const router = Router();
router.use(Container.get(GetCashflowTransaction).router());
router.use(Container.get(GetCashflowAccounts).router());
router.use(Container.get(CommandCashflowTransaction).router());
router.use(Container.get(DeleteCashflowTransaction).router());
return router;
}
}

View File

@@ -0,0 +1,98 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator';
import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions';
import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
@Service()
export default class DeleteCashflowTransaction extends BaseController {
@Inject()
deleteCashflowService: DeleteCashflowTransactionService;
/**
* Controller router.
*/
public router() {
const router = Router();
router.delete(
'/transactions/:transactionId',
CheckPolicies(CashflowAction.Delete, AbilitySubject.Cashflow),
[param('transactionId').exists().isInt().toInt()],
this.asyncMiddleware(this.deleteCashflowTransaction),
this.catchServiceErrors
);
return router;
}
/**
* Retrieve the cashflow account transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private deleteCashflowTransaction = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { transactionId } = req.params;
try {
const { oldCashflowTransaction } =
await this.deleteCashflowService.deleteCashflowTransaction(
tenantId,
transactionId
);
return res.status(200).send({
id: oldCashflowTransaction.id,
message: 'The cashflow transaction has been deleted successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Catches the service errors.
* @param error
* @param req
* @param res
* @param next
* @returns
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
return res.boom.badRequest(
'The given cashflow transaction not found.',
{
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 100 }],
}
);
}
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
return res.boom.badRequest(null, {
errors: [
{
type: 'TRANSACTIONS_DATE_LOCKED',
code: 4000,
data: { ...error.payload },
},
],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,95 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param, query } from 'express-validator';
import GetCashflowAccountsService from '@/services/Cashflow/GetCashflowAccountsService';
import BaseController from '../BaseController';
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
@Service()
export default class GetCashflowAccounts extends BaseController {
@Inject()
getCashflowAccountsService: GetCashflowAccountsService;
@Inject()
getCashflowTransactionsService: GetCashflowTransactionsService;
/**
* Controller router.
*/
public router() {
const router = Router();
router.get(
'/accounts',
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
[
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(),
],
this.asyncMiddleware(this.getCashflowAccounts),
this.catchServiceErrors
);
return router;
}
/**
* Retrieve the cashflow accounts.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private getCashflowAccounts = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
// Filter query.
const filter = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...this.matchedQueryData(req),
};
try {
const cashflowAccounts =
await this.getCashflowAccountsService.getCashflowAccounts(
tenantId,
filter
);
return res.status(200).send({
cashflow_accounts: this.transfromToResponse(cashflowAccounts),
});
} catch (error) {
next(error);
}
};
/**
* Catches the service errors.
* @param {Error} error - Error.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next -
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
}
next(error);
}
}

View File

@@ -0,0 +1,98 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator';
import BaseController from '../BaseController';
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
@Service()
export default class GetCashflowAccounts extends BaseController {
@Inject()
getCashflowTransactionsService: GetCashflowTransactionsService;
/**
* Controller router.
*/
public router() {
const router = Router();
router.get(
'/transactions/:transactionId',
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
[param('transactionId').exists().isInt().toInt()],
this.asyncMiddleware(this.getCashflowTransaction),
this.catchServiceErrors
);
return router;
}
/**
* Retrieve the cashflow account transactions.
* @param {Request} req - Request object.
* @param {Response} res - Response object.
* @param {NextFunction} next
*/
private getCashflowTransaction = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { transactionId } = req.params;
try {
const cashflowTransaction =
await this.getCashflowTransactionsService.getCashflowTransaction(
tenantId,
transactionId
);
return res.status(200).send({
cashflow_transaction: this.transfromToResponse(cashflowTransaction),
});
} catch (error) {
next(error);
}
};
/**
* Catches the service errors.
* @param {Error} error - Error.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next -
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
return res.boom.badRequest(
'The given cashflow tranasction not found.',
{
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 200 }],
}
);
}
if (error.errorType === 'ACCOUNT_ID_HAS_INVALID_TYPE') {
return res.boom.badRequest(
'The given cashflow account has invalid type.',
{
errors: [{ type: 'ACCOUNT_ID_HAS_INVALID_TYPE', code: 300 }],
}
);
}
if (error.errorType === 'ACCOUNT_NOT_FOUND') {
return res.boom.badRequest('The given account not found.', {
errors: [{ type: 'ACCOUNT_NOT_FOUND', code: 400 }],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,146 @@
import { Service, Inject } from 'typedi';
import { check } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions';
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces';
@Service()
export default class NewCashflowTransactionController extends BaseController {
@Inject()
private newCashflowTranscationService: NewCashflowTransactionService;
/**
* Router constructor.
*/
public router() {
const router = Router();
router.post(
'/transactions',
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
this.newTransactionValidationSchema,
this.validationResult,
this.asyncMiddleware(this.newCashflowTransaction),
this.catchServiceErrors
);
return router;
}
/**
* New cashflow transaction validation schema.
*/
get newTransactionValidationSchema() {
return [
check('date').exists().isISO8601().toDate(),
check('reference_no').optional({ nullable: true }).trim().escape(),
check('description')
.optional({ nullable: true })
.isLength({ min: 3 })
.trim()
.escape(),
check('transaction_type').exists(),
check('amount').exists().isFloat().toFloat(),
check('cashflow_account_id').exists().isInt().toInt(),
check('credit_account_id').exists().isInt().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('publish').default(false).isBoolean().toBoolean(),
];
}
/**
* Creates a new cashflow transaction.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private newCashflowTransaction = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId, userId } = req;
const ownerContributionDTO = this.matchedBodyData(req);
try {
const { cashflowTransaction } =
await this.newCashflowTranscationService.newCashflowTransaction(
tenantId,
ownerContributionDTO,
userId
);
return res.status(200).send({
id: cashflowTransaction.id,
message: 'New cashflow transaction has been created successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Handle the service errors.
* @param error
* @param req
* @param res
* @param next
* @returns
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND') {
return res.boom.badRequest('Cashflow accounts ids not found.', {
errors: [{ type: 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND', code: 100 }],
});
}
if (error.errorType === 'CREDIT_ACCOUNTS_IDS_NOT_FOUND') {
return res.boom.badRequest('Credit accounts ids not found.', {
errors: [{ type: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND', code: 200 }],
});
}
if (error.errorType === 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE') {
return res.boom.badRequest('Cashflow .', {
errors: [{ type: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE', code: 300 }],
});
}
if (error.errorType === 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE') {
return res.boom.badRequest(
'Cashflow accounts should be cash or bank type.',
{
errors: [{ type: 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE', code: 300 }],
}
);
}
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
return res.boom.badRequest('Cashflow transaction not found.', {
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 500 }],
});
}
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
return res.boom.badRequest(null, {
errors: [
{
type: 'TRANSACTIONS_DATE_LOCKED',
code: 4000,
data: { ...error.payload },
},
],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,404 @@
import { check, param, query, body, ValidationChain } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import BaseController from '@/api/controllers/BaseController';
import ContactsService from '@/services/Contacts/ContactsService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes';
@Service()
export default class ContactsController extends BaseController {
@Inject()
contactsService: ContactsService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Express router.
*/
router() {
const router = Router();
router.get(
'/auto-complete',
[...this.autocompleteQuerySchema],
this.validationResult,
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
this.dynamicListService.handlerErrorsToResponse
);
router.get(
'/:id',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.getContact.bind(this))
);
router.post(
'/:id/inactivate',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.inactivateContact.bind(this)),
this.handlerServiceErrors
);
router.post(
'/:id/activate',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.activateContact.bind(this)),
this.handlerServiceErrors
);
return router;
}
/**
* Auto-complete list query validation schema.
*/
get autocompleteQuerySchema() {
return [
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
query('limit').optional().isNumeric().toInt(),
];
}
/**
* Retrieve details of the given contact.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async getContact(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: contactId } = req.params;
try {
const contact = await this.contactsService.getContact(
tenantId,
contactId
);
return res.status(200).send({
customer: this.transfromToResponse(contact),
});
} catch (error) {
next(error);
}
}
/**
* Retrieve auto-complete contacts list.
* @param {Request} req - Request object.
* @param {Response} res - Response object.
* @param {NextFunction} next
*/
async autocompleteContacts(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'display_name',
limit: 10,
...this.matchedQueryData(req),
};
try {
const contacts = await this.contactsService.autocompleteContacts(
tenantId,
filter
);
return res.status(200).send({ contacts });
} catch (error) {
next(error);
}
}
/**
* @returns {ValidationChain[]}
*/
get contactDTOSchema(): ValidationChain[] {
return [
check('salutation')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('first_name')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('last_name')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('company_name')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('display_name')
.exists()
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('email')
.optional({ nullable: true })
.isString()
.normalizeEmail()
.isEmail()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('website')
.optional({ nullable: true })
.isString()
.trim()
.isURL()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('work_phone')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('personal_phone')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_1')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_2')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_city')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_country')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_email')
.optional({ nullable: true })
.isString()
.isEmail()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_postcode')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_phone')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_state')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_1')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_2')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_city')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_country')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_email')
.optional({ nullable: true })
.isString()
.isEmail()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_postcode')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_phone')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_state')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('note')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }),
check('active').optional().isBoolean().toBoolean(),
];
}
/**
* Contact new DTO schema.
* @returns {ValidationChain[]}
*/
get contactNewDTOSchema(): ValidationChain[] {
return [
check('opening_balance')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
.toInt(),
check('opening_balance_exchange_rate')
.default(1)
.isFloat({ gt: 0 })
.toFloat(),
body('opening_balance_at')
.if(body('opening_balance').exists())
.exists()
.isISO8601(),
check('opening_balance_branch_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
];
}
/**
* Contact edit DTO schema.
* @returns {ValidationChain[]}
*/
get contactEditDTOSchema(): ValidationChain[] {
return [];
}
/**
* @returns {ValidationChain[]}
*/
get specificContactSchema(): ValidationChain[] {
return [param('id').exists().isNumeric().toInt()];
}
/**
* Activates the given contact.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async activateContact(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: contactId } = req.params;
try {
await this.contactsService.activateContact(tenantId, contactId);
return res.status(200).send({
id: contactId,
message: 'The given contact activated successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Inactivate the given contact.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async inactivateContact(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: contactId } = req.params;
try {
await this.contactsService.inactivateContact(tenantId, contactId);
return res.status(200).send({
id: contactId,
message: 'The given contact inactivated successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private handlerServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CONTACT.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'CONTACT_ALREADY_ACTIVE') {
return res.boom.badRequest(null, {
errors: [{ type: 'CONTACT_ALREADY_ACTIVE', code: 700 }],
});
}
if (error.errorType === 'CONTACT_ALREADY_INACTIVE') {
return res.boom.badRequest(null, {
errors: [{ type: 'CONTACT_ALREADY_INACTIVE', code: 800 }],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,351 @@
import { Request, Response, Router, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { check, query } from 'express-validator';
import ContactsController from '@/api/controllers/Contacts/Contacts';
import CustomersService from '@/services/Contacts/CustomersService';
import { ServiceError } from '@/exceptions';
import {
ICustomerNewDTO,
ICustomerEditDTO,
AbilitySubject,
CustomerAction,
} from '@/interfaces';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { CustomersApplication } from '@/services/Contacts/Customers/CustomersApplication';
@Service()
export default class CustomersController extends ContactsController {
@Inject()
private customersApplication: CustomersApplication;
@Inject()
private dynamicListService: DynamicListingService;
/**
* Express router.
*/
router() {
const router = Router();
router.post(
'/',
CheckPolicies(CustomerAction.Create, AbilitySubject.Customer),
[
...this.contactDTOSchema,
...this.contactNewDTOSchema,
...this.customerDTOSchema,
...this.createCustomerDTOSchema,
],
this.validationResult,
asyncMiddleware(this.newCustomer.bind(this)),
this.handlerServiceErrors
);
router.post(
'/:id/opening_balance',
CheckPolicies(CustomerAction.Edit, AbilitySubject.Customer),
[
...this.specificContactSchema,
check('opening_balance').exists().isNumeric().toFloat(),
check('opening_balance_at').optional().isISO8601(),
check('opening_balance_exchange_rate')
.default(1)
.isFloat({ gt: 0 })
.toFloat(),
check('opening_balance_branch_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
],
this.validationResult,
asyncMiddleware(this.editOpeningBalanceCustomer.bind(this)),
this.handlerServiceErrors
);
router.post(
'/:id',
CheckPolicies(CustomerAction.Edit, AbilitySubject.Customer),
[
...this.contactDTOSchema,
...this.contactEditDTOSchema,
...this.customerDTOSchema,
],
this.validationResult,
asyncMiddleware(this.editCustomer.bind(this)),
this.handlerServiceErrors
);
router.delete(
'/:id',
CheckPolicies(CustomerAction.Delete, AbilitySubject.Customer),
[...this.specificContactSchema],
this.validationResult,
asyncMiddleware(this.deleteCustomer.bind(this)),
this.handlerServiceErrors
);
router.get(
'/',
CheckPolicies(CustomerAction.View, AbilitySubject.Customer),
[...this.validateListQuerySchema],
this.validationResult,
asyncMiddleware(this.getCustomersList.bind(this)),
this.dynamicListService.handlerErrorsToResponse
);
router.get(
'/:id',
CheckPolicies(CustomerAction.View, AbilitySubject.Customer),
[...this.specificContactSchema],
this.validationResult,
asyncMiddleware(this.getCustomer.bind(this)),
this.handlerServiceErrors
);
return router;
}
/**
* Customer DTO schema.
*/
get customerDTOSchema() {
return [
check('customer_type')
.exists()
.isIn(['business', 'individual'])
.trim()
.escape(),
];
}
/**
* Create customer DTO schema.
*/
get createCustomerDTOSchema() {
return [
check('currency_code')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: 3 }),
];
}
/**
* List param query schema.
*/
get validateListQuerySchema() {
return [
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('view_slug').optional().isString().trim(),
query('stringified_filter_roles').optional().isJSON(),
query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
/**
* Creates a new customer.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async newCustomer(req: Request, res: Response, next: NextFunction) {
const contactDTO: ICustomerNewDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
try {
const contact = await this.customersApplication.createCustomer(
tenantId,
contactDTO,
user
);
return res.status(200).send({
id: contact.id,
message: 'The customer has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Edits the given customer details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editCustomer(req: Request, res: Response, next: NextFunction) {
const contactDTO: ICustomerEditDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
const { id: contactId } = req.params;
try {
await this.customersApplication.editCustomer(
tenantId,
contactId,
contactDTO,
user
);
return res.status(200).send({
id: contactId,
message: 'The customer has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Changes the opening balance of the given customer.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async editOpeningBalanceCustomer(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req;
const { id: customerId } = req.params;
const openingBalanceEditDTO = this.matchedBodyData(req);
try {
await this.customersApplication.editOpeningBalance(
tenantId,
customerId,
openingBalanceEditDTO
);
return res.status(200).send({
id: customerId,
message:
'The opening balance of the given customer has been changed successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieve details of the given customer id.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getCustomer(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: contactId } = req.params;
try {
const customer = await this.customersApplication.getCustomer(
tenantId,
contactId,
user
);
return res.status(200).send({
customer: this.transfromToResponse(customer),
});
} catch (error) {
next(error);
}
}
/**
* Deletes the given customer from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteCustomer(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: contactId } = req.params;
try {
await this.customersApplication.deleteCustomer(tenantId, contactId, user);
return res.status(200).send({
id: contactId,
message: 'The customer has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieve customers paginated and filterable list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getCustomersList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
inactiveMode: false,
sortOrder: 'desc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
try {
const { customers, pagination, filterMeta } =
await this.customersApplication.getCustomers(tenantId, filter);
return res.status(200).send({
customers: this.transfromToResponse(customers),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
}
}
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private handlerServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'contacts_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
return res.boom.badRequest(null, {
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
});
}
if (error.errorType === 'CUSTOMER_HAS_TRANSACTIONS') {
return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOMER_HAS_TRANSACTIONS', code: 600 }],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,332 @@
import { Request, Response, Router, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body, query, ValidationChain, check } from 'express-validator';
import ContactsController from '@/api/controllers/Contacts/Contacts';
import { ServiceError } from '@/exceptions';
import {
IVendorNewDTO,
IVendorEditDTO,
IVendorsFilter,
AbilitySubject,
VendorAction,
} from '@/interfaces';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { VendorsApplication } from '@/services/Contacts/Vendors/VendorsApplication';
@Service()
export default class VendorsController extends ContactsController {
@Inject()
private vendorsApplication: VendorsApplication;
/**
* Express router.
*/
router() {
const router = Router();
router.post(
'/',
CheckPolicies(VendorAction.Create, AbilitySubject.Vendor),
[
...this.contactDTOSchema,
...this.contactNewDTOSchema,
...this.vendorDTOSchema,
],
this.validationResult,
asyncMiddleware(this.newVendor.bind(this)),
this.handlerServiceErrors
);
router.post(
'/:id/opening_balance',
CheckPolicies(VendorAction.Edit, AbilitySubject.Vendor),
[
...this.specificContactSchema,
check('opening_balance').exists().isNumeric().toFloat(),
check('opening_balance_at').optional().isISO8601(),
check('opening_balance_exchange_rate')
.default(1)
.isFloat({ gt: 0 })
.toFloat(),
check('opening_balance_branch_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
],
this.validationResult,
asyncMiddleware(this.editOpeningBalanceVendor.bind(this)),
this.handlerServiceErrors
);
router.post(
'/:id',
CheckPolicies(VendorAction.Edit, AbilitySubject.Vendor),
[
...this.contactDTOSchema,
...this.contactEditDTOSchema,
...this.vendorDTOSchema,
],
this.validationResult,
asyncMiddleware(this.editVendor.bind(this)),
this.handlerServiceErrors
);
router.delete(
'/:id',
CheckPolicies(VendorAction.Delete, AbilitySubject.Vendor),
[...this.specificContactSchema],
this.validationResult,
asyncMiddleware(this.deleteVendor.bind(this)),
this.handlerServiceErrors
);
router.get(
'/:id',
CheckPolicies(VendorAction.View, AbilitySubject.Vendor),
[...this.specificContactSchema],
this.validationResult,
asyncMiddleware(this.getVendor.bind(this)),
this.handlerServiceErrors
);
router.get(
'/',
CheckPolicies(VendorAction.View, AbilitySubject.Vendor),
[...this.vendorsListSchema],
this.validationResult,
asyncMiddleware(this.getVendorsList.bind(this))
);
return router;
}
/**
* Vendor DTO schema.
* @returns {ValidationChain[]}
*/
get vendorDTOSchema(): ValidationChain[] {
return [
check('currency_code')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ min: 3, max: 3 }),
];
}
/**
* Vendors datatable list validation schema.
* @returns {ValidationChain[]}
*/
get vendorsListSchema() {
return [
query('view_slug').optional().isString().trim(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
/**
* Creates a new vendor.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async newVendor(req: Request, res: Response, next: NextFunction) {
const contactDTO: IVendorNewDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
try {
const vendor = await this.vendorsApplication.createVendor(
tenantId,
contactDTO,
user
);
return res.status(200).send({
id: vendor.id,
message: 'The vendor has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Edits the given vendor details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editVendor(req: Request, res: Response, next: NextFunction) {
const contactDTO: IVendorEditDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
const { id: contactId } = req.params;
try {
await this.vendorsApplication.editVendor(
tenantId,
contactId,
contactDTO,
user
);
return res.status(200).send({
id: contactId,
message: 'The vendor has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Changes the opening balance of the given vendor.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async editOpeningBalanceVendor(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req;
const { id: vendorId } = req.params;
const editOpeningBalanceDTO = this.matchedBodyData(req);
try {
await this.vendorsApplication.editOpeningBalance(
tenantId,
vendorId,
editOpeningBalanceDTO
);
return res.status(200).send({
id: vendorId,
message:
'The opening balance of the given vendor has been changed successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Deletes the given vendor from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteVendor(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: contactId } = req.params;
try {
await this.vendorsApplication.deleteVendor(tenantId, contactId, user);
return res.status(200).send({
id: contactId,
message: 'The vendor has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retrieve details of the given vendor id.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getVendor(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: vendorId } = req.params;
try {
const vendor = await this.vendorsApplication.getVendor(
tenantId,
vendorId,
user
);
return res.status(200).send(this.transfromToResponse({ vendor }));
} catch (error) {
next(error);
}
}
/**
* Retrieve vendors datatable list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getVendorsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const vendorsFilter: IVendorsFilter = {
inactiveMode: false,
sortOrder: 'desc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
try {
const { vendors, pagination, filterMeta } =
await this.vendorsApplication.getVendors(tenantId, vendorsFilter);
return res.status(200).send({
vendors: this.transfromToResponse(vendors),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
}
}
/**
* Handle service errors.
* @param {Error} error -
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private handlerServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'contacts_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'VENDORS.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
return res.boom.badRequest(null, {
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
});
}
if (error.errorType === 'VENDOR_HAS_TRANSACTIONS') {
return res.boom.badRequest(null, {
errors: [{ type: 'VENDOR_HAS_TRANSACTIONS', code: 600 }],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,211 @@
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import CurrenciesService from '@/services/Currencies/CurrenciesService';
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
@Service()
export default class CurrenciesController extends BaseController {
@Inject()
currenciesService: CurrenciesService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
[...this.listSchema],
this.validationResult,
asyncMiddleware(this.all.bind(this))
);
router.post(
'/',
[...this.currencyDTOSchemaValidation],
this.validationResult,
asyncMiddleware(this.newCurrency.bind(this)),
this.handlerServiceError
);
router.post(
'/:id',
[...this.currencyIdParamSchema, ...this.currencyEditDTOSchemaValidation],
this.validationResult,
asyncMiddleware(this.editCurrency.bind(this)),
this.handlerServiceError
);
router.delete(
'/:currency_code',
[...this.currencyParamSchema],
this.validationResult,
asyncMiddleware(this.deleteCurrency.bind(this)),
this.handlerServiceError
);
return router;
}
get currencyDTOSchemaValidation(): ValidationChain[] {
return [
check('currency_name').exists().trim(),
check('currency_code').exists().trim(),
check('currency_sign').exists().trim(),
];
}
get currencyEditDTOSchemaValidation(): ValidationChain[] {
return [
check('currency_name').exists().trim(),
check('currency_sign').exists().trim(),
];
}
get currencyIdParamSchema(): ValidationChain[] {
return [param('id').exists().isNumeric().toInt()];
}
get currencyParamSchema(): ValidationChain[] {
return [param('currency_code').exists().trim().escape()];
}
get listSchema(): ValidationChain[] {
return [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
];
}
/**
* Retrieve all registered currency details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async all(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const currencies = await this.currenciesService.listCurrencies(tenantId);
return res.status(200).send({
currencies: this.transfromToResponse(currencies),
});
} catch (error) {
next(error);
}
}
/**
* Creates a new currency on the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async newCurrency(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const currencyDTO = this.matchedBodyData(req);
try {
await this.currenciesService.newCurrency(tenantId, currencyDTO);
return res.status(200).send({
currency_code: currencyDTO.currencyCode,
message: 'The currency has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Edits details of the given currency.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteCurrency(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { currency_code: currencyCode } = req.params;
try {
await this.currenciesService.deleteCurrency(tenantId, currencyCode);
return res.status(200).send({
currency_code: currencyCode,
message: 'The currency has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Deletes the currency.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editCurrency(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { id: currencyId } = req.params;
const editCurrencyDTO = this.matchedBodyData(req);
try {
const currency = await this.currenciesService.editCurrency(
tenantId,
currencyId,
editCurrencyDTO
);
return res.status(200).send({
currency_code: currency.currencyCode,
message: 'The currency has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Handles currencies service error.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handlerServiceError(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'currency_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CURRENCY_NOT_FOUND', code: 100 }],
});
}
if (error.errorType === 'currency_code_exists') {
return res.boom.badRequest(null, {
errors: [{
type: 'CURRENCY_CODE_EXISTS',
message: 'The given currency code is already exists.',
code: 200,
}],
});
}
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
return res.boom.badRequest(null, {
errors: [
{
type: 'CANNOT_DELETE_BASE_CURRENCY',
code: 300,
message: 'Cannot delete the base currency.',
},
],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,47 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import DashboardService from '@/services/Dashboard/DashboardService';
@Service()
export default class DashboardMetaController {
@Inject()
dashboardService: DashboardService;
/**
*
* @returns
*/
router() {
const router = Router();
router.get('/boot', this.getDashboardBoot);
return router;
}
/**
* Retrieve the dashboard boot meta.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
getDashboardBoot = async (
req: Request,
res: Response,
next: NextFunction
) => {
const authorizedUser = req.user;
const { tenantId } = req;
try {
const meta = await this.dashboardService.getBootMeta(
tenantId,
authorizedUser
);
return res.status(200).send({ meta });
} catch (error) {
next(error);
}
};
}

View File

@@ -0,0 +1,220 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import { ServiceError } from '@/exceptions';
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
@Service()
export default class ExchangeRatesController extends BaseController {
@Inject()
exchangeRatesService: ExchangeRatesService;
@Inject()
dynamicListService: DynamicListingService;
/**
* Constructor method.
*/
router() {
const router = Router();
router.get(
'/',
[...this.exchangeRatesListSchema],
this.validationResult,
asyncMiddleware(this.exchangeRates.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.handleServiceError,
);
router.post(
'/',
[...this.exchangeRateDTOSchema],
this.validationResult,
asyncMiddleware(this.addExchangeRate.bind(this)),
this.handleServiceError
);
router.post(
'/:id',
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
this.validationResult,
asyncMiddleware(this.editExchangeRate.bind(this)),
this.handleServiceError
);
router.delete(
'/:id',
[...this.exchangeRateIdSchema],
this.validationResult,
asyncMiddleware(this.deleteExchangeRate.bind(this)),
this.handleServiceError
);
return router;
}
get exchangeRatesListSchema() {
return [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
];
}
get exchangeRateDTOSchema() {
return [
check('exchange_rate').exists().isNumeric().toFloat(),
check('currency_code').exists().trim().escape(),
check('date').exists().isISO8601(),
];
}
get exchangeRateEditDTOSchema() {
return [check('exchange_rate').exists().isNumeric().toFloat()];
}
get exchangeRateIdSchema() {
return [param('id').isNumeric().toInt()];
}
get exchangeRatesIdsSchema() {
return [
query('ids').isArray({ min: 2 }),
query('ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve exchange rates.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async exchangeRates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
page: 1,
pageSize: 12,
filterRoles: [],
columnSortBy: 'created_at',
sortOrder: 'asc',
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
tenantId,
filter
);
return res.status(200).send({ exchange_rates: exchangeRates });
} catch (error) {
next(error);
}
}
/**
* Adds a new exchange rate on the given date.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const exchangeRateDTO = this.matchedBodyData(req);
try {
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
tenantId,
exchangeRateDTO
);
return res.status(200).send({ id: exchangeRate.id });
} catch (error) {
next(error);
}
}
/**
* Edit the given exchange rate.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: exchangeRateId } = req.params;
const exchangeRateDTO = this.matchedBodyData(req);
try {
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
tenantId,
exchangeRateId,
exchangeRateDTO
);
return res.status(200).send({
id: exchangeRateId,
message: 'The exchange rate has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Delete the given exchange rate from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: exchangeRateId } = req.params;
try {
await this.exchangeRatesService.deleteExchangeRate(
tenantId,
exchangeRateId
);
return res.status(200).send({ id: exchangeRateId });
} catch (error) {
next(error);
}
}
/**
* Handle service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handleServiceError(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
return res.status(404).send({
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
});
}
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
return res.status(400).send({
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
});
}
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
return res.status(400).send({
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,456 @@
import { Inject, Service } from 'typedi';
import { check, param, query } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController';
import {
AbilitySubject,
ExpenseAction,
IExpenseCreateDTO,
IExpenseEditDTO,
} from '@/interfaces';
import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ExpensesApplication } from '@/services/Expenses/ExpensesApplication';
@Service()
export class ExpensesController extends BaseController {
@Inject()
private expensesApplication: ExpensesApplication;
@Inject()
private dynamicListService: DynamicListingService;
/**
* Express router.
*/
router() {
const router = Router();
router.post(
'/',
CheckPolicies(ExpenseAction.Create, AbilitySubject.Expense),
[...this.expenseDTOSchema],
this.validationResult,
asyncMiddleware(this.newExpense.bind(this)),
this.catchServiceErrors
);
router.post(
'/:id/publish',
CheckPolicies(ExpenseAction.Edit, AbilitySubject.Expense),
[...this.expenseParamSchema],
this.validationResult,
asyncMiddleware(this.publishExpense.bind(this)),
this.catchServiceErrors
);
router.post(
'/:id',
CheckPolicies(ExpenseAction.Edit, AbilitySubject.Expense),
[...this.editExpenseDTOSchema, ...this.expenseParamSchema],
this.validationResult,
asyncMiddleware(this.editExpense.bind(this)),
this.catchServiceErrors
);
router.delete(
'/:id',
CheckPolicies(ExpenseAction.Delete, AbilitySubject.Expense),
[...this.expenseParamSchema],
this.validationResult,
asyncMiddleware(this.deleteExpense.bind(this)),
this.catchServiceErrors
);
router.get(
'/',
CheckPolicies(ExpenseAction.View, AbilitySubject.Expense),
[...this.expensesListSchema],
this.validationResult,
asyncMiddleware(this.getExpensesList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors
);
router.get(
'/:id',
CheckPolicies(ExpenseAction.View, AbilitySubject.Expense),
[this.expenseParamSchema],
this.validationResult,
asyncMiddleware(this.getExpense.bind(this)),
this.catchServiceErrors
);
return router;
}
/**
* Expense DTO schema.
*/
get expenseDTOSchema() {
return [
check('reference_no')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('payment_date').exists().isISO8601().toDate(),
check('payment_account_id')
.exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('description')
.optional({ nullable: true })
.isString()
.isLength({ max: DATATYPES_LENGTH.TEXT }),
check('currency_code').optional().isString().isLength({ max: 3 }),
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(),
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('categories').exists().isArray({ min: 1 }),
check('categories.*.index')
.exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('categories.*.expense_account_id')
.exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('categories.*.amount')
.optional({ nullable: true })
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
.toFloat(),
check('categories.*.description')
.optional()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
check('categories.*.project_id')
.optional({ nullable: true })
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
];
}
/**
* Edit expense validation schema.
*/
get editExpenseDTOSchema() {
return [
check('reference_no')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('payment_date').exists().isISO8601().toDate(),
check('payment_account_id')
.exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('description')
.optional({ nullable: true })
.isString()
.isLength({ max: DATATYPES_LENGTH.TEXT }),
check('currency_code').optional().isString().isLength({ max: 3 }),
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(),
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('categories').exists().isArray({ min: 1 }),
check('categories.*.id').optional().isNumeric().toInt(),
check('categories.*.index')
.exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('categories.*.expense_account_id')
.exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('categories.*.amount')
.optional({ nullable: true })
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
.toFloat(),
check('categories.*.description')
.optional()
.trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
check('categories.*.project_id')
.optional({ nullable: true })
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
];
}
/**
* Expense param validation schema.
*/
get expenseParamSchema() {
return [param('id').exists().isNumeric().toInt()];
}
/**
* Expenses list validation schema.
*/
get expensesListSchema() {
return [
query('view_slug').optional({ nullable: true }).isString().trim(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('search_keyword').optional({ nullable: true }).isString().trim(),
];
}
/**
* Creates a new expense on
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async newExpense(req: Request, res: Response, next: NextFunction) {
const expenseDTO: IExpenseCreateDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
try {
const expense = await this.expensesApplication.createExpense(
tenantId,
expenseDTO,
user
);
return res.status(200).send({
id: expense.id,
message: 'The expense has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Edits details of the given expense.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editExpense(req: Request, res: Response, next: NextFunction) {
const { id: expenseId } = req.params;
const expenseDTO: IExpenseEditDTO = this.matchedBodyData(req);
const { tenantId, user } = req;
try {
await this.expensesApplication.editExpense(
tenantId,
expenseId,
expenseDTO,
user
);
return res.status(200).send({
id: expenseId,
message: 'The expense has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Deletes the given expense.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: expenseId } = req.params;
try {
await this.expensesApplication.deleteExpense(tenantId, expenseId, user);
return res.status(200).send({
id: expenseId,
message: 'The expense has been deleted successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Publishs the given expense.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async publishExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: expenseId } = req.params;
try {
await this.expensesApplication.publishExpense(tenantId, expenseId, user);
return res.status(200).send({
id: expenseId,
message: 'The expense has been published successfully',
});
} catch (error) {
next(error);
}
}
/**
* Retrieve expneses list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getExpensesList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
sortOrder: 'desc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
try {
const { expenses, pagination, filterMeta } =
await this.expensesApplication.getExpenses(tenantId, filter);
return res.status(200).send({
expenses: this.transfromToResponse(expenses),
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
}
}
/**
* Retrieve expense details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: expenseId } = req.params;
try {
const expense = await this.expensesApplication.getExpense(
tenantId,
expenseId
);
return res.status(200).send(this.transfromToResponse({ expense }));
} catch (error) {
next(error);
}
}
/**
* Transform service errors to api response errors.
* @param {Response} res
* @param {ServiceError} error
*/
private catchServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'expense_not_found') {
return res.boom.badRequest('Expense not found.', {
errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }],
});
}
if (error.errorType === 'EXPENSES_NOT_FOUND') {
return res.boom.badRequest('Expenses not found.', {
errors: [{ type: 'EXPENSES_NOT_FOUND', code: 110 }],
});
}
if (error.errorType === 'total_amount_equals_zero') {
return res.boom.badRequest('Expense total should not equal zero.', {
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }],
});
}
if (error.errorType === 'payment_account_not_found') {
return res.boom.badRequest('Payment account not found.', {
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }],
});
}
if (error.errorType === 'some_expenses_not_found') {
return res.boom.badRequest('Some expense accounts not found.', {
errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }],
});
}
if (error.errorType === 'payment_account_has_invalid_type') {
return res.boom.badRequest('Payment account has invalid type.', {
errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }],
});
}
if (error.errorType === 'expenses_account_has_invalid_type') {
return res.boom.badRequest(null, {
errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE', code: 600 }],
});
}
if (error.errorType === 'expense_already_published') {
return res.boom.badRequest(null, {
errors: [{ type: 'EXPENSE_ALREADY_PUBLISHED', code: 700 }],
});
}
if (error.errorType === 'contact_not_found') {
return res.boom.badRequest(null, {
errors: [{ type: 'CONTACT_NOT_FOUND', code: 800 }],
});
}
if (error.errorType === 'EXPENSE_HAS_ASSOCIATED_LANDED_COST') {
return res.status(400).send({
errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }],
});
}
if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') {
return res.status(400).send({
errors: [
{ type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', code: 1000 },
],
});
}
if (
error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES'
) {
return res.status(400).send({
errors: [
{
type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
code: 1100,
},
],
});
}
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
return res.boom.badRequest(null, {
errors: [
{
type: 'TRANSACTIONS_DATE_LOCKED',
code: 4000,
data: { ...error.payload },
},
],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,15 @@
import { Router } from 'express';
import { Container, Service } from 'typedi';
import { ExpensesController } from './Expenses';
@Service()
export default class ExpensesBaseController {
router() {
const router = Router();
router.use('/', Container.get(ExpensesController).router());
return router;
}
}

View File

@@ -0,0 +1,107 @@
import { Router } from 'express';
import { Container, Service } from 'typedi';
import BalanceSheetController from './FinancialStatements/BalanceSheet';
import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet';
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
import JournalSheetController from './FinancialStatements/JournalSheet';
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
import APAgingSummary from './FinancialStatements/APAgingSummary';
import PurchasesByItemsController from './FinancialStatements/PurchasesByItem';
import SalesByItemsController from './FinancialStatements/SalesByItems';
import InventoryValuationController from './FinancialStatements/InventoryValuationSheet';
import CustomerBalanceSummaryController from './FinancialStatements/CustomerBalanceSummary';
import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary';
import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers';
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
@Service()
export default class FinancialStatementsService {
/**
* Router constructor.
*/
router() {
const router = Router();
router.use(
'/balance_sheet',
Container.get(BalanceSheetController).router()
);
router.use(
'/profit_loss_sheet',
Container.get(ProfitLossController).router()
);
router.use(
'/general_ledger',
Container.get(GeneralLedgerController).router()
);
router.use(
'/trial_balance_sheet',
Container.get(TrialBalanceSheetController).router()
);
router.use('/journal', Container.get(JournalSheetController).router());
router.use(
'/receivable_aging_summary',
Container.get(ARAgingSummary).router()
);
router.use(
'/payable_aging_summary',
Container.get(APAgingSummary).router()
);
router.use(
'/purchases-by-items',
Container.get(PurchasesByItemsController).router()
);
router.use(
'/sales-by-items',
Container.get(SalesByItemsController).router()
);
router.use(
'/inventory-valuation',
Container.get(InventoryValuationController).router()
);
router.use(
'/customer-balance-summary',
Container.get(CustomerBalanceSummaryController).router(),
);
router.use(
'/vendor-balance-summary',
Container.get(VendorBalanceSummaryController).router(),
);
router.use(
'/transactions-by-customers',
Container.get(TransactionsByCustomers).router(),
);
router.use(
'/transactions-by-vendors',
Container.get(TransactionsByVendors).router(),
);
router.use(
'/cash-flow',
Container.get(CashFlowStatementController).router(),
);
router.use(
'/inventory-item-details',
Container.get(InventoryDetailsController).router(),
);
router.use(
'/transactions-by-reference',
Container.get(TransactionsByReferenceController).router(),
);
router.use(
'/cashflow-account-transactions',
Container.get(CashflowAccountTransactions).router(),
);
router.use(
'/project-profitability-summary',
Container.get(ProjectProfitabilityController).router(),
)
return router;
}
}

View File

@@ -0,0 +1,69 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator';
import { Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import APAgingSummaryReportService from '@/services/FinancialStatements/AgingSummary/APAgingSummaryService';
import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
export default class APAgingSummaryReportController extends BaseFinancialReportController {
@Inject()
APAgingSummaryService: APAgingSummaryReportService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report),
this.validationSchema,
asyncMiddleware(this.payableAgingSummary.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema() {
return [
...this.sheetNumberFormatValidationSchema,
query('as_date').optional().isISO8601(),
query('aging_days_before').optional().isNumeric().toInt(),
query('aging_periods').optional().isNumeric().toInt(),
query('vendors_ids').optional().isArray({ min: 1 }),
query('vendors_ids.*').isInt({ min: 1 }).toInt(),
query('none_zero').default(true).isBoolean().toBoolean(),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve payable aging summary report.
*/
async payableAgingSummary(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const filter = this.matchedQueryData(req);
try {
const { data, columns, query, meta } =
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
return res.status(200).send({
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,74 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response } from 'express';
import { query } from 'express-validator';
import ARAgingSummaryService from '@/services/FinancialStatements/AgingSummary/ARAgingSummaryService';
import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class ARAgingSummaryReportController extends BaseFinancialReportController {
@Inject()
ARAgingSummaryService: ARAgingSummaryService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report),
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.receivableAgingSummary.bind(this))
);
return router;
}
/**
* AR aging summary validation roles.
*/
get validationSchema() {
return [
...this.sheetNumberFormatValidationSchema,
query('as_date').optional().isISO8601(),
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
query('customers_ids').optional().isArray({ min: 1 }),
query('customers_ids.*').isInt({ min: 1 }).toInt(),
query('none_zero').default(true).isBoolean().toBoolean(),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve AR aging summary report.
*/
async receivableAgingSummary(req: Request, res: Response) {
const { tenantId, settings } = req;
const filter = this.matchedQueryData(req);
try {
const { data, columns, query, meta } =
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
return res.status(200).send({
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
} catch (error) {
console.log(error);
}
}
}

View File

@@ -0,0 +1,126 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import { castArray } from 'lodash';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BalanceSheetStatementService from '@/services/FinancialStatements/BalanceSheet/BalanceSheetService';
import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import BalanceSheetTable from '@/services/FinancialStatements/BalanceSheet/BalanceSheetTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class BalanceSheetStatementController extends BaseFinancialReportController {
@Inject()
balanceSheetService: BalanceSheetStatementService;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report),
this.balanceSheetValidationSchema,
this.validationResult,
asyncMiddleware(this.balanceSheet.bind(this))
);
return router;
}
/**
* Balance sheet validation schecma.
* @returns {ValidationChain[]}
*/
get balanceSheetValidationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('accounting_method').optional().isIn(['cash', 'accural']),
query('from_date').optional(),
query('to_date').optional(),
query('display_columns_type').optional().isIn(['date_periods', 'total']),
query('display_columns_by')
.optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']),
query('account_ids').isArray().optional(),
query('account_ids.*').isNumeric().toInt(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
// Percentage of column/row.
query('percentage_of_column').optional().isBoolean().toBoolean(),
query('percentage_of_row').optional().isBoolean().toBoolean(),
// Camparsion periods periods.
query('previous_period').optional().isBoolean().toBoolean(),
query('previous_period_amount_change').optional().isBoolean().toBoolean(),
query('previous_period_percentage_change')
.optional()
.isBoolean()
.toBoolean(),
// Camparsion periods periods.
query('previous_year').optional().isBoolean().toBoolean(),
query('previous_year_amount_change').optional().isBoolean().toBoolean(),
query('previous_year_percentage_change')
.optional()
.isBoolean()
.toBoolean(),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve the balance sheet.
*/
async balanceSheet(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const i18n = this.tenancy.i18n(tenantId);
let filter = this.matchedQueryData(req);
filter = {
...filter,
accountsIds: castArray(filter.accountsIds),
};
try {
const { data, columns, query, meta } =
await this.balanceSheetService.balanceSheet(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
const table = new BalanceSheetTable(data, query, i18n);
switch (acceptType) {
case 'application/json+table':
return res.status(200).send({
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query,
meta,
});
case 'json':
default:
return res.status(200).send({ data, columns, query, meta });
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,26 @@
import { query } from 'express-validator';
import BaseController from "../BaseController";
export default class BaseFinancialReportController extends BaseController {
get sheetNumberFormatValidationSchema() {
return [
query('number_format.precision')
.optional()
.isInt({ min: 0, max: 5 })
.toInt(),
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
query('number_format.show_zero').optional().isBoolean().toBoolean(),
query('number_format.format_money')
.optional()
.isIn(['total', 'always', 'none'])
.trim(),
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim()
.escape(),
];
}
}

View File

@@ -0,0 +1,137 @@
import { Inject, Service } from 'typedi';
import { query } from 'express-validator';
import {
NextFunction,
Router,
Request,
Response,
ValidationChain,
} from 'express';
import BaseFinancialReportController from '../BaseFinancialReportController';
import CashFlowStatementService from '@/services/FinancialStatements/CashFlow/CashFlowService';
import {
ICashFlowStatementDOO,
ICashFlowStatement,
AbilitySubject,
ReportsAction,
} from '@/interfaces';
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class CashFlowController extends BaseFinancialReportController {
@Inject()
cashFlowService: CashFlowStatementService;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_CASHFLOW, AbilitySubject.Report),
this.cashflowValidationSchema,
this.validationResult,
this.asyncMiddleware(this.cashFlow.bind(this))
);
return router;
}
/**
* Balance sheet validation schecma.
* @returns {ValidationChain[]}
*/
get cashflowValidationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('from_date').optional(),
query('to_date').optional(),
query('display_columns_type').optional().isIn(['date_periods', 'total']),
query('display_columns_by')
.optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(cashFlowDOO: ICashFlowStatementDOO) {
const { data, query, meta } = cashFlowDOO;
return {
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
};
}
/**
* Transformes the report statement to table rows.
* @param {ITransactionsByVendorsStatement} statement -
*/
private transformToTableRows(
cashFlowDOO: ICashFlowStatementDOO,
tenantId: number
) {
const i18n = this.tenancy.i18n(tenantId);
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
return {
table: {
data: cashFlowTable.tableRows(),
columns: cashFlowTable.tableColumns(),
},
query: this.transfromToResponse(cashFlowDOO.query),
meta: this.transfromToResponse(cashFlowDOO.meta),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async cashFlow(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const filter = {
...this.matchedQueryData(req),
};
try {
const cashFlow = await this.cashFlowService.cashFlow(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res
.status(200)
.send(this.transformToTableRows(cashFlow, tenantId));
case 'json':
default:
return res.status(200).send(this.transformJsonResponse(cashFlow));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,168 @@
import { Inject, Service } from 'typedi';
import { query } from 'express-validator';
import {
NextFunction,
Router,
Request,
Response,
ValidationChain,
} from 'express';
import BaseFinancialReportController from '../BaseFinancialReportController';
import {
AbilitySubject,
ICashFlowStatementDOO,
ReportsAction,
} from '@/interfaces';
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import CashflowAccountTransactionsService from '@/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsService';
import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class CashFlowAccountTransactionsController extends BaseFinancialReportController {
@Inject()
tenancy: HasTenancyService;
@Inject()
cashflowAccountTransactions: CashflowAccountTransactionsService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_CASHFLOW_ACCOUNT_TRANSACTION,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.cashFlow),
this.catchServiceErrors
);
return router;
}
/**
* Cashflow account transactions validation schecma.
* @returns {ValidationChain[]}
*/
get validationSchema(): ValidationChain[] {
return [
query('account_id').exists().isInt().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
];
}
/**
* Retrieve the cashflow account transactions statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(casahflowAccountTransactions) {
const { transactions, pagination } = casahflowAccountTransactions;
return {
transactions: this.transfromToResponse(transactions),
pagination: this.transfromToResponse(pagination),
};
}
/**
* Transformes the report statement to table rows.
* @param {ITransactionsByVendorsStatement} statement -
*/
private transformToTableRows(
cashFlowDOO: ICashFlowStatementDOO,
tenantId: number
) {
const i18n = this.tenancy.i18n(tenantId);
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
return {
table: {
data: cashFlowTable.tableRows(),
columns: cashFlowTable.tableColumns(),
},
query: this.transfromToResponse(cashFlowDOO.query),
meta: this.transfromToResponse(cashFlowDOO.meta),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
private cashFlow = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const cashFlowAccountTransactions =
await this.cashflowAccountTransactions.cashflowAccountTransactions(
tenantId,
query
);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
// case 'application/json+table':
// return res
// .status(200)
// .send(this.transformToTableRows(cashFlow, tenantId));
case 'json':
default:
return res
.status(200)
.send(this.transformJsonResponse(cashFlowAccountTransactions));
}
} catch (error) {
next(error);
}
};
/**
* Catches the service errors.
* @param {Error} error - Error.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next -
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
if (error.errorType === 'ACCOUNT_ID_HAS_INVALID_TYPE') {
return res.boom.badRequest(
'The given account id should be cash, bank or credit card type.',
{
errors: [{ type: 'ACCOUNT_ID_HAS_INVALID_TYPE', code: 200 }],
}
);
}
if (error.errorType === 'account_not_found') {
return res.boom.notFound('The given account not found.', {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
});
}
}
next(error);
}
}

View File

@@ -0,0 +1,139 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator';
import { Inject } from 'typedi';
import {
AbilitySubject,
ICustomerBalanceSummaryStatement,
ReportsAction,
} from '@/interfaces';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import CustomerBalanceSummary from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService';
import BaseFinancialReportController from '../BaseFinancialReportController';
import CustomerBalanceSummaryTableRows from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import HasTenancyService from '@/services/Tenancy/TenancyService';
export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController {
@Inject()
customerBalanceSummaryService: CustomerBalanceSummary;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.customerBalanceSummary.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema() {
return [
...this.sheetNumberFormatValidationSchema,
// As date.
query('as_date').optional().isISO8601(),
// Percentages.
query('percentage_column').optional().isBoolean().toBoolean(),
// Filters none-zero or none-transactions.
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
// Customers ids.
query('customers_ids').optional().isArray({ min: 1 }),
query('customers_ids.*').exists().isInt().toInt(),
];
}
/**
* Transformes the balance summary statement to table rows.
* @param {ICustomerBalanceSummaryStatement} statement -
*/
private transformToTableRows(
tenantId,
{ data, query }: ICustomerBalanceSummaryStatement
) {
const i18n = this.tenancy.i18n(tenantId);
const tableRows = new CustomerBalanceSummaryTableRows(data, query, i18n);
return {
table: {
columns: tableRows.tableColumns(),
data: tableRows.tableRows(),
},
query: this.transfromToResponse(query),
};
}
/**
* Transformes the balance summary statement to raw json.
* @param {ICustomerBalanceSummaryStatement} customerBalance -
*/
private transformToJsonResponse({
data,
columns,
query,
}: ICustomerBalanceSummaryStatement) {
return {
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
};
}
/**
* Retrieve payable aging summary report.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async customerBalanceSummary(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, settings } = req;
const filter = this.matchedQueryData(req);
try {
const customerBalanceSummary =
await this.customerBalanceSummaryService.customerBalanceSummary(
tenantId,
filter
);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res
.status(200)
.send(this.transformToTableRows(tenantId, customerBalanceSummary));
case 'application/json':
default:
return res
.status(200)
.send(this.transformToJsonResponse(customerBalanceSummary));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,79 @@
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';
@Service()
export default class GeneralLedgerReportController extends BaseFinancialReportController {
@Inject()
generalLedgetService: GeneralLedgerService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.generalLedger.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
query('basis').optional(),
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
query('none_transactions').default(true).isBoolean().toBoolean(),
query('accounts_ids').optional().isArray({ min: 1 }),
query('accounts_ids.*').isInt().toInt(),
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
query('order').optional().isIn(['desc', 'asc']),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve the general ledger financial statement.
* @param {Request} req -
* @param {Response} res -
*/
async generalLedger(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.generalLedgetService.generalLedger(tenantId, filter);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,148 @@
import { Inject, Service } from 'typedi';
import { query } from 'express-validator';
import {
NextFunction,
Router,
Request,
Response,
ValidationChain,
} from 'express';
import BaseController from '@/api/controllers/BaseController';
import InventoryDetailsService from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsService';
import InventoryDetailsTable from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class InventoryDetailsController extends BaseController {
@Inject()
inventoryDetailsService: InventoryDetailsService;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_INVENTORY_ITEM_DETAILS,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.inventoryDetails.bind(this))
);
return router;
}
/**
* Balance sheet validation schecma.
* @returns {ValidationChain[]}
*/
get validationSchema(): ValidationChain[] {
return [
query('number_format.precision')
.optional()
.isInt({ min: 0, max: 5 })
.toInt(),
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim()
.escape(),
query('from_date').optional(),
query('to_date').optional(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
query('items_ids').optional().isArray(),
query('items_ids.*').optional().isInt().toInt(),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
// Filtering by warehouses.
query('warehouses_ids').optional().toArray().isArray({ min: 1 }),
query('warehouses_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(inventoryDetails) {
const { data, query, meta } = inventoryDetails;
return {
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
};
}
/**
* Transformes the report statement to table rows.
*/
private transformToTableRows(inventoryDetails, tenantId: number) {
const i18n = this.tenancy.i18n(tenantId);
const inventoryDetailsTable = new InventoryDetailsTable(
inventoryDetails,
i18n
);
return {
table: {
data: inventoryDetailsTable.tableData(),
columns: inventoryDetailsTable.tableColumns(),
},
query: this.transfromToResponse(inventoryDetails.query),
meta: this.transfromToResponse(inventoryDetails.meta),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async inventoryDetails(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const filter = {
...this.matchedQueryData(req),
};
try {
const inventoryDetails =
await this.inventoryDetailsService.inventoryDetails(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res
.status(200)
.send(this.transformToTableRows(inventoryDetails, tenantId));
case 'json':
default:
return res
.status(200)
.send(this.transformJsonResponse(inventoryDetails));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,89 @@
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 BaseFinancialReportController from './BaseFinancialReportController';
import InventoryValuationService from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class InventoryValuationReportController extends BaseFinancialReportController {
@Inject()
inventoryValuationService: InventoryValuationService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.inventoryValuation.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
query('items_ids').optional().isArray(),
query('items_ids.*').optional().isInt().toInt(),
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
query('none_transactions').default(true).isBoolean().toBoolean(),
query('none_zero').default(false).isBoolean().toBoolean(),
query('only_active').default(false).isBoolean().toBoolean(),
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
query('order').optional().isIn(['desc', 'asc']),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
// Filtering by warehouses.
query('warehouses_ids').optional().toArray().isArray({ min: 1 }),
query('warehouses_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve the general ledger financial statement.
* @param {Request} req -
* @param {Response} res -
*/
async inventoryValuation(req: Request, res: Response, next: NextFunction) {
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);
}
}
}

View File

@@ -0,0 +1,84 @@
import { Inject, Service } from 'typedi';
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';
@Service()
export default class JournalSheetController extends BaseFinancialReportController {
@Inject()
journalService: JournalSheetService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_JOURNAL, AbilitySubject.Report),
this.journalValidationSchema,
this.validationResult,
this.asyncMiddleware(this.journal.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get journalValidationSchema() {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
query('transaction_type').optional().trim().escape(),
query('transaction_id').optional().isInt().toInt(),
oneOf(
[
query('account_ids').optional().isArray({ min: 1 }),
query('account_ids.*').optional().isNumeric().toInt(),
],
[query('account_ids').optional().isNumeric().toInt()]
),
query('from_range').optional().isNumeric().toInt(),
query('to_range').optional().isNumeric().toInt(),
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
];
}
/**
* Retrieve the ledger report of the given account.
* @param {Request} req -
* @param {Response} res -
*/
async journal(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
let filter = this.matchedQueryData(req);
filter = {
...filter,
accountsIds: castArray(filter.accountsIds),
};
try {
const { data, query, meta } = await this.journalService.journalSheet(
tenantId,
filter
);
return res.status(200).send({
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,124 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import ProfitLossSheetService from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService';
import BaseFinancialReportController from './BaseFinancialReportController';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import { ProfitLossSheetTable } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export default class ProfitLossSheetController extends BaseFinancialReportController {
@Inject()
profitLossSheetService: ProfitLossSheetService;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report),
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.profitLossSheet.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('basis').optional(),
query('from_date').optional().isISO8601().toDate(),
query('to_date').optional().isISO8601().toDate(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
query('accounts_ids').isArray().optional(),
query('accounts_ids.*').isNumeric().toInt(),
query('display_columns_type').optional().isIn(['total', 'date_periods']),
query('display_columns_by')
.optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']),
// Percentage options.
query('percentage_column').optional().isBoolean().toBoolean(),
query('percentage_row').optional().isBoolean().toBoolean(),
query('percentage_expense').optional().isBoolean().toBoolean(),
query('percentage_income').optional().isBoolean().toBoolean(),
// Camparsion periods periods.
query('previous_period').optional().isBoolean().toBoolean(),
query('previous_period_amount_change').optional().isBoolean().toBoolean(),
query('previous_period_percentage_change')
.optional()
.isBoolean()
.toBoolean(),
// Camparsion periods periods.
query('previous_year').optional().isBoolean().toBoolean(),
query('previous_year_amount_change').optional().isBoolean().toBoolean(),
query('previous_year_percentage_change')
.optional()
.isBoolean()
.toBoolean(),
// Filtering by branches.
query('branches_ids').optional().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve profit/loss financial statement.
* @param {Request} req -
* @param {Response} res -
*/
async profitLossSheet(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const i18n = this.tenancy.i18n(tenantId);
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.profitLossSheetService.profitLossSheet(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
const table = new ProfitLossSheetTable(data, query, i18n);
return res.status(200).send({
table: {
rows: table.tableRows(),
columns: table.tableColumns(),
},
query,
meta,
});
case 'json':
default:
return res.status(200).send({
data,
query,
meta,
});
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,151 @@
import { Inject, Service } from 'typedi';
import { query } from 'express-validator';
import {
NextFunction,
Router,
Request,
Response,
ValidationChain,
} from 'express';
import BaseFinancialReportController from '../BaseFinancialReportController';
import {
ICashFlowStatementDOO,
AbilitySubject,
ReportsAction,
IProjectProfitabilitySummaryPOJO,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ProjectProfitabilitySummaryTable } from '@/services/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable';
import { ProjectProfitabilitySummaryService } from '@/services/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryService';
@Service()
export default class ProjectProfitabilityController extends BaseFinancialReportController {
@Inject()
private projectProfitabilityService: ProjectProfitabilitySummaryService;
@Inject()
private tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_PROJECT_PROFITABILITY_SUMMARY,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.projectProfitabilitySummary.bind(this))
);
return router;
}
/**
* Balance sheet validation schecma.
* @returns {ValidationChain[]}
*/
get validationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('from_date').optional(),
query('to_date').optional(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
// Filtering by projects.
query('products_ids').optional().toArray().isArray({ min: 1 }),
query('products_ids.*').isNumeric().toInt(),
// Filtering by branches.
query('branches_ids').optional().toArray().isArray({ min: 1 }),
query('branches_ids.*').isNumeric().toInt(),
];
}
/**
* Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(projectProfitabilityPOJO: IProjectProfitabilitySummaryPOJO) {
const { data, query, meta } = projectProfitabilityPOJO;
return {
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
};
}
/**
* Transformes the report statement to table rows.
* @param {ITransactionsByVendorsStatement} statement -
*/
private transformToTableRows(
projectProfitabilityPOJO: IProjectProfitabilitySummaryPOJO,
tenantId: number
) {
const i18n = this.tenancy.i18n(tenantId);
const projectProfitabilityTable = new ProjectProfitabilitySummaryTable(
projectProfitabilityPOJO.data,
i18n
);
return {
table: {
data: projectProfitabilityTable.tableData(),
columns: projectProfitabilityTable.tableColumns(),
},
query: this.transfromToResponse(projectProfitabilityPOJO.query),
// meta: this.transfromToResponse(cashFlowDOO.meta),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async projectProfitabilitySummary(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = {
...this.matchedQueryData(req),
};
try {
const projectProfitability =
await this.projectProfitabilityService.projectProfitabilitySummary(
tenantId,
filter
);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res
.status(200)
.send(this.transformToTableRows(projectProfitability, tenantId));
case 'json':
default:
return res
.status(200)
.send(this.transformJsonResponse(projectProfitability));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,82 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class PurchasesByItemReportController extends BaseFinancialReportController {
@Inject()
purchasesByItemsService: PurchasesByItemsService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_PURCHASES_BY_ITEMS,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.purchasesByItems.bind(this))
);
return router;
}
/**
* Validation schema.
* @return {ValidationChain[]}
*/
get validationSchema(): ValidationChain[] {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
// Filter items.
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
// Filters items.
query('none_transactions').optional().isBoolean().toBoolean(),
query('only_active').optional().isBoolean().toBoolean(),
// Specific items.
query('items_ids').optional().isArray(),
query('items_ids.*').optional().isInt().toInt(),
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
query('order').optional().isIn(['desc', 'asc']),
];
}
/**
* Retrieve the general ledger financial statement.
* @param {Request} req -
* @param {Response} res -
*/
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } =
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,84 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController';
import SalesByItemsReportService from '@/services/FinancialStatements/SalesByItems/SalesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class SalesByItemsReportController extends BaseFinancialReportController {
@Inject()
salesByItemsService: SalesByItemsReportService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_SALES_BY_ITEMS,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.purchasesByItems.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
// Specific items.
query('items_ids').optional().isArray(),
query('items_ids.*').optional().isInt().toInt(),
// Number format.
query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
// Filters items.
query('none_transactions').default(true).isBoolean().toBoolean(),
query('only_active').default(false).isBoolean().toBoolean(),
// Order by.
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
query('order').optional().isIn(['desc', 'asc']),
];
}
/**
* Retrieve the general ledger financial statement.
* @param {Request} req -
* @param {Response} res -
*/
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const { data, query, meta } = await this.salesByItemsService.salesByItems(
tenantId,
filter
);
return res.status(200).send({
meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
});
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,131 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator';
import { Inject, Service } from 'typedi';
import {
AbilitySubject,
ITransactionsByCustomersStatement,
ReportsAction,
} from '@/interfaces';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from '../BaseFinancialReportController';
import TransactionsByCustomersService from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService';
import TransactionsByCustomersTableRows from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
@Service()
export default class TransactionsByCustomersReportController extends BaseFinancialReportController {
@Inject()
transactionsByCustomersService: TransactionsByCustomersService;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.transactionsByCustomers.bind(this))
);
return router;
}
/**
* Validation schema.
*/
private get validationSchema() {
return [
...this.sheetNumberFormatValidationSchema,
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
// Customers ids.
query('customers_ids').optional().isArray({ min: 1 }),
query('customers_ids.*').exists().isInt().toInt(),
];
}
/**
* Transformes the statement to table rows response.
* @param {ITransactionsByCustomersStatement} statement -
*/
private transformToTableResponse(customersTransactions, tenantId) {
const i18n = this.tenancy.i18n(tenantId);
const table = new TransactionsByCustomersTableRows(
customersTransactions,
i18n
);
return {
table: {
rows: table.tableRows(),
},
};
}
/**
* Transformes the statement to json response.
* @param {ITransactionsByCustomersStatement} statement -
*/
private transfromToJsonResponse(
data,
columns
): ITransactionsByCustomersStatement {
return {
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
};
}
/**
* Retrieve payable aging summary report.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async transactionsByCustomers(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const report =
await this.transactionsByCustomersService.transactionsByCustomers(
tenantId,
filter
);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'json':
return res
.status(200)
.send(this.transfromToJsonResponse(report.data, report.columns));
case 'application/json+table':
default:
return res
.status(200)
.send(this.transformToTableResponse(report.data, tenantId));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,90 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import TransactionsByReferenceService from '@/services/FinancialStatements/TransactionsByReference';
import { ITransactionsByReferenceTransaction } from '@/interfaces';
@Service()
export default class TransactionsByReferenceController extends BaseController {
@Inject()
private transactionsByReferenceService: TransactionsByReferenceService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.transactionsByReference.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
query('reference_id').exists().isInt(),
query('reference_type').exists().isString(),
query('number_format.precision')
.optional()
.isInt({ min: 0, max: 5 })
.toInt(),
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim()
.escape(),
];
}
/**
* Retrieve transactions by the given reference type and id.
* @param {Request} req - Request object.
* @param {Response} res - Response.
* @param {NextFunction} next
* @returns
*/
public async transactionsByReference(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const data =
await this.transactionsByReferenceService.getTransactionsByReference(
tenantId,
filter
);
return res
.status(200)
.send(this.transformToJsonResponse(data.transactions));
} catch (error) {
next(error);
}
}
/**
* Transformes the given report transaction to json response.
* @param transactions
* @returns
*/
private transformToJsonResponse(
transactions: ITransactionsByReferenceTransaction[]
) {
return {
transactions: this.transfromToResponse(transactions),
};
}
}

View File

@@ -0,0 +1,124 @@
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import { Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from '../BaseFinancialReportController';
import TransactionsByVendorsTableRows from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows';
import TransactionsByVendorsService from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
import {
AbilitySubject,
ITransactionsByVendorsStatement,
ReportsAction,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import CheckPolicies from '@/api/middleware/CheckPolicies';
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
@Inject()
transactionsByVendorsService: TransactionsByVendorsService;
@Inject()
tenancy: HasTenancyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_VENDORS_TRANSACTIONS,
AbilitySubject.Report
),
this.validationSchema,
this.validationResult,
asyncMiddleware(this.transactionsByVendors.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
// Vendors ids.
query('vendors_ids').optional().isArray({ min: 1 }),
query('vendors_ids.*').exists().isInt().toInt(),
];
}
/**
* Transformes the report statement to table rows.
* @param {ITransactionsByVendorsStatement} statement -
*/
private transformToTableRows(tenantId: number, transactions: any[]) {
const i18n = this.tenancy.i18n(tenantId);
const table = new TransactionsByVendorsTableRows(transactions, i18n);
return {
table: {
data: table.tableRows(),
},
};
}
/**
* Transformes the report statement to json response.
* @param {ITransactionsByVendorsStatement} statement -
*/
private transformToJsonResponse({
data,
columns,
query,
}: ITransactionsByVendorsStatement) {
return {
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
};
}
/**
* Retrieve payable aging summary report.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async transactionsByVendors(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const report =
await this.transactionsByVendorsService.transactionsByVendors(
tenantId,
filter
);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res
.status(200)
.send(this.transformToTableRows(tenantId, report.data));
case 'json':
default:
return res.status(200).send(this.transformToJsonResponse(report));
}
} catch (error) {
next(error);
}
}
}

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