Compare commits

...

198 Commits

Author SHA1 Message Date
a.bouhuolia
b8ce39d253 chore 2023-03-27 22:39:02 +02:00
a.bouhuolia
478670c1d5 chore: Github action to dockernize server 2023-03-27 22:37:38 +02:00
Ahmed Bouhuolia
bd80e7f7be Merge pull request #96 from bigcapitalhq/linces-software-GPL2
chore: license the software to GPLv2
2023-03-27 18:08:17 +02:00
a.bouhuolia
b38eeec600 chore: license the software to GPLv2 2023-03-27 18:06:27 +02:00
Ahmed Bouhuolia
faefd0b91d Update README.md 2023-03-26 18:51:30 +02:00
a.bouhuolia
aa89a83a83 chore: update CHANGELOG 2023-03-26 18:40:20 +02:00
a.bouhuolia
3fbb809e3d Merge branch 'develop' into main 2023-03-26 18:25:00 +02:00
a.bouhuolia
a17ef17d56 fix(webapp): content typo 2023-03-26 18:20:29 +02:00
a.bouhuolia
04cdd7c989 Merge branch 'develop' of https://github.com/bigcapitalhq/client into develop 2023-03-26 17:35:23 +02:00
a.bouhuolia
10fd576c38 fix(webapp): login page tweaks. 2023-03-26 17:35:08 +02:00
Ahmed Bouhuolia
54e6478211 Merge pull request #94 from bigcapitalhq/dependabot/npm_and_yarn/webpack-5.76.0
chore(deps-dev): bump webpack from 5.75.0 to 5.76.0
2023-03-19 01:13:34 +02:00
dependabot[bot]
a4d101fae9 chore(deps-dev): bump webpack from 5.75.0 to 5.76.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.75.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.75.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-15 10:55:30 +00:00
Ahmed Bouhuolia
41a68cf5e8 Merge pull request #90 from bigcapitalhq/docker-dev-prod
feat: add docker compose for development env.
2023-03-09 00:45:42 +02:00
Ahmed Bouhuolia
3076bc2684 Update vercel.json 2023-03-09 00:26:38 +02:00
Ahmed Bouhuolia
d244227023 Update vercel.json 2023-03-09 00:24:33 +02:00
Ahmed Bouhuolia
9007aca856 Update index.tsx 2023-03-09 00:12:56 +02:00
a.bouhuolia
061fc4fc18 chore: add vercel config file 2023-03-09 00:07:52 +02:00
a.bouhuolia
65c23949e6 chore: update frozen package-lock.json 2023-03-08 23:17:36 +02:00
Ahmed Bouhuolia
efa56624a9 Delete .vercelignore 2023-03-08 22:19:11 +02:00
Ahmed Bouhuolia
123573f022 Delete vercel.json 2023-03-08 22:12:12 +02:00
Ahmed Bouhuolia
7302ec4464 Update vercel.json 2023-03-08 22:11:58 +02:00
a.bouhuolia
2d9859cde0 Merge branch 'develop' into docker-dev-prod 2023-03-07 21:14:34 +02:00
a.bouhuolia
8c3d6b61d6 fix(webapp): import issue. 2023-03-07 21:13:49 +02:00
Ahmed Bouhuolia
0ce9c93077 Merge pull request #92 from bigcapitalhq/BIG-423-optimize-the-setup-pages-design
feat(webapp): optimize the setup organization page design
2023-03-07 20:43:44 +02:00
Ahmed Bouhuolia
c3a2ea5064 Merge pull request #89 from bigcapitalhq/BIG-422-deprecated-the-subscription-module
feat(server): deprecate the subscription module.
2023-03-07 20:42:11 +02:00
a.bouhuolia
28de827a99 chore(webapp): remove the un-used import 2023-03-07 20:41:09 +02:00
a.bouhuolia
b4559703f9 feat(webapp): optimize the setup organization page design 2023-03-06 23:05:21 +02:00
Ahmed Bouhuolia
7532b44a57 Merge pull request #91 from bigcapitalhq/vercel-ignore
fix: remove the ignore script from vercel.
2023-03-06 01:55:34 +02:00
Ahmed Bouhuolia
a142b734d3 fix: remove the ignore script from vercel. 2023-03-06 01:54:51 +02:00
a.bouhuolia
f26ced97fe feat(webapp): deprecate the subscription from webapp. 2023-03-05 13:21:06 +02:00
a.bouhuolia
25fb280e29 feat: add docker compose for development env. 2023-03-04 23:10:09 +02:00
a.bouhuolia
0c1bf302e5 feat(webapp): deprecate the subscription step in onboarding process 2023-03-04 23:08:02 +02:00
a.bouhuolia
57e3f68219 feat(server): deprecated the subscription module. 2023-03-02 22:49:46 +02:00
a.bouhuolia
3b79ac66ae feat(server): deprecated the subscription module. 2023-03-02 22:44:14 +02:00
a.bouhuolia
44fc26b156 feat(server): deprecated the subscription module. 2023-03-02 21:34:06 +02:00
Ahmed Bouhuolia
d46f8faf26 Merge pull request #88 from bigcapitalhq/BIG-411-no-icon-on-the-duplicate-item-menu
fix(webapp): add icon to duplicate item of items context menu
2023-02-16 22:34:58 +02:00
a.bouhuolia
2263cf5657 fix(webapp): add icon to duplicate item of items context menu 2023-02-16 22:34:14 +02:00
Ahmed Bouhuolia
058d525afc Merge pull request #87 from bigcapitalhq/BIG-412-inconsistent-style-of-quick-customer-vendor-drawer
fix(webapp): inconsistent style of quick customer/vendor drawer
2023-02-16 22:10:37 +02:00
a.bouhuolia
490b8e09f2 fix(webapp): inconsistent style of quick customer/vendor drawer 2023-02-16 22:09:18 +02:00
Ahmed Bouhuolia
e488c0eea9 Merge pull request #86 from bigcapitalhq/BIG-421-account-form-issues
fix: BIG-421 account form issues
2023-02-15 21:55:03 +02:00
a.bouhuolia
f093239a15 fix(webapp): retrieve nested graph accounts 2023-02-15 21:53:50 +02:00
a.bouhuolia
5c537e094d fix(server): retrieve nested graph accounts 2023-02-15 21:53:13 +02:00
a.bouhuolia
a371fd44f7 chore: update package-lock.json 2023-02-15 00:01:46 +02:00
Ahmed Bouhuolia
59cb168331 Merge pull request #85 from bigcapitalhq/BIG-414-control-max-nested-accounts-to-be-6-levels
feat(server): validate the max depth level of the parent account.
2023-02-14 23:48:45 +02:00
a.bouhuolia
8a5fbfc041 feat(server): validate the max depth level of the parent account. 2023-02-14 23:47:24 +02:00
Ahmed Bouhuolia
e3a072e267 Merge pull request #84 from bigcapitalhq/BIG-406-accounts-chart-lags-scroll-down
fix(webapp): accounts chart lags scroll down
2023-02-14 23:25:43 +02:00
a.bouhuolia
b03606406e fix(webapp): accounts chart lags scroll down 2023-02-14 23:20:01 +02:00
a.bouhuolia
a1a7ee2b5b chore: add ignoreCommand to vercel configure 2023-02-13 21:38:37 +02:00
a.bouhuolia
228ae71a1c Merge https://github.com/bigcapitalhq/client into develop 2023-02-13 21:26:59 +02:00
a.bouhuolia
71a8d3e77f chore: add file to vercel 2023-02-13 21:26:46 +02:00
Ahmed Bouhuolia
4ddeb927cc Merge pull request #83 from bigcapitalhq/bigcapital-cli
feat(server): bigcapital cli commands
2023-02-13 20:51:03 +02:00
a.bouhuolia
72c1685fa6 feat(server): move all cli commands codebase to be TS based. 2023-02-13 20:47:09 +02:00
a.bouhuolia
7e7ee24109 feat(server): bigcapital cli commands 2023-02-09 23:38:13 +02:00
Ahmed Bouhuolia
708d971717 ci: change webapp package name. 2023-02-08 23:35:41 +02:00
Ahmed Bouhuolia
7781d092ca ci: webapp Github actions (#81) 2023-02-08 23:33:03 +02:00
a.bouhuolia
d0e84fb51a chore: update vercel config 2023-02-08 00:02:43 +02:00
a.bouhuolia
0e673ffa7c chore: update Vercel config 2023-02-08 00:00:49 +02:00
a.bouhuolia
4c4c73db2d chore: add Vercel config file. 2023-02-07 23:57:52 +02:00
a.bouhuolia
0086ee5186 chore: change build script 2023-02-07 23:30:46 +02:00
a.bouhuolia
bb49fcb42b WIP 2023-02-07 23:07:19 +02:00
a.bouhuolia
d47b1165c4 chore: change supported node engine version. 2023-02-07 21:35:21 +02:00
a.bouhuolia
f2e1efcb45 chore(server): prettify files 2023-02-07 20:25:22 +02:00
a.bouhuolia
2e3b2cbf92 chore: add husky for commit message lint 2023-02-07 20:24:44 +02:00
a.bouhuolia
68e61429aa chore: update README.md 2023-02-07 19:30:13 +02:00
a.bouhuolia
646be4bb20 chore: add .gitkeep directory 2023-02-07 19:28:38 +02:00
a.bouhuolia
dfd8b0ca4e chore: remove the bin from the server. 2023-02-07 19:27:57 +02:00
a.bouhuolia
2ab1a5606a chore: update package-lock.json 2023-02-07 01:20:24 +02:00
Ahmed Bouhuolia
c33370d4d2 Merge pull request #75 from bigcapitalhq/lerna
Lerna
2023-02-06 23:39:28 +02:00
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
130008168a Merge branch 'develop' into main 2022-11-06 16:14:41 +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
a.bouhuolia
4d00f53600 feat: release v1.7.6-rc.2 version. 2022-04-23 00:46:16 +02:00
a.bouhuolia
5e293e4f19 Merge branch 'develop' into main 2022-04-23 00:42:23 +02:00
Ahmed Bouhuolia
01038136f2 Update CHANGELOG.md 2022-04-23 00:41:18 +02:00
a.bouhuolia
6bc5eec8b6 Merge branch 'develop' into main 2022-04-23 00:38:28 +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
13767 changed files with 325860 additions and 153971 deletions

View File

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

View File

@@ -19,7 +19,7 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: abouhuolia/bigcapital-client
IMAGE_NAME: abouhuolia/bigcapital-webapp
jobs:
setup-build-publish-deploy:
@@ -50,8 +50,9 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
file: ./packages/webapp/Dockerfile
push: true
tags: ghcr.io/bigcapitalhq/client:latest
tags: ghcr.io/bigcapitalhq/webapp:latest
labels: ${{ steps.meta.outputs.labels }}
# Send notification to Slack channel.
- name: Slack Notification built and published successfully.

View File

@@ -0,0 +1,30 @@
name: push
on:
release:
types: [created]
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE_NAME: abouhuolia/bigcapital-webapp
jobs:
image-build-and-push-webapp:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
# Login to Container registry.
- name: Log in to the Container registry
uses: docker/login-action@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
# Install and boostrap Lerna.
- name: Install and boostrap Lerna
working-directory: ./
run: npm install -g lerna && npm run bootstrap

25
.gitignore vendored
View File

@@ -1,23 +1,2 @@
# 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/
data

4
.husky/commit-msg Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn commitlint --edit

2
.husky/pre-commit Normal file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

1
.nvmrc Normal file
View File

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

View File

@@ -2,14 +2,78 @@
All notable changes to Bigcapital server-side will be in this file.
## [1.7.1-rc.2] - 30-03-2022
## [0.8.1] - 26-03-2023
## Added
`@bigcaptial/monorepo`
### Added
* add docker compose for development env. by @abouolia
`@bigcapital/webapp`
### Fixes
* fix: hide the project name entry if the feature was not enabled. by @abouolia
* fix: labels of add money in/out don't appear. by @abouolia
* fix: accounts chart lags when scrolling down. by @abouolia
* fix: the inconsistent style of quick customer/vendor drawer. by @abouolia
* fix: add an icon to duplicate item of items context menu. by @abouolia
* fix: account form issues. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/86
### Added
* Optimize the design of setup organization page. by @abouolia
`@bigcapital/server`
### Added
* bigcapital CLI commands by @abouolia
* deprecate the subscription module. @abouolia
### Fixes
* fix: Validate the max depth level of the parent account. by @abouolia
## [0.7.6] - 23-04-2022
`@bigcapital/webapp`
### Fixed
- `BIG-374` Refactoring sidebar men with ability permissions and feature control on each item.
## [0.7.5] - 20-04-2022
### Fixed.
- `BIG-378` Reports drawers columns css conflict.
## [0.7.3] - 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.
## [0.7.2] - 04-04-2022
`@bigcapital/webapp`
### Fixed
- Add the missing Arabic localization.
- Subscription plans modifications.
## [0.7.1] - 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
### 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.
@@ -23,9 +87,19 @@ All notable changes to Bigcapital server-side will be in this file.
- `BIG-352` Fix terms and notes fields on footer of all services.
- `BIG-354` Validate the warehouse transfer quantity should be above zero.
## [1.7.0-rc.1] - 24-03-2022
`@bigcapital/server`
## Added
### 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.
## [0.7.0] - 24-03-2022
`@bigcapital/webapp`
### Added
- Multiply currencies with foreign currencies.
- Multiply warehouses to track inventory items.
- Multiply branches to track organization transactions.
@@ -33,7 +107,7 @@ All notable changes to Bigcapital server-side will be in this file.
- Integrate financial reports with multiply branches.
- Integrate inventory reports with multiply warehouses.
## Changes
### Changes
- Optimize style of sale invoice form.
- Optimize style of sale receipt form.
- Optimize style of credit note form.
@@ -43,19 +117,36 @@ All notable changes to Bigcapital server-side will be in this file.
- Optimize style of manual journal form.
- Optimize style of expense form.
## [1.6.3] - 21-02-2022
### Fixed
- `BIG-337` Display billing page once the organization subscription is inactive.
## [1.6.2] - 19-02-2022
### Fixed
- fix syled components dependency with imported as default components.
## [1.6.0] - 18-02-2022
`@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.
## [0.6.3] - 21-02-2022
`@bigcapital/webapp`
### Fixed
- `BIG-337` Display billing page once the organization subscription is inactive.
## [0.6.2] - 19-02-2022
### Fixed
- fix syled components dependency with imported as default components.
## [0.6.0] - 18-02-2022
`@bigcapital/webapp`
### Added
- Balance sheet comparison of previous period (PP).
- Balance sheet comparison of previous year (PY).
- Balance sheet percentage analysis columns and rows basis.
@@ -63,28 +154,45 @@ All notable changes to Bigcapital server-side will be in this file.
- Profit & loss sheet comparison of previous year (PY).
- Profit & loss sheet percentage analysis columns, rows, income and expenses basis.
## [1.5.8] - 13-01-2022
## [0.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.
## [1.5.3] - 03-01-2020
`@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.
## [0.5.3] - 03-01-2020
`@bigcapital/webapp`
### Fixed
- Localize the global errors.
- Expand account name column on trial balance sheet.
## [1.5.0] - 20-12-2021
## [0.5.0] - 20-12-2021
`@bigcapital/webapp`
### Added
@@ -104,7 +212,9 @@ All notable changes to Bigcapital server-side will be in this file.
- Dashboard meta boot and authenticated user request query.
- Optimize Arabic localization.
## [1.4.0] - 11-09-2021
## [0.4.0] - 11-09-2021
`@bigcapital/webapp`
### Added
@@ -123,7 +233,9 @@ fix: BIG-144 - Typo adjustment dialog success message.
fix: BIG-148 - Items entries ordered by index.
fix: BIG-132 AR/AP aging summary report filter by none transactions/zero contacts.
## [1.2.0-RC] - 03-09-2021
## [0.2.0] - 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,20 +0,0 @@
FROM node:14.15.0 as build
USER root
WORKDIR /app
COPY ./package.json /app/package.json
COPY ./package-lock.json /app/package-lock.json
RUN npm install
COPY . .
RUN npm run build
FROM nginx
COPY ./nginx/sites/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/build /usr/share/nginx/html

339
LICENSE Normal file
View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

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.com/invite/c8nPBJafeb) - Ask for help.
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
- [Source Code](https://github.com/bigcapitalhq/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/bigcapitalhq/bigcapital/releases) for more information what has changed recently.

1
commitlint.config.js Normal file
View File

@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-lerna-scopes'] };

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

34
docker-compose.yml Normal file
View File

@@ -0,0 +1,34 @@
version: '3.3'
services:
mysql:
build:
context: ./docker/mysql
args:
- MYSQL_DATABASE=bigcapital_system
- MYSQL_USER=default_user
- MYSQL_PASSWORD=secret
- MYSQL_ROOT_PASSWORD=root
volumes:
- ./data/mysql/:/var/lib/mysql
expose:
- '3306'
ports:
- '3306:3306'
mongo:
build: ./docker/mongo
expose:
- '27017'
volumes:
- ./data/mongo/:/var/lib/mongodb
ports:
- '27017:27017'
redis:
build:
context: ./docker/redis
expose:
- "6379"
volumes:
- ./data/redis:/data

1
docker/mongo/Dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM mongo:5.0

18
docker/mysql/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM mysql:5.7
ADD my.cnf /etc/mysql/conf.d/my.cnf
RUN chown -R mysql:root /var/lib/mysql/
ARG MYSQL_DATABASE=default_database
ARG MYSQL_USER=default_user
ARG MYSQL_PASSWORD=secret
ARG MYSQL_ROOT_PASSWORD=root
ENV MYSQL_DATABASE=$MYSQL_DATABASE
ENV MYSQL_USER=$MYSQL_USER
ENV MYSQL_PASSWORD=$MYSQL_PASSWORD
ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
CMD ["mysqld"]
EXPOSE 3306

2
docker/mysql/my.cnf Normal file
View File

@@ -0,0 +1,2 @@
[mysqld]
bind-address = 0.0.0.0

5
docker/redis/Dockerfile Normal file
View File

@@ -0,0 +1,5 @@
FROM redis:4.0
COPY redis.conf /usr/local/etc/redis/redis.conf
CMD [ "redis-server", "/usr/local/etc/redis/redis.conf" ]

48
docker/redis/redis.conf Normal file
View File

@@ -0,0 +1,48 @@
daemonize no
pidfile /var/run/redis.pid
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 0
loglevel notice
logfile ""
databases 16
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
appendonly no
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-entries 512
list-max-ziplist-value 64
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes

6
lerna.json Normal file
View File

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

21870
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,220 +1,34 @@
{
"name": "bigcapital-client",
"version": "1.7.1",
"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 build --scope \"@bigcapital/webapp\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\"",
"build:server": "lerna run build --scope \"@bigcapital/server\"",
"prepare": "husky install"
},
"workspaces": [
"packages/*",
"shared/*"
],
"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"
"@commitlint/config-conventional": "^17.4.2",
"@commitlint/config-lerna-scopes": "^17.4.2",
"husky": "^8.0.3",
"lerna": "^6.4.1",
"@commitlint/cli": "^17.4.2"
},
"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"
]
"engines": {
"node": "14.x"
},
"babel": {
"presets": [
"react-app"
]
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"proxy": "http://localhost:3000/"
"dependencies": {}
}

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 @@
# @bigcapital/server

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,146 @@
{
"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:app": "cross-env NODE_ENV=production webpack --config scripts/webpack.config.js",
"build:commands": "cross-env NODE_ENV=production webpack --config scripts/webpack.cli.js",
"build": "npm-run-all build:*",
"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,11 @@
const { getCommonWebpackOptions } = require('./webpack.common');
const inputEntry = './src/commands/index.ts';
const outputDir = '../build';
const outputFilename = 'commands.js';
module.exports = getCommonWebpackOptions({
inputEntry,
outputDir,
outputFilename,
});

View File

@@ -0,0 +1,76 @@
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';
exports.getCommonWebpackOptions = ({
inputEntry,
outputDir,
outputFilename,
}) => {
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 })
);
}
return webpackOptions;
};

View File

@@ -0,0 +1,11 @@
const { getCommonWebpackOptions } = require('./webpack.common');
const inputEntry = './src/server.ts';
const outputDir = '../build';
const outputFilename = 'index.js';
module.exports = getCommonWebpackOptions({
inputEntry,
outputDir,
outputFilename,
});

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,517 @@
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';
import { MAX_ACCOUNTS_CHART_DEPTH } from 'services/Accounts/constants';
@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 },
],
}
);
}
if (error.errorType === 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL') {
return res.boom.badRequest(
'The parent account exceeded the depth level of accounts chart.',
{
errors: [
{
type: 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
code: 1500,
data: {
maxDepth: MAX_ACCOUNTS_CHART_DEPTH,
},
},
],
}
);
}
}
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(),
];
}
}

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