Compare commits

..

101 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
8c0ef61038 hotfix: add the pnpm-workspace.yaml file to Dockerfile 2024-05-06 22:28:13 +02:00
Ahmed Bouhuolia
76bb82f2b4 chore: update the pnpm-lock file 2024-05-06 22:25:45 +02:00
Ahmed Bouhuolia
4c0dc276dd hotfix: rollback the pnpm-workspace.yaml file 2024-05-06 22:04:15 +02:00
Ahmed Bouhuolia
a69c4b4067 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-05-06 21:13:06 +02:00
Ahmed Bouhuolia
d81e544e82 chore: update pnpm-lock.yaml version 2024-05-06 21:11:39 +02:00
Ahmed Bouhuolia
6eeda23559 Update README.md 2024-05-06 20:04:21 +02:00
Ahmed Bouhuolia
cd046cbe27 hotbug: remove the pnpm-workspace.yaml from Dockerfile 2024-05-06 17:55:26 +02:00
Ahmed Bouhuolia
9b7bc1e5b9 Merge branch 'main' into develop 2024-05-06 17:51:24 +02:00
Ahmed Bouhuolia
987341ed29 Merge branch 'main' into develop 2024-05-06 17:50:02 +02:00
Ahmed Bouhuolia
d7cad17f1b Merge pull request #426 from bigcapitalhq/big-163-user-email-verification-after-signing-up
feat: User email verification after signing-up.
2024-05-06 17:46:26 +02:00
Ahmed Bouhuolia
dd02ae471e feat: redirect to the setup page if user is verified 2024-05-06 17:45:32 +02:00
Ahmed Bouhuolia
a5bfb0b02b fix: the email confirmation link on mail message 2024-05-06 17:36:36 +02:00
Ahmed Bouhuolia
f4440c9a03 feat: ability to enable/disable email confirmation from env variables 2024-05-03 18:30:19 +02:00
Ahmed Bouhuolia
cb88c234d1 feat: sync the isVerified state of authed user 2024-05-03 16:00:31 +02:00
Ahmed Bouhuolia
f6a0476fb4 feat: bump CHANGELOG 2024-05-03 11:28:11 +02:00
Ahmed Bouhuolia
63cdc45295 Merge pull request #430 from bigcapitalhq/export-resources
feat: Export resource data to csv, xlsx
2024-05-02 16:20:20 +02:00
Ahmed Bouhuolia
83a5010dc5 feat: flatten the nested columns of exported data 2024-05-02 15:38:57 +02:00
Ahmed Bouhuolia
55aab76c9b feat: configure columns of resources export 2024-05-01 20:15:35 +02:00
Ahmed Bouhuolia
495941f43a feat: style the export dialog form 2024-05-01 19:09:53 +02:00
Ahmed Bouhuolia
00a1e070c6 feat: export dialog on resource table 2024-05-01 16:26:10 +02:00
Ahmed Bouhuolia
fab71d2b65 feat: wip configure resources to be exportable 2024-05-01 12:45:24 +02:00
Ahmed Bouhuolia
9504bb5ccd fix: Running migration Docker container on Windows (#432) 2024-05-01 00:52:23 +02:00
Ahmed Bouhuolia
7e89966f20 feat: wip export resource data 2024-05-01 00:20:13 +02:00
Ahmed Bouhuolia
8a96c41258 feat: export resource data to csv, xlsx 2024-04-29 23:45:11 +02:00
Vederis Leunardus
4a713980bf feat: Pushing docker containers by version tag (#421)
* feat: build docker image based on tag version

* refactor: tabbing

* refactor: revert conflict
2024-04-29 11:39:47 +02:00
Ahmed Bouhuolia
2a1cbf6ced Merge branch 'main' into develop 2024-04-28 18:18:31 +02:00
Ahmed Bouhuolia
9103b60653 feat: New Relic tracking (#429) 2024-04-28 18:12:59 +02:00
Ahmed Bouhuolia
b9fc0cdd9e feat: wip email confirmation 2024-04-28 17:51:11 +02:00
Ahmed Bouhuolia
4368c18479 feat: User email verification after signing-up. 2024-04-26 12:21:40 +02:00
Ahmed Bouhuolia
7e5c6b6487 hotfix: parse the mail secure env variable (#424) 2024-04-24 21:09:56 +02:00
Ahmed Bouhuolia
b7214044bb Merge branch 'main' into develop 2024-04-24 20:05:49 +02:00
Ahmed Bouhuolia
93cb3615c3 Merge branch 'main' into develop 2024-04-24 20:05:33 +02:00
Ahmed Bouhuolia
7abfa6a162 feat: ability to enable/disable the bank connect feature (#423) 2024-04-24 20:01:04 +02:00
Ahmed Bouhuolia
1372a1f0a8 hotfix: fix the subscription plan when subscribe on cloud (#422) 2024-04-24 15:30:36 +02:00
allcontributors[bot]
484024ec28 docs: add cloudsbird as a contributor for code (#418)
* docs: update README.md [skip ci]

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

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-04-23 19:37:47 +02:00
Vederis Leunardus
46639c7b86 feat: Update Docker Build-Push Action and Add ARM64 Support (#412)
* feat: build for arm64 platform

* fix: typo and add buildx and qemu

* feat: update the docker login-action version

* feat: update the docker login-action version

* feat: add the digest
2024-04-23 19:33:27 +02:00
allcontributors[bot]
d10d1654c1 docs: add benpsnyder as a contributor for code (#417)
* docs: update README.md [skip ci]

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

---------

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2024-04-22 10:21:09 +02:00
Ahmed Bouhuolia
2f06070ecb Merge pull request #414 from benpsnyder/feat/upgrade-to-latest-lerna
feat(repo): upgrade to latest lerna v8 and pnpm v9
2024-04-22 10:16:50 +02:00
Ahmed Bouhuolia
deefdb9bfd fix: update pnpm-lock.yaml file 2024-04-22 10:04:18 +02:00
Ben Snyder
3cc62d80de fix(repo): replace usage of yarn with pnpm/pnpx 2024-04-21 20:34:35 -04:00
Ben Snyder
4962c5d4d3 feat(repo): upgrade to latest lerna v8 and pnpm v9 2024-04-21 20:29:38 -04:00
Ahmed Bouhuolia
571a332658 Merge pull request #410 from bigcapitalhq/seed-free-subscription-to-tenants
feat: seed free subscription to tenants that have no subscription.
2024-04-19 09:36:35 +02:00
Ahmed Bouhuolia
b44c318a5d feat: seed free subscription to tenants that have no subscription. 2024-04-19 09:34:47 +02:00
Ahmed Bouhuolia
bd9717f4dc chore: Add Bigcapital Cloud link on README.md file 2024-04-17 19:51:05 +02:00
Ahmed Bouhuolia
f48aea8e5a feat: add the new env variables to docker compose 2024-04-17 19:21:35 +02:00
Ahmed Bouhuolia
0ac3a5dea9 fix: add /imports directory to storage dir 2024-04-17 18:25:17 +02:00
Ahmed Bouhuolia
56b40ad4cb fix: TS and linit errors 2024-04-17 17:51:21 +02:00
Ahmed Bouhuolia
9b6f934990 fix: add @lemonsqueezy/lemonsqueezy package dependencies. 2024-04-17 17:44:35 +02:00
Ahmed Bouhuolia
80e3522f8a Merge pull request #408 from bigcapitalhq/fix-import-store-absolute-path
fix: absolute storage imports path.
2024-04-17 17:37:39 +02:00
Ahmed Bouhuolia
7975643765 fix: absolute storage imports path. 2024-04-17 17:36:35 +02:00
Ahmed Bouhuolia
2ac7f86bdb Merge pull request #407 from bigcapitalhq/auto-subscribe-free
chore: add default value to env variable
2024-04-16 21:24:49 +02:00
Ahmed Bouhuolia
956b9b6812 chore: add default value to env variable 2024-04-16 21:22:40 +02:00
Ahmed Bouhuolia
60248ec3f6 Merge pull request #406 from bigcapitalhq/auto-subscribe-free
feat: auto subscribe to free plan once signup on community version.
2024-04-16 20:58:29 +02:00
Ahmed Bouhuolia
9d3f1541eb feat: auto subscribe to free plan once signup on community version. 2024-04-16 20:57:05 +02:00
Ahmed Bouhuolia
9b5f1a36ab Merge branch 'main' into develop 2024-04-16 12:58:22 +02:00
Ahmed Bouhuolia
8ee691e1ed Merge pull request #405 from bigcapitalhq/subscription-page-content
feat: subscription page content
2024-04-16 12:55:57 +02:00
Ahmed Bouhuolia
f9cb14da9e feat: subscription page content 2024-04-16 12:54:36 +02:00
Ahmed Bouhuolia
5e87581f4e hotfix: creating a vendor 2024-04-15 22:48:54 +02:00
Ahmed Bouhuolia
8ca9cf39da Merge pull request #404 from bigcapitalhq/optimize-ui-onboarding
feat: optimize the onboarding subscription experience.
2024-04-15 14:53:52 +02:00
Ahmed Bouhuolia
9001fea524 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2024-04-15 14:51:19 +02:00
Ahmed Bouhuolia
dea0d71732 Merge pull request #402 from bigcapitalhq/lemon-squeezy-payment
feat: Integrate Lemon Squeezy payment
2024-04-15 14:49:39 +02:00
Ahmed Bouhuolia
c191c4bd26 feat: remove other payment methods 2024-04-15 14:49:27 +02:00
Ahmed Bouhuolia
47d82ce591 feat: optimize the onboarding subscription experience. 2024-04-15 12:48:16 +02:00
Ahmed Bouhuolia
9321db2a3a feat: sweep up lemon squeezy webhooks. 2024-04-14 12:58:53 +02:00
Ahmed Bouhuolia
e486333c96 feat: sweep up the Lemon Squeezy integration 2024-04-14 12:44:02 +02:00
Ahmed Bouhuolia
a9748b23c0 feat: listen LemonSqueezy webhooks 2024-04-14 11:55:36 +02:00
Ahmed Bouhuolia
693ae61141 feat: integrate LemonSqueezy to subscription payment 2024-04-14 10:33:29 +02:00
Ahmed Bouhuolia
9807ac04b0 Revert "feat(webapp): deprecate the subscription step in onboarding process"
This reverts commit 0c1bf302e5.
2024-04-13 15:18:59 +02:00
Ahmed Bouhuolia
bddfde4138 Revert "feat(webapp): deprecate the subscription step in onboarding process"
This reverts commit 0c1bf302e5.
2024-04-13 14:07:32 +02:00
Ahmed Bouhuolia
a39dcd00d5 Revert "feat(server): deprecated the subscription module."
This reverts commit 3b79ac66ae.
2024-04-13 11:05:53 +02:00
Ahmed Bouhuolia
4d616e9287 Revert "feat(server): deprecated the subscription module."
This reverts commit 44fc26b156.
2024-04-13 10:17:48 +02:00
Ahmed Bouhuolia
dc52fb1de5 fix: lint error 2024-04-09 22:24:03 +02:00
Ahmed Bouhuolia
21a1777424 Merge branch 'release/v0.15.2' into develop 2024-04-09 22:11:10 +02:00
Ahmed Bouhuolia
16b721db91 Merge pull request #401 from bigcapitalhq/import-fields-hints
feat: add hints to import fields
2024-04-09 22:01:16 +02:00
Ahmed Bouhuolia
079491823d feat: add hints to import fields 2024-04-09 22:00:04 +02:00
Ahmed Bouhuolia
f7a87a6e9c Merge pull request #400 from bigcapitalhq/clean-up-templ-import-files
feat: clean up the imported temp files
2024-04-09 00:16:36 +02:00
Ahmed Bouhuolia
e0cdf42980 chore: update the server .gitignore 2024-04-09 00:13:58 +02:00
Ahmed Bouhuolia
ee56653f4b fix: remove the import table from tenants dbs 2024-04-09 00:13:32 +02:00
Ahmed Bouhuolia
2310b09778 fix: delete imported file if error has thrown 2024-04-09 00:11:34 +02:00
Ahmed Bouhuolia
0684e50ebd feat: clean up the imported temp files 2024-04-09 00:09:32 +02:00
Ahmed Bouhuolia
aaa8f39e50 feat: add section label to import mapping 2024-04-08 00:39:18 +02:00
Ahmed Bouhuolia
af981ce630 Merge pull request #396 from bigcapitalhq/aggregate-rows-import
feat: Aggregate rows import
2024-04-07 23:55:06 +02:00
Ahmed Bouhuolia
a1f8417b5d feat: revert the resource columns 2024-04-07 23:48:23 +02:00
Ahmed Bouhuolia
086b060351 feat: add sample import sheet to invoices 2024-04-07 15:26:23 +02:00
Ahmed Bouhuolia
bbafdcd8bd feat: configuring import services on more resources 2024-04-06 04:06:15 +02:00
Ahmed Bouhuolia
dd9098bdc1 feat: more resources support importing 2024-04-05 02:00:12 +02:00
Ahmed Bouhuolia
3851d34ba4 feat: aggregate rows on import feature 2024-04-04 05:01:09 +02:00
Ahmed Bouhuolia
b9651f30d5 chore: remove console.log 2024-04-01 04:40:40 +02:00
Ahmed Bouhuolia
45b5fb4088 fix: syntax error 2024-04-01 03:06:01 +02:00
Ahmed Bouhuolia
aa64bcf69b Merge pull request #393 from bigcapitalhq/import-relations-mapping
feat: linking relation with id in importing
2024-04-01 03:02:04 +02:00
Ahmed Bouhuolia
cbd867b334 Merge branch 'develop' into import-relations-mapping 2024-04-01 03:01:24 +02:00
Ahmed Bouhuolia
1a8ca83786 Merge pull request #395 from bigcapitalhq/validate-imported-sheet-empty
feat: validate the given imported sheet whether is empty
2024-04-01 02:58:41 +02:00
Ahmed Bouhuolia
80c14ba1a0 Merge branch 'develop' into validate-imported-sheet-empty 2024-04-01 02:58:33 +02:00
Ahmed Bouhuolia
785045dbad feat: validate the given imported sheet whether is empty 2024-04-01 02:57:30 +02:00
Ahmed Bouhuolia
291301c1e3 Merge pull request #394 from bigcapitalhq/advanced-import-parser
feat: advanced parser for numeric and boolean import values
2024-04-01 02:27:34 +02:00
Ahmed Bouhuolia
824e4e13d1 feat: advanced parser for numeric and boolean import values 2024-04-01 02:26:08 +02:00
Ahmed Bouhuolia
74da28b464 feat: linking relation with id in importing 2024-04-01 01:13:31 +02:00
Ahmed Bouhuolia
22a016b56e Merge pull request #392 from bigcapitalhq/import-show-unique-value-preview
fix: show the unique row value in the import preview
2024-03-28 05:39:40 +02:00
Ahmed Bouhuolia
040f016273 fix: show the unique row value in the import preview 2024-03-28 05:38:24 +02:00
Ahmed Bouhuolia
8ab809fc71 Merge pull request #389 from bigcapitalhq/accounts-bank-transactions-demo-sheet
feat: add sample sheet to accounts and bank transactions
2024-03-28 00:57:58 +02:00
Ahmed Bouhuolia
2baa667c5d fix(webapp): hotfix pdf request hook 2024-03-19 05:22:15 +02:00
325 changed files with 11970 additions and 43394 deletions

View File

@@ -105,6 +105,24 @@
"contributions": [
"bug"
]
},
{
"login": "benpsnyder",
"name": "Ben Snyder",
"avatar_url": "https://avatars.githubusercontent.com/u/707567?v=4",
"profile": "https://snyder.tech",
"contributions": [
"code"
]
},
{
"login": "cloudsbird",
"name": "Vederis Leunardus",
"avatar_url": "https://avatars.githubusercontent.com/u/13505006?v=4",
"profile": "http://vederis.id",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -48,6 +48,9 @@ SIGNUP_DISABLED=false
SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS=
# Sign-up Email Confirmation
SIGNUP_EMAIL_CONFIRMATION=false
# API rate limit (points,duration,block duration).
API_RATE_LIMIT=120,60,600
@@ -95,3 +98,8 @@ PLAID_LINK_WEBHOOK=
PLAID_SANDBOX_REDIRECT_URI=
PLAID_DEVELOPMENT_REDIRECT_URI=
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET=

View File

@@ -12,20 +12,37 @@ env:
jobs:
build-publish-webapp:
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
name: Build and deploy webapp container
runs-on: ubuntu-latest
environment: production
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Container registry.
- name: Log in to the Container registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
@@ -35,14 +52,29 @@ jobs:
# Builds and push the Docker image.
- name: Build and push Docker image
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
id: build
with:
context: .
file: ./packages/webapp/Dockerfile
push: true
tags: ghcr.io/bigcapitalhq/webapp:latest
labels: ${{ steps.meta.outputs.labels }}
context: ./
file: ./packages/webapp/Dockerfile
platforms: ${{ matrix.platform }}
push: true
tags: ghcr.io/bigcapitalhq/webapp:latest, ghcr.io/bigcapitalhq/webapp:${{github.ref_name}}
labels: ${{ steps.meta.outputs.labels }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-main-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Send notification to Slack channel.
- name: Slack Notification built and published webapp container successfully.
uses: rtCamp/action-slack-notify@v2
@@ -53,12 +85,23 @@ jobs:
name: Build and deploy server container
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Container registry.
- name: Log in to the Container registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -66,14 +109,30 @@ jobs:
# Builds and push the Docker image.
- name: Build and push Docker image
uses: docker/build-push-action@v2
uses: docker/build-push-action@v5
id: build
with:
context: ./
file: ./packages/server/Dockerfile
platforms: ${{ matrix.platform }}
push: true
tags: ghcr.io/bigcapitalhq/server:latest
tags: ghcr.io/bigcapitalhq/server:latest, ghcr.io/bigcapitalhq/server:${{github.ref_name}}
labels: ${{ steps.meta.outputs.labels }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-main-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Send notification to Slack channel.
- name: Slack Notification built and published server container successfully.
uses: rtCamp/action-slack-notify@v2

View File

@@ -8,14 +8,14 @@ on:
- '**.ts'
- '**.tsx'
- '**/tsconfig.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
- '.github/workflows/e2e.yml'
pull_request:
paths:
- '**.ts'
- '**.tsx'
- '**/tsconfig.json'
- 'yarn.lock'
- 'pnpm-lock.yaml'
- '.github/workflows/e2e.yml'
defaults:

View File

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

View File

@@ -2,6 +2,64 @@
All notable changes to Bigcapital server-side will be in this file.
## [0.16.10]
* fix: Running migration Docker container on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/432
## [0.16.9]
* feat: New Relic for tracking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/429
## [0.16.8]
* feat: Ability to enable/disable the bank connect feature by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/423
## [0.16.6]
* hotfix: fix the subscription plan when subscribe on cloud by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/422
## [0.16.5]
IMPORTANT: If you upgraded to the v0.16 recently you should upgrade to v0.16.4 as soon as possible, because there're some breaking changes affected the sign-in and some users reported couldn't sign-in.
* feat: Seed free subscription to tenants that have no subscription. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/410
## [0.16.3]
* feat: Integrate Lemon Squeezy payment by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/402
* feat: optimize the onboarding subscription experience. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/404
* feat: subscription page content by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/405
* feat: auto subscribe to free plan once signup on community version. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/406
* chore: add default value to env variable by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/407
* fix: absolute storage imports path. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/408
## [0.16.0]
* feat: add convert to invoice button on estimate drawer toolbar by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/361
* feat(webapp): add mark as delivered to action bar of invoice details … by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/360
* feat(webapp): Dialog to choose the bank service provider by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/378
* feat: Categorize the bank synced transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/377
* feat: uncategorize the cashflow transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/381
* Import resources from csv/xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/382
* feat(webapp): import resource UI by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/386
* fix: import resources improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/388
* feat: add sample sheet to accounts and bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/389
* fix: show the unique row value in the import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/392
* feat: advanced parser for numeric and boolean import values by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/394
* feat: validate the given imported sheet whether is empty by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/395
* feat: linking relation with id in importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/393
* feat: Aggregate rows import by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/396
* feat: clean up the imported temp files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/400
* feat: add hints to import fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/401
## [0.15.0]
* feat: Printing financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/363
* feat: Convert invoice status after sending mail notification by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/332
* feat: Bigcapital <> Plaid Integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/346
* fix: Broken transactions by vendor report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/369
* fix: Optimize the print style some financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/370
## [0.14.0] - 30-01-2024
* feat: purchases by items exporting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/327

View File

@@ -1,6 +1,6 @@
<p align="center">
<p align="center">
<a href="https://bigcapital.ly" target="_blank">
<a href="https://bigcapital.app" target="_blank">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/bigcapital.svg" alt="Bigcapital" width="280" height="75">
</a>
</p>
@@ -25,6 +25,10 @@
<img src="https://img.shields.io/twitter/follow/bigcapitalhq?style=social" alt="twitter" />
</a>
</p>
<p align="center">
<a href="https://my.bigcapital.app">Bigcapital Cloud</a>
</p>
</p>
# What's Bigcapital?
@@ -47,7 +51,7 @@ Bigcapital is available open-source under AGPL license. You can host it on your
### Docker
To get started with self-hosted with Docker and Docker Compose, take a look at the [Docker guide](https://docs.bigcapital.ly/deployment/docker).
To get started with self-hosted with Docker and Docker Compose, take a look at the [Docker guide](https://docs.bigcapital.app/deployment/docker).
## Development
@@ -70,7 +74,7 @@ You can integrate Bigcapital API with your system to organize your transactions
# Resources
- [Documentation](https://docs.bigcapital.ly/) - Learn how to use.
- [Documentation](https://docs.bigcapital.app/) - Learn how to use.
- [Contribution](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md) - Welcome to any contributions.
- [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help.
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
@@ -118,6 +122,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asenawritescode"><img src="https://avatars.githubusercontent.com/u/67445192?v=4?s=100" width="100px;" alt="Asena"/><br /><sub><b>Asena</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aasenawritescode" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://snyder.tech"><img src="https://avatars.githubusercontent.com/u/707567?v=4?s=100" width="100px;" alt="Ben Snyder"/><br /><sub><b>Ben Snyder</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=benpsnyder" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://vederis.id"><img src="https://avatars.githubusercontent.com/u/13505006?v=4?s=100" width="100px;" alt="Vederis Leunardus"/><br /><sub><b>Vederis Leunardus</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=cloudsbird" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@@ -21,16 +21,12 @@ services:
depends_on:
- server
- webapp
deploy:
restart_policy:
condition: unless-stopped
restart: on-failure
webapp:
container_name: bigcapital-webapp
image: ghcr.io/bigcapitalhq/webapp:latest
deploy:
restart_policy:
condition: unless-stopped
restart: on-failure
server:
container_name: bigcapital-server
@@ -45,9 +41,7 @@ services:
- mysql
- mongo
- redis
deploy:
restart_policy:
condition: unless-stopped
restart: on-failure
environment:
# Mail
- MAIL_HOST=${MAIL_HOST}
@@ -92,6 +86,30 @@ services:
- GOTENBERG_URL=${GOTENBERG_URL}
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
# Bank Sync
- BANKING_CONNECT=${BANKING_CONNECT}
# Plaid
- PLAID_ENV=${PLAID_ENV}
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
- PLAID_SECRET_DEVELOPMENT=${PLAID_SECRET_DEVELOPMENT}
- PLAID_SECRET_SANDBOX=${b8cf42b441e110451e2f69ad7e1e9f}
- PLAID_LINK_WEBHOOK=${PLAID_LINK_WEBHOOK}
# Lemon Squeez
- LEMONSQUEEZY_API_KEY=${LEMONSQUEEZY_API_KEY}
- LEMONSQUEEZY_STORE_ID=${LEMONSQUEEZY_STORE_ID}
- LEMONSQUEEZY_WEBHOOK_SECRET=${LEMONSQUEEZY_WEBHOOK_SECRET}
- HOSTED_ON_BIGCAPITAL_CLOUD=${HOSTED_ON_BIGCAPITAL_CLOUD}
# New Relic matrics tracking.
- NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=${NEW_RELIC_DISTRIBUTED_TRACING_ENABLED}
- NEW_RELIC_LOG=${NEW_RELIC_LOG}
- NEW_RELIC_AI_MONITORING_ENABLED=${NEW_RELIC_AI_MONITORING_ENABLED}
- NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_CUSTOM_INSIGHTS_EVENTS_MAX_SAMPLES_STORED}
- NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED=${NEW_RELIC_SPAN_EVENTS_MAX_SAMPLES_STORED}
database_migration:
container_name: bigcapital-database-migration
build:
@@ -111,9 +129,7 @@ services:
mysql:
container_name: bigcapital-mysql
deploy:
restart_policy:
condition: unless-stopped
restart: on-failure
build:
context: ./docker/mariadb
environment:
@@ -128,9 +144,7 @@ services:
mongo:
container_name: bigcapital-mongo
deploy:
restart_policy:
condition: unless-stopped
restart: on-failure
build: ./docker/mongo
expose:
- '27017'
@@ -139,9 +153,7 @@ services:
redis:
container_name: bigcapital-redis
deploy:
restart_policy:
condition: unless-stopped
restart: on-failure
build:
context: ./docker/redis
expose:

View File

@@ -37,4 +37,5 @@ RUN git clone https://github.com/vishnubob/wait-for-it.git
ADD docker/migration/start.sh /
RUN chmod +x /start.sh
CMD ["/start.sh"]
# Once we listen the mysql port run the migration task.
CMD ./wait-for-it/wait-for-it.sh mysql:3306 -- sh -c "node ./build/commands.js system:migrate:latest && node ./build/commands.js tenants:migrate:latest"

View File

@@ -1,5 +0,0 @@
# Migrate the master system database.
./wait-for-it/wait-for-it.sh mysql:3306 -- node ./build/commands.js system:migrate:latest
# Migrate all tenants.
./wait-for-it/wait-for-it.sh mysql:3306 -- node ./build/commands.js tenants:migrate:latest

View File

@@ -2,6 +2,7 @@
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"npmClient": "pnpm",
"useWorkspaces": true,
"packages": ["packages/*"]
}
"packages": [
"packages/*"
]
}

View File

@@ -19,7 +19,8 @@
"@faker-js/faker": "^8.0.2",
"@playwright/test": "^1.32.3",
"husky": "^8.0.3",
"lerna": "^6.4.1"
"lerna": "^8.1.2",
"pnpm": "^9.0.5"
},
"engines": {
"node": "16.x || 17.x || 18.x"

View File

@@ -3,3 +3,6 @@
stdout.log
/dist
/build
/public/imports
dist

View File

@@ -78,6 +78,9 @@ ENV MAIL_HOST=$MAIL_HOST \
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
# New Relic config file.
ENV NEW_RELIC_NO_CONFIG_FILE=true
# Create app directory.
WORKDIR /app
@@ -89,8 +92,8 @@ RUN npm install -g pnpm
# Copy application dependency manifests to the container image.
COPY ./package*.json ./
COPY ./pnpm-lock.yaml ./pnpm-lock.yaml
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./lerna.json ./lerna.json
COPY ./pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY ./packages/server/package*.json ./packages/server/
# Install application dependencies

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@
"dependencies": {
"@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@types/i18n": "^0.8.7",
"@types/knex": "^0.16.1",
"@types/mathjs": "^6.0.12",
@@ -81,6 +82,7 @@
"mustache": "^3.0.3",
"mysql": "^2.17.1",
"mysql2": "^1.6.5",
"newrelic": "^11.15.0",
"node-cache": "^4.2.1",
"nodemailer": "^6.3.0",
"nodemon": "^1.19.1",
@@ -89,17 +91,17 @@
"objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7",
"objection-unique": "^1.2.2",
"plaid": "^10.3.0",
"pluralize": "^8.0.0",
"pug": "^3.0.2",
"puppeteer": "^10.2.0",
"plaid": "^10.3.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",
"source-map-loader": "^4.0.1",
"socket.io": "^4.7.4",
"source-map-loader": "^4.0.1",
"tmp-promise": "^3.0.3",
"ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0",

BIN
packages/server/public/.DS_Store vendored Normal file

Binary file not shown.

BIN
packages/server/public/imports/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -244,26 +244,29 @@
"account.field.active": "Active",
"account.field.currency": "Currency",
"account.field.balance": "Balance",
"account.field.bank_balance": "Bank Balance",
"account.field.parent_account": "Parent Account",
"account.field.created_at": "Created at",
"item.field.type": "Item type",
"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.type.non-inventory": "Non Inventory",
"item.field.name": "Item Name",
"item.field.code": "Item 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.cost_price": "Cost Price",
"item.field.sell_price": "Sell 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.field.created_at": "Created At",
"item_category.field.name": "Name",
"item_category.field.description": "Description",
"item_category.field.count": "Count",
@@ -276,8 +279,14 @@
"invoice.field.invoice_message": "Invoice message",
"invoice.field.terms_conditions": "Terms & conditions",
"invoice.field.amount": "Amount",
"invoice.field.exchange_rate": "Exchange Rate",
"invoice.field.payment_amount": "Payment amount",
"invoice.field.due_amount": "Due amount",
"invoice.field.delivered": "Delivered",
"invoice.field.item_name": "Item Name",
"invoice.field.rate": "Rate",
"invoice.field.quantity": "Quantity",
"invoice.field.description": "Description",
"invoice.field.status": "Status",
"invoice.field.status.paid": "Paid",
"invoice.field.status.partially-paid": "Partially paid",
@@ -286,6 +295,8 @@
"invoice.field.status.delivered": "Delivered",
"invoice.field.status.draft": "Draft",
"invoice.field.created_at": "Created at",
"invoice.field.currency": "Currency",
"invoice.field.entries": "Entries",
"estimate.field.amount": "Amount",
"estimate.field.estimate_number": "Estimate number",
"estimate.field.customer": "Customer",
@@ -300,22 +311,31 @@
"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",
"payment_receive.field.customer": "Customer",
"payment_receive.field.exchange_rate": "Exchange Rate",
"payment_receive.field.payment_date": "Payment Date",
"payment_receive.field.reference_no": "Reference No.",
"payment_receive.field.deposit_account": "Deposit Account",
"payment_receive.field.entries": "Entries",
"payment_receive.field.invoice": "Invoice",
"payment_receive.field.entries.payment_amount": "Payment Amount",
"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.due_amount": "Due Amount",
"bill_payment.field.payment_account": "Payment Account",
"bill_payment.field.payment_number": "Payment No.",
"bill_payment.field.payment_date": "Payment Date",
"bill_payment.field.reference_no": "Reference No.",
"bill_payment.field.description": "Description",
"bill_payment.field.exchange_rate": "Exchange Rate",
"bill_payment.field.note": "Note",
"bill_payment.field.entries.bill": "Bill No.",
"bill_payment.field.entries.payment_amount": "Payment Amount",
"bill_payment.field.reference": "Reference No.",
"bill_payment.field.created_at": "Created at",
"bill.field.vendor": "Vendor",
"bill.field.bill_number": "Bill number",
@@ -343,22 +363,30 @@
"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.payment_date": "Payment Date",
"expense.field.payment_account": "Payment Account",
"expense.field.amount": "Amount",
"expense.field.currency_code": "Currency",
"expense.field.exchange_rate": "Exchange Rate",
"expense.field.reference_no": "Reference No.",
"expense.field.description": "Description",
"expense.field.line_description": "Line Description",
"expense.field.published": "Published",
"expense.field.categories": "Categories",
"expense.field.expense_account": "Expense Account",
"expense.field.publish": "Publish",
"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.journal_number": "Journal No.",
"manual_journal.field.reference": "Reference No.",
"manual_journal.field.journal_type": "Journal type",
"manual_journal.field.journal_type": "Journal Type",
"manual_journal.field.amount": "Amount",
"manual_journal.field.description": "Description",
"manual_journal.field.currency": "Currency",
"manual_journal.field.exchange_rate": "Exchange Rate",
"manual_journal.field.status": "Status",
"manual_journal.field.created_at": "Created at",
"receipt.field.amount": "Amount",
@@ -404,6 +432,7 @@
"vendor.field.created_at": "Created at",
"vendor.field.balance": "Balance",
"vendor.field.status": "Status",
"vendor.field.note": "Note",
"vendor.field.currency": "Currency",
"vendor.field.status.active": "Active",
"vendor.field.status.inactive": "Inactive",
@@ -411,6 +440,8 @@
"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",

View File

@@ -349,7 +349,7 @@ export default class AccountsController extends BaseController {
// Filter query.
const filter = {
sortOrder: 'desc',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req),

View File

@@ -9,6 +9,8 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
import AuthenticationApplication from '@/services/Authentication/AuthApplication';
import JWTAuth from '@/api/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
@Service()
export default class AuthenticationController extends BaseController {
@Inject()
@@ -28,6 +30,20 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.login.bind(this)),
this.handlerErrors
);
router.use('/register/verify/resend', JWTAuth);
router.use('/register/verify/resend', AttachCurrentTenantUser);
router.post(
'/register/verify/resend',
asyncMiddleware(this.registerVerifyResendMail.bind(this)),
this.handlerErrors
);
router.post(
'/register/verify',
this.signupVerifySchema,
this.validationResult,
asyncMiddleware(this.registerVerify.bind(this)),
this.handlerErrors
);
router.post(
'/register',
this.registerSchema,
@@ -99,6 +115,17 @@ export default class AuthenticationController extends BaseController {
];
}
private get signupVerifySchema(): ValidationChain[] {
return [
check('email')
.exists()
.isString()
.isEmail()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('token').exists().isString(),
];
}
/**
* Reset password schema.
* @returns {ValidationChain[]}
@@ -166,6 +193,58 @@ export default class AuthenticationController extends BaseController {
}
}
/**
* Verifies the provider user's email after signin-up.
* @param {Request} req
* @param {Response}| res
* @param {Function} next
* @returns {Response|void}
*/
private async registerVerify(req: Request, res: Response, next: Function) {
const signUpVerifyDTO: { email: string; token: string } =
this.matchedBodyData(req);
try {
const user = await this.authApplication.signUpConfirm(
signUpVerifyDTO.email,
signUpVerifyDTO.token
);
return res.status(200).send({
type: 'success',
message: 'The given user has verified successfully',
user,
});
} catch (error) {
next(error);
}
}
/**
* Resends the confirmation email to the user.
* @param {Request} req
* @param {Response}| res
* @param {Function} next
*/
private async registerVerifyResendMail(
req: Request,
res: Response,
next: Function
) {
const { user } = req;
try {
const data = await this.authApplication.signUpConfirmResend(user.id);
return res.status(200).send({
type: 'success',
message: 'The given user has verified successfully',
data,
});
} catch (error) {
next(error);
}
}
/**
* Send reset password handler
* @param {Request} req

View File

@@ -289,7 +289,7 @@ export default class CustomersController extends ContactsController {
const filter = {
inactiveMode: false,
sortOrder: 'desc',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),

View File

@@ -144,10 +144,8 @@ export default class VendorsController extends ContactsController {
try {
const vendor = await this.vendorsApplication.createVendor(
tenantId,
contactDTO,
user
contactDTO
);
return res.status(200).send({
id: vendor.id,
message: 'The vendor has been created successfully.',
@@ -272,7 +270,7 @@ export default class VendorsController extends ContactsController {
const vendorsFilter: IVendorsFilter = {
inactiveMode: false,
sortOrder: 'desc',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),

View File

@@ -8,10 +8,10 @@ export default class DashboardMetaController {
dashboardService: DashboardService;
/**
*
* Constructor router.
* @returns
*/
router() {
public router() {
const router = Router();
router.get('/boot', this.getDashboardBoot);
@@ -25,7 +25,7 @@ export default class DashboardMetaController {
* @param {Response} res -
* @param {NextFunction} next -
*/
getDashboardBoot = async (
private getDashboardBoot = async (
req: Request,
res: Response,
next: NextFunction

View File

@@ -0,0 +1,99 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { ServiceError } from '@/exceptions';
import { ExportApplication } from '@/services/Export/ExportApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service()
export class ExportController extends BaseController {
@Inject()
private exportResourceApp: ExportApplication;
/**
* Router constructor method.
*/
router() {
const router = Router();
router.get(
'/',
[
query('resource').exists(),
query('format').isIn(['csv', 'xlsx']).optional(),
],
this.validationResult,
this.export.bind(this),
this.catchServiceErrors
);
return router;
}
/**
* Imports xlsx/csv to the given resource type.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async export(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_PDF,
]);
const data = await this.exportResourceApp.export(
tenantId,
query.resource,
acceptType === ACCEPT_TYPE.APPLICATION_XLSX ? 'xlsx' : 'csv'
);
if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(data);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(data);
}
} 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) {
return res.status(400).send({
errors: [{ type: error.errorType }],
});
}
next(error);
}
}

View File

@@ -37,6 +37,7 @@ export class ImportController extends BaseController {
[
param('import_id').exists().isString(),
body('mapping').exists().isArray({ min: 1 }),
body('mapping.*.group').optional(),
body('mapping.*.from').exists(),
body('mapping.*.to').exists(),
],
@@ -47,6 +48,7 @@ export class ImportController extends BaseController {
router.get(
'/sample',
[query('resource').exists(), query('format').optional()],
this.validationResult,
this.downloadImportSample.bind(this),
this.catchServiceErrors
);

View File

@@ -1,21 +1,34 @@
import Multer from 'multer';
import { ServiceError } from '@/exceptions';
import { getImportsStoragePath } from '@/services/Import/_utils';
export function allowSheetExtensions(req, file, cb) {
if (
file.mimetype !== 'text/csv' &&
file.mimetype !== 'application/vnd.ms-excel' &&
file.mimetype !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
file.mimetype !== 'application/vnd.ms-excel' &&
file.mimetype !==
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
) {
cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID'));
return;
}
cb(null, true);
}
const storage = Multer.diskStorage({
destination: function (req, file, cb) {
const path = getImportsStoragePath();
cb(null, path);
},
filename: function (req, file, cb) {
// Add the creation timestamp to clean up temp files later.
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix);
},
});
export const uploadImportFile = Multer({
dest: './public/imports',
storage,
limits: { fileSize: 5 * 1024 * 1024 },
fileFilter: allowSheetExtensions,
});

View File

@@ -6,7 +6,7 @@ import ItemTransactionsController from './ItemsTransactions';
@Service()
export default class ItemsBaseController {
router() {
public router() {
const router = Router();
router.use('/', Container.get(ItemsController).router());

View File

@@ -6,6 +6,7 @@ import { check, ValidationChain } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import JWTAuth from '@/api/middleware/jwtAuth';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import OrganizationService from '@/services/Organization/OrganizationService';
import { MONTHS, ACCEPTED_LOCALES } from '@/services/Organization/constants';
@@ -17,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
@Service()
export default class OrganizationController extends BaseController {
@Inject()
private organizationService: OrganizationService;
organizationService: OrganizationService;
/**
* Router constructor.
@@ -25,10 +26,13 @@ export default class OrganizationController extends BaseController {
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.use('/build', SubscriptionMiddleware('main'));
router.post(
'/build',
this.buildOrganizationValidationSchema,

View File

@@ -297,8 +297,7 @@ export default class VendorCreditController extends BaseController {
try {
const vendorCredit = await this.createVendorCreditService.newVendorCredit(
tenantId,
vendorCreditCreateDTO,
user
vendorCreditCreateDTO
);
return res.status(200).send({

View File

@@ -338,8 +338,7 @@ export default class PaymentReceivesController extends BaseController {
try {
const creditNote = await this.createCreditNoteService.newCreditNote(
tenantId,
creditNoteDTO,
user
creditNoteDTO
);
return res.status(200).send({
id: creditNote.id,

View File

@@ -0,0 +1,88 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Service, Inject } from 'typedi';
import { body } from 'express-validator';
import JWTAuth from '@/api/middleware/jwtAuth';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import SubscriptionService from '@/services/Subscription/SubscriptionService';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '../BaseController';
import { LemonSqueezyService } from '@/services/Subscription/LemonSqueezyService';
@Service()
export class SubscriptionController extends BaseController {
@Inject()
private subscriptionService: SubscriptionService;
@Inject()
private lemonSqueezyService: LemonSqueezyService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.use(JWTAuth);
router.use(AttachCurrentTenantUser);
router.use(TenancyMiddleware);
router.post(
'/lemon/checkout_url',
[body('variantId').exists().trim()],
this.validationResult,
this.getCheckoutUrl.bind(this)
);
router.get('/', asyncMiddleware(this.getSubscriptions.bind(this)));
return router;
}
/**
* Retrieve all subscriptions of the authenticated user's tenant.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getSubscriptions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
try {
const subscriptions = await this.subscriptionService.getSubscriptions(
tenantId
);
return res.status(200).send({ subscriptions });
} catch (error) {
next(error);
}
}
/**
* Retrieves the LemonSqueezy checkout url.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getCheckoutUrl(
req: Request,
res: Response,
next: NextFunction
) {
const { variantId } = this.matchedBodyData(req);
const { user } = req;
try {
const checkout = await this.lemonSqueezyService.getCheckout(
variantId,
user
);
return res.status(200).send(checkout);
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1 @@
export * from './SubscriptionController';

View File

@@ -3,6 +3,7 @@ import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
import { Request, Response } from 'express';
import { Inject, Service } from 'typedi';
import BaseController from '../BaseController';
import { LemonSqueezyWebhooks } from '@/services/Subscription/LemonSqueezyWebhooks';
import { PlaidWebhookTenantBootMiddleware } from '@/services/Banking/Plaid/PlaidWebhookTenantBootMiddleware';
@Service()
@@ -10,18 +11,39 @@ export class Webhooks extends BaseController {
@Inject()
private plaidApp: PlaidApplication;
@Inject()
private lemonWebhooksService: LemonSqueezyWebhooks;
/**
* Router constructor.
*/
router() {
const router = Router();
router.use(PlaidWebhookTenantBootMiddleware);
router.use('/plaid', PlaidWebhookTenantBootMiddleware);
router.post('/plaid', this.plaidWebhooks.bind(this));
router.post('/lemon', this.lemonWebhooks.bind(this));
return router;
}
/**
* Listens to Lemon Squeezy webhooks events.
* @param {Request} req
* @param {Response} res
* @returns {Response}
*/
public async lemonWebhooks(req: Request, res: Response) {
const data = req.body;
const signature = req.headers['x-signature'] ?? '';
const rawBody = req.rawBody;
await this.lemonWebhooksService.handlePostWebhook(rawBody, data, signature);
return res.status(200).send();
}
/**
* Listens to Plaid webhooks.
* @param {Request} req

View File

@@ -4,6 +4,7 @@ import { Container } from 'typedi';
// Middlewares
import JWTAuth from '@/api/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
import EnsureTenantIsInitialized from '@/api/middleware/EnsureTenantIsInitialized';
import SettingsMiddleware from '@/api/middleware/SettingsMiddleware';
@@ -36,6 +37,7 @@ import Resources from './controllers/Resources';
import ExchangeRates from '@/api/controllers/ExchangeRates';
import Media from '@/api/controllers/Media';
import Ping from '@/api/controllers/Ping';
import { SubscriptionController } from '@/api/controllers/Subscription';
import InventoryAdjustments from '@/api/controllers/Inventory/InventoryAdjustments';
import asyncRenderMiddleware from './middleware/AsyncRenderMiddleware';
import Jobs from './controllers/Jobs';
@@ -59,6 +61,7 @@ import { TaxRatesController } from './controllers/TaxRates/TaxRates';
import { ImportController } from './controllers/Import/ImportController';
import { BankingController } from './controllers/Banking/BankingController';
import { Webhooks } from './controllers/Webhooks/Webhooks';
import { ExportController } from './controllers/Export/ExportController';
export default () => {
const app = Router();
@@ -70,6 +73,7 @@ export default () => {
app.use('/auth', Container.get(Authentication).router());
app.use('/invite', Container.get(InviteUsers).nonAuthRouter());
app.use('/subscription', Container.get(SubscriptionController).router());
app.use('/organization', Container.get(Organization).router());
app.use('/ping', Container.get(Ping).router());
app.use('/jobs', Container.get(Jobs).router());
@@ -83,6 +87,7 @@ export default () => {
dashboard.use(JWTAuth);
dashboard.use(AttachCurrentTenantUser);
dashboard.use(TenancyMiddleware);
dashboard.use(SubscriptionMiddleware('main'));
dashboard.use(EnsureTenantIsInitialized);
dashboard.use(SettingsMiddleware);
dashboard.use(I18nAuthenticatedMiddlware);
@@ -136,12 +141,11 @@ export default () => {
dashboard.use('/warehouses', Container.get(WarehousesController).router());
dashboard.use('/projects', Container.get(ProjectsController).router());
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
dashboard.use('/import', Container.get(ImportController).router());
dashboard.use('/export', Container.get(ExportController).router())
dashboard.use('/', Container.get(ProjectTasksController).router());
dashboard.use('/', Container.get(ProjectTimesController).router());
dashboard.use('/', Container.get(WarehousesItemController).router());
dashboard.use('/dashboard', Container.get(DashboardController).router());

View File

@@ -0,0 +1,29 @@
import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express';
export default (subscriptionSlug = 'main') =>
async (req: Request, res: Response, next: NextFunction) => {
const { tenant, tenantId } = req;
const { subscriptionRepository } = Container.get('repositories');
if (!tenant) {
throw new Error('Should load `TenancyMiddlware` before this middleware.');
}
const subscription = await subscriptionRepository.getBySlugInTenant(
subscriptionSlug,
tenantId
);
// Validate in case there is no any already subscription.
if (!subscription) {
return res.boom.badRequest('Tenant has no subscription.', {
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
});
}
// Validate in case the subscription is inactive.
else if (subscription.inactive()) {
return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
});
}
next();
};

View File

@@ -1,6 +1,6 @@
import dotenv from 'dotenv';
import path from 'path';
import { toInteger } from 'lodash';
import { defaultTo, toInteger } from 'lodash';
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
dotenv.config();
@@ -55,7 +55,7 @@ module.exports = {
mail: {
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
secure: !!parseInt(process.env.MAIL_SECURE, 10),
secure: parseBoolean(defaultTo(process.env.MAIL_SECURE, false), false),
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
from: process.env.MAIL_FROM_ADDRESS,
@@ -153,6 +153,13 @@ module.exports = {
),
},
/**
* Sign-up email confirmation
*/
signupConfirmation: {
enabled: parseBoolean<boolean>(process.env.SIGNUP_EMAIL_CONFIRMATION, false),
},
/**
* Puppeteer remote browserless connection.
*/
@@ -180,6 +187,14 @@ module.exports = {
},
},
/**
* Bank Synchronization.
*/
bankSync: {
enabled: parseBoolean(defaultTo(process.env.BANKING_CONNECT, false), false),
provider: 'plaid',
},
/**
* Plaid.
*/
@@ -190,6 +205,24 @@ module.exports = {
secretSandbox: process.env.PLAID_SECRET_SANDBOX,
redirectSandBox: process.env.PLAID_SANDBOX_REDIRECT_URI,
redirectDevelopment: process.env.PLAID_DEVELOPMENT_REDIRECT_URI,
linkWebhook: process.env.PLAID_LINK_WEBHOOK
linkWebhook: process.env.PLAID_LINK_WEBHOOK,
},
/**
* Lemon Squeezy.
*/
lemonSqueezy: {
key: process.env.LEMONSQUEEZY_API_KEY,
storeId: process.env.LEMONSQUEEZY_STORE_ID,
webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
},
/**
* Bigcapital (Cloud).
* NOTE: DO NOT CHANGE THIS OPTION OR ADD THIS ENV VAR.
*/
hostedOnBigcapitalCloud: parseBoolean(
defaultTo(process.env.HOSTED_ON_BIGCAPITAL_CLOUD, false),
false
),
};

View File

@@ -0,0 +1,8 @@
export default class NotAllowedChangeSubscriptionPlan {
constructor() {
this.name = "NotAllowedChangeSubscriptionPlan";
}
}

View File

@@ -1,3 +1,4 @@
import NotAllowedChangeSubscriptionPlan from './NotAllowedChangeSubscriptionPlan';
import ServiceError from './ServiceError';
import ServiceErrors from './ServiceErrors';
import TenantAlreadyInitialized from './TenantAlreadyInitialized';
@@ -6,6 +7,7 @@ import TenantDBAlreadyExists from './TenantDBAlreadyExists';
import TenantDatabaseNotBuilt from './TenantDatabaseNotBuilt';
export {
NotAllowedChangeSubscriptionPlan,
ServiceError,
ServiceErrors,
TenantAlreadyInitialized,

View File

@@ -66,16 +66,27 @@ export interface IAuthResetedPasswordEventPayload {
password: string;
}
export interface IAuthSendingResetPassword {
user: ISystemUser,
user: ISystemUser;
token: string;
}
export interface IAuthSendedResetPassword {
user: ISystemUser,
user: ISystemUser;
token: string;
}
export interface IAuthGetMetaPOJO {
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
}
}
export interface IAuthSignUpVerifingEventPayload {
email: string;
verifyToken: string;
userId: number;
}
export interface IAuthSignUpVerifiedEventPayload {
email: string;
verifyToken: string;
userId: number;
}

View File

@@ -1,6 +1,7 @@
export enum Features {
WAREHOUSES = 'warehouses',
BRANCHES = 'branches',
BankSyncing = 'BankSyncing'
}
export interface IFeatureAllItem {

View File

@@ -32,11 +32,13 @@ export interface IModelMetaFieldCommon {
name: string;
column: string;
columnable?: boolean;
fieldType: IModelColumnType;
customQuery?: Function;
required?: boolean;
importHint?: string;
importableRelationLabel?: string;
order?: number;
unique?: number;
dataTransferObjectKey?: string;
}
export interface IModelMetaFieldText {
@@ -67,6 +69,7 @@ export type IModelMetaField = IModelMetaFieldCommon &
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
| IModelMetaCollectionField
);
export interface IModelMetaEnumerationOption {
@@ -90,12 +93,93 @@ export interface IModelMetaRelationEnumerationField {
relationEntityKey: string;
}
export interface IModelMetaFieldWithFields {
fields: IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
);
}
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
collectionOf: 'object';
}
export interface IModelMetaCollectionFieldCommon {
fieldType: 'collection';
collectionMinLength?: number;
collectionMaxLength?: number;
}
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
IModelMetaCollectionObjectField;
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
IModelMetaRelationEnumerationField;
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
exportable?: boolean;
exportFlattenOn?: string;
importable?: boolean;
importAggregator?: string;
importAggregateOn?: string;
importAggregateBy?: string;
fields: { [key: string]: IModelMetaField };
columns: { [key: string]: IModelMetaColumn };
}
// ----
export interface IModelMetaFieldCommon2 {
name: string;
required?: boolean;
importHint?: string;
order?: number;
unique?: number;
}
export interface IModelMetaRelationField2 {
fieldType: 'relation';
relationModel: string;
importableRelationLabel: string | string[];
}
export type IModelMetaField2 = IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField2
| IModelMetaCollectionField
);
export interface ImodelMetaColumnMeta {
name: string;
accessor?: string;
exportable?: boolean;
}
interface IModelMetaColumnText {
type: 'text;';
}
interface IModelMetaColumnCollection {
type: 'collection';
collectionOf: 'object';
columns: { [key: string]: ImodelMetaColumnMeta & IModelMetaColumnText };
}
export type IModelMetaColumn = ImodelMetaColumnMeta &
(IModelMetaColumnText | IModelMetaColumnCollection);

View File

@@ -62,13 +62,13 @@ export default class MetableStore implements IMetableStore {
* @param {String} key -
* @param {Mixied} defaultValue -
*/
get(query: string | IMetaQuery, defaultValue: any): any | false {
get(query: string | IMetaQuery, defaultValue: any): any | null {
const metadata = this.find(query);
return metadata
? metadata.value
: typeof defaultValue !== 'undefined'
? defaultValue
: false;
: null;
}
/**

View File

@@ -89,7 +89,10 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; }
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete';
import { SubscribeFreeOnSignupCommunity } from '@/services/Subscription/events/SubscribeFreeOnSignupCommunity';
import { SendVerfiyMailOnSignUp } from '@/services/Authentication/events/SendVerfiyMailOnSignUp';
export default () => {
return new EventPublisher();
@@ -218,6 +221,9 @@ export const susbcribers = () => {
// Cashflow
DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete
PreventDeleteTransactionOnDelete,
SubscribeFreeOnSignupCommunity,
SendVerfiyMailOnSignUp
];
};

View File

@@ -36,7 +36,13 @@ export default ({ app }) => {
// Boom response objects.
app.use(boom());
app.use(bodyParser.json());
app.use(
bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
// Parses both json and urlencoded.
app.use(json());

View File

@@ -11,6 +11,8 @@ import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEsti
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
import { SendVerifyMailJob } from '@/services/Authentication/jobs/SendVerifyMailJob';
export default ({ agenda }: { agenda: Agenda }) => {
new ResetPasswordMailJob(agenda);
@@ -25,6 +27,10 @@ export default ({ agenda }: { agenda: Agenda }) => {
new SaleReceiptMailNotificationJob(agenda);
new PaymentReceiveMailNotificationJob(agenda);
new PlaidFetchTransactionsJob(agenda);
new ImportDeleteExpiredFilesJobs(agenda);
new SendVerifyMailJob(agenda);
agenda.start();
agenda.start().then(() => {
agenda.every('1 hours', 'delete-expired-imported-files', {});
});
};

View File

@@ -1,6 +1,7 @@
import Container from 'typedi';
import {
SystemUserRepository,
SubscriptionRepository,
TenantRepository,
} from '@/system/repositories';
@@ -10,6 +11,7 @@ export default () => {
return {
systemUserRepository: new SystemUserRepository(knex, cache),
subscriptionRepository: new SubscriptionRepository(knex, cache),
tenantRepository: new TenantRepository(knex, cache),
};
}

View File

@@ -61,7 +61,6 @@ import Task from 'models/Task';
import TaxRate from 'models/TaxRate';
import TaxRateTransaction from 'models/TaxRateTransaction';
import Attachment from 'models/Attachment';
import Import from 'models/Import';
import PlaidItem from 'models/PlaidItem';
import UncategorizedCashflowTransaction from 'models/UncategorizedCashflowTransaction';
@@ -128,7 +127,6 @@ export default (knex) => {
TaxRate,
TaxRateTransaction,
Attachment,
Import,
PlaidItem,
UncategorizedCashflowTransaction
};

View File

@@ -7,23 +7,17 @@ export default {
sortField: 'name',
},
importable: true,
exportable: true,
fields: {
name: {
name: 'account.field.name',
column: 'name',
fieldType: 'text',
unique: true,
required: true,
importable: true,
exportable: true,
order: 1,
},
description: {
name: 'account.field.description',
column: 'description',
fieldType: 'text',
importable: true,
exportable: true,
},
slug: {
name: 'account.field.slug',
@@ -31,19 +25,13 @@ export default {
fieldType: 'text',
columnable: false,
filterable: false,
importable: false,
},
code: {
name: 'account.field.code',
column: 'code',
fieldType: 'text',
exportable: true,
importable: true,
minLength: 3,
maxLength: 6,
importHint: 'Unique number to identify the account.',
},
rootType: {
root_type: {
name: 'account.field.root_type',
fieldType: 'enumeration',
options: [
@@ -55,7 +43,6 @@ export default {
],
filterCustomQuery: RootTypeFieldFilterQuery,
sortable: false,
importable: false,
},
normal: {
name: 'account.field.normal',
@@ -66,9 +53,8 @@ export default {
],
filterCustomQuery: NormalTypeFieldFilterQuery,
sortable: false,
importable: false,
},
accountType: {
type: {
name: 'account.field.type',
column: 'account_type',
fieldType: 'enumeration',
@@ -76,47 +62,121 @@ export default {
label: accountType.label,
key: accountType.key,
})),
required: true,
importable: true,
exportable: true,
order: 2,
},
active: {
name: 'account.field.active',
column: 'active',
fieldType: 'boolean',
filterable: false,
exportable: true,
importable: true,
},
balance: {
name: 'account.field.balance',
column: 'amount',
fieldType: 'number',
importable: false,
},
currencyCode: {
currency: {
name: 'account.field.currency',
column: 'currency_code',
fieldType: 'text',
filterable: false,
importable: true,
exportable: true,
},
parentAccount: {
name: 'account.field.parent_account',
column: 'parent_account_id',
fieldType: 'relation',
to: { model: 'Account', to: 'id' },
importable: false,
},
createdAt: {
created_at: {
name: 'account.field.created_at',
column: 'created_at',
fieldType: 'date',
importable: false,
},
},
columns: {
name: {
name: 'account.field.name',
type: 'text',
},
code: {
name: 'account.field.code',
type: 'text',
},
rootType: {
name: 'account.field.root_type',
type: 'text',
accessor: 'accountRootType',
},
accountType: {
name: 'account.field.type',
accessor: 'accountTypeLabel',
type: 'text',
},
accountNormal: {
name: 'account.field.normal',
accessor: 'accountNormalFormatted',
},
currencyCode: {
name: 'account.field.currency',
type: 'text',
},
bankBalance: {
name: 'account.field.bank_balance',
accessor: 'bankBalanceFormatted',
type: 'text',
exportable: true,
},
balance: {
name: 'account.field.balance',
accessor: 'amount',
},
description: {
name: 'account.field.description',
type: 'text',
},
active: {
name: 'account.field.active',
type: 'boolean',
},
createdAt: {
name: 'account.field.created_at',
},
},
fields2: {
name: {
name: 'account.field.name',
fieldType: 'text',
unique: true,
required: true,
},
description: {
name: 'account.field.description',
fieldType: 'text',
},
code: {
name: 'account.field.code',
fieldType: 'text',
minLength: 3,
maxLength: 6,
unique: true,
importHint: 'Unique number to identify the account.',
},
accountType: {
name: 'account.field.type',
fieldType: 'enumeration',
options: ACCOUNT_TYPES.map((accountType) => ({
label: accountType.label,
key: accountType.key,
})),
required: true,
},
active: {
name: 'account.field.active',
fieldType: 'boolean',
},
currencyCode: {
name: 'account.field.currency',
fieldType: 'text',
},
parentAccountId: {
name: 'account.field.parent_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
},
},
};

View File

@@ -1,10 +1,15 @@
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
importable: true,
exportFlattenOn: 'entries',
exportable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'billNumber',
fields: {
vendor: {
name: 'bill.field.vendor',
@@ -77,6 +82,155 @@ export default {
fieldType: 'date',
},
},
columns: {
billNumber: {
name: 'Bill No.',
type: 'text',
},
referenceNo: {
name: 'Reference No.',
type: 'text',
},
billDate: {
name: 'Date',
type: 'date',
},
dueDate: {
name: 'Due Date',
type: 'date',
},
vendorId: {
name: 'Vendor',
accessor: 'vendor.displayName',
type: 'text',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
exchangeRate: {
name: 'Exchange Rate',
type: 'number',
},
currencyCode: {
name: 'Currency Code',
type: 'text',
},
dueAmount: {
name: 'Due Amount',
accessor: 'formattedDueAmount',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'formattedPaymentAmount',
},
note: {
name: 'Note',
type: 'text',
},
open: {
name: 'Open',
type: 'boolean',
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
billNumber: {
name: 'Bill No.',
fieldType: 'text',
required: true,
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
billDate: {
name: 'Date',
fieldType: 'date',
required: true,
},
dueDate: {
name: 'Due Date',
fieldType: 'date',
required: true,
},
vendorId: {
name: 'Vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
note: {
name: 'Note',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'Item',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Line Description',
fieldType: 'text',
},
},
},
},
};
/**

View File

@@ -4,6 +4,11 @@ export default {
sortOrder: 'DESC',
sortField: 'bill_date',
},
exportable: true,
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentNumber',
fields: {
vendor: {
name: 'bill_payment.field.vendor',
@@ -33,7 +38,7 @@ export default {
relationType: 'enumeration',
relationKey: 'paymentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
@@ -63,4 +68,107 @@ export default {
fieldType: 'date',
},
},
columns: {
vendor: {
name: 'bill_payment.field.vendor',
type: 'relation',
accessor: 'vendor.displayName',
},
paymentDate: {
name: 'bill_payment.field.payment_date',
type: 'date',
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
type: 'text',
},
paymentAccount: {
name: 'bill_payment.field.payment_account',
accessor: 'paymentAccount.name',
type: 'text',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
currencyCode: {
name: 'Currency Code',
type: 'text',
},
exchangeRate: {
name: 'bill_payment.field.exchange_rate',
type: 'number',
},
statement: {
name: 'bill_payment.field.note',
type: 'text',
},
reference: {
name: 'bill_payment.field.reference',
type: 'text',
},
},
fields2: {
vendorId: {
name: 'bill_payment.field.vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
payment_date: {
name: 'bill_payment.field.payment_date',
fieldType: 'date',
required: true,
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
fieldType: 'text',
unique: true,
importHint: 'The payment number should be unique.',
},
paymentAccountId: {
name: 'bill_payment.field.payment_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the account name or code.',
},
exchangeRate: {
name: 'bill_payment.field.exchange_rate',
fieldType: 'number',
},
statement: {
name: 'bill_payment.field.note',
fieldType: 'text',
},
reference: {
name: 'bill_payment.field.reference',
fieldType: 'text',
},
entries: {
name: 'bill_payment.field.entries',
column: 'entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
billId: {
name: 'bill_payment.field.entries.bill',
fieldType: 'relation',
relationModel: 'Bill',
relationImportMatch: 'billNumber',
required: true,
importHint: 'Matches the bill number.',
},
paymentAmount: {
name: 'bill_payment.field.entries.payment_amount',
fieldType: 'number',
required: true,
},
},
},
},
};

View File

@@ -187,18 +187,4 @@ export default class Contact extends TenantModel {
},
};
}
static get fields() {
return {
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
created_at: {
column: 'created_at',
},
};
}
}

View File

@@ -12,6 +12,14 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'creditNoteNumber',
fields: {
customer: {
name: 'credit_note.field.customer',
@@ -77,4 +85,133 @@ export default {
fieldType: 'date',
},
},
columns: {
customer: {
name: 'Customer',
type: 'relation',
accessor: 'customer.displayName',
},
exchangeRate: {
name: 'Exchange Rate',
type: 'number',
},
creditNoteDate: {
name: 'Credit Note Date',
type: 'date',
},
referenceNo: {
name: 'Reference No.',
type: 'text',
},
note: {
name: 'Note',
type: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
type: 'text',
},
creditNoteNumber: {
name: 'Credit Note Number',
type: 'text',
},
open: {
name: 'Open',
type: 'boolean',
},
entries: {
name: 'Entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
creditNoteDate: {
name: 'Credit Note Date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
note: {
name: 'Note',
fieldType: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
fieldType: 'text',
},
creditNoteNumber: {
name: 'Credit Note Number',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
fields: {
itemId: {
name: 'Item',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
},
},
},
};

View File

@@ -1,215 +1,381 @@
export default {
importable: true,
exportable: true,
defaultFilterField: 'displayName',
defaultSort: {
sortOrder: 'DESC',
sortField: 'createdAt',
sortField: 'created_at',
},
fields: {
first_name: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
},
last_name: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
},
display_name: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
},
work_phone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
},
personal_phone: {
name: 'vendor.field.personal_pone',
column: 'personal_phone',
fieldType: 'text',
},
company_name: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
},
created_at: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'vendor.field.balance',
column: 'balance',
fieldType: 'number',
},
opening_balance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
},
opening_balance_at: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
},
currency_code: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
},
status: {
name: 'vendor.field.status',
type: 'enumeration',
options: [
{ key: 'overdue', label: 'vendor.field.status.overdue' },
{ key: 'unpaid', label: 'vendor.field.status.unpaid' },
],
filterCustomQuery: (query, role) => {
switch (role.value) {
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
},
columns: {
firstName: {
name: 'vendor.field.first_name',
type: 'text',
},
lastName: {
name: 'vendor.field.last_name',
type: 'text',
},
displayName: {
name: 'vendor.field.display_name',
type: 'text',
},
email: {
name: 'vendor.field.email',
type: 'text',
},
workPhone: {
name: 'vendor.field.work_phone',
type: 'text',
},
personalPhone: {
name: 'vendor.field.personal_phone',
type: 'text',
},
companyName: {
name: 'vendor.field.company_name',
type: 'text',
},
website: {
name: 'vendor.field.website',
type: 'text',
},
balance: {
name: 'vendor.field.balance',
type: 'number',
},
openingBalance: {
name: 'vendor.field.opening_balance',
type: 'number',
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
type: 'date',
},
currencyCode: {
name: 'vendor.field.currency',
type: 'text',
},
status: {
name: 'vendor.field.status',
},
note: {
name: 'vendor.field.note',
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
type: 'text',
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
type: 'text',
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
type: 'text',
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
type: 'text',
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
type: 'text',
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
type: 'text',
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
type: 'text',
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
type: 'text',
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
type: 'text',
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
type: 'text',
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
type: 'text',
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
type: 'text',
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
type: 'text',
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
type: 'text',
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
},
},
fields2: {
customerType: {
name: 'Customer Type',
column: 'contact_type',
fieldType: 'enumeration',
options: [
{ key: 'business', label: 'Business' },
{ key: 'individual', label: 'Individual' },
],
importable: true,
required: true,
},
firstName: {
name: 'customer.field.first_name',
column: 'first_name',
fieldType: 'text',
importable: true,
},
lastName: {
name: 'customer.field.last_name',
column: 'last_name',
fieldType: 'text',
importable: true,
},
displayName: {
name: 'customer.field.display_name',
column: 'display_name',
fieldType: 'text',
required: true,
importable: true,
},
email: {
name: 'customer.field.email',
column: 'email',
fieldType: 'text',
importable: true,
},
workPhone: {
name: 'customer.field.work_phone',
column: 'work_phone',
fieldType: 'text',
importable: true,
},
personalPhone: {
name: 'customer.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
importable: true,
},
companyName: {
name: 'customer.field.company_name',
column: 'company_name',
fieldType: 'text',
importable: true,
},
website: {
name: 'customer.field.website',
column: 'website',
fieldType: 'url',
importable: true,
},
balance: {
name: 'customer.field.balance',
column: 'balance',
fieldType: 'number',
},
openingBalance: {
name: 'customer.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
importable: true,
},
openingBalanceAt: {
name: 'customer.field.opening_balance_at',
column: 'opening_balance_at',
filterable: false,
fieldType: 'date',
importable: true,
},
openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
importable: true,
},
currencyCode: {
name: 'customer.field.currency',
column: 'currency_code',
fieldType: 'text',
importable: true,
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
importable: true,
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
importable: true,
},
status: {
name: 'customer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'active', label: 'customer.field.status.active' },
{ key: 'inactive', label: 'customer.field.status.inactive' },
{ key: 'overdue', label: 'customer.field.status.overdue' },
{ key: 'unpaid', label: 'customer.field.status.unpaid' },
],
filterCustomQuery: statusFieldFilterQuery,
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
fieldType: 'text',
importable: true,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
fieldType: 'text',
importable: true,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
fieldType: 'text',
importable: true,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
fieldType: 'text',
importable: true,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
fieldType: 'text',
importable: true,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
fieldType: 'text',
importable: true,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
fieldType: 'text',
importable: true,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
fieldType: 'text',
importable: true,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
fieldType: 'text',
importable: true,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
fieldType: 'text',
importable: true,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
fieldType: 'text',
importable: true,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
fieldType: 'text',
importable: true,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
fieldType: 'text',
importable: true,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
fieldType: 'text',
importable: true,
},
//
createdAt: {
name: 'customer.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -7,13 +7,16 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
importable: true,
exportFlattenOn: 'categories',
exportable: true,
fields: {
'payment_date': {
payment_date: {
name: 'expense.field.payment_date',
column: 'payment_date',
fieldType: 'date',
},
'payment_account': {
payment_account: {
name: 'expense.field.payment_account',
column: 'payment_account_id',
fieldType: 'relation',
@@ -24,27 +27,27 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'amount': {
amount: {
name: 'expense.field.amount',
column: 'total_amount',
fieldType: 'number',
},
'reference_no': {
reference_no: {
name: 'expense.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
'description': {
description: {
name: 'expense.field.description',
column: 'description',
fieldType: 'text',
},
'published': {
published: {
name: 'expense.field.published',
column: 'published_at',
fieldType: 'date',
},
'status': {
status: {
name: 'expense.field.status',
fieldType: 'enumeration',
options: [
@@ -54,12 +57,121 @@ export default {
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
created_at: {
name: 'expense.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
columns: {
paymentReceive: {
name: 'expense.field.payment_account',
type: 'text',
accessor: 'paymentAccount.name'
},
referenceNo: {
name: 'expense.field.reference_no',
type: 'text',
},
paymentDate: {
name: 'expense.field.payment_date',
type: 'date',
},
currencyCode: {
name: 'expense.field.currency_code',
type: 'text',
},
exchangeRate: {
name: 'expense.field.exchange_rate',
type: 'number',
},
description: {
name: 'expense.field.description',
type: 'text',
},
categories: {
name: 'expense.field.categories',
type: 'collection',
collectionOf: 'object',
columns: {
expenseAccount: {
name: 'expense.field.expense_account',
accessor: 'expenseAccount.name',
},
amount: {
name: 'expense.field.amount',
accessor: 'amountFormatted',
},
description: {
name: 'expense.field.line_description',
type: 'text',
},
},
},
publish: {
name: 'expense.field.publish',
type: 'boolean',
},
},
fields2: {
paymentAccountId: {
name: 'expense.field.payment_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the account name or code.',
},
referenceNo: {
name: 'expense.field.reference_no',
fieldType: 'text',
},
paymentDate: {
name: 'expense.field.payment_date',
fieldType: 'date',
required: true,
},
currencyCode: {
name: 'expense.field.currency_code',
fieldType: 'text',
},
exchangeRate: {
name: 'expense.field.exchange_rate',
fieldType: 'number',
},
description: {
name: 'expense.field.description',
fieldType: 'text',
},
categories: {
name: 'expense.field.categories',
fieldType: 'collection',
collectionOf: 'object',
fields: {
expenseAccountId: {
name: 'expense.field.expense_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the account name or code.',
},
amount: {
name: 'expense.field.amount',
fieldType: 'number',
required: true,
},
description: {
name: 'expense.field.line_description',
fieldType: 'text',
},
},
},
publish: {
name: 'expense.field.publish',
fieldType: 'boolean',
},
},
};
function StatusFieldFilterQuery(query, role) {

View File

@@ -4,6 +4,54 @@ export default {
sortOrder: 'DESC',
sortField: 'date',
},
columns: {
date: {
name: 'inventory_adjustment.field.date',
column: 'date',
fieldType: 'date',
exportable: true,
},
type: {
name: 'inventory_adjustment.field.type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'increment', name: 'inventory_adjustment.field.type.increment' },
{ key: 'decrement', name: 'inventory_adjustment.field.type.decrement' },
],
exportable: true,
},
adjustmentAccount: {
name: 'inventory_adjustment.field.adjustment_account',
type: 'adjustment_account_id',
exportable: true,
},
reason: {
name: 'inventory_adjustment.field.reason',
type: 'text',
exportable: true,
},
referenceNo: {
name: 'inventory_adjustment.field.reference_no',
type: 'text',
exportable: true,
},
description: {
name: 'inventory_adjustment.field.description',
type: 'text',
exportable: true,
},
publishedAt: {
name: 'inventory_adjustment.field.published_at',
type: 'date',
exportable: true,
},
createdAt: {
name: 'inventory_adjustment.field.created_at',
type: 'date',
exportable: true,
},
},
fields: {
date: {
name: 'inventory_adjustment.field.date',

View File

@@ -1,5 +1,6 @@
export default {
importable: true,
exportable: true,
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
@@ -15,45 +16,38 @@ export default {
{ key: 'service', label: 'item.field.type.service' },
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
],
importable: true,
},
name: {
name: 'item.field.name',
column: 'name',
fieldType: 'text',
importable: true,
},
code: {
name: 'item.field.code',
column: 'code',
fieldType: 'text',
importable: true,
},
sellable: {
name: 'item.field.sellable',
column: 'sellable',
fieldType: 'boolean',
importable: true,
},
purchasable: {
name: 'item.field.purchasable',
column: 'purchasable',
fieldType: 'boolean',
importable: true,
},
sellPrice: {
sell_price: {
name: 'item.field.cost_price',
column: 'sell_price',
fieldType: 'number',
importable: true,
},
costPrice: {
cost_price: {
name: 'item.field.cost_account',
column: 'cost_price',
fieldType: 'number',
importable: true,
},
costAccount: {
cost_account: {
name: 'item.field.sell_account',
column: 'cost_account_id',
fieldType: 'relation',
@@ -63,10 +57,8 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
importable: true,
},
sellAccount: {
sell_account: {
name: 'item.field.sell_description',
column: 'sell_account_id',
fieldType: 'relation',
@@ -76,10 +68,8 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
importable: true,
},
inventoryAccount: {
inventory_account: {
name: 'item.field.inventory_account',
column: 'inventory_account_id',
@@ -88,32 +78,26 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
importable: true,
},
sellDescription: {
sell_description: {
name: 'Sell description',
column: 'sell_description',
fieldType: 'text',
importable: true,
},
purchaseDescription: {
purchase_description: {
name: 'Purchase description',
column: 'purchase_description',
fieldType: 'text',
importable: true,
},
quantityOnHand: {
quantity_on_hand: {
name: 'item.field.quantity_on_hand',
column: 'quantity_on_hand',
fieldType: 'number',
importable: true,
},
note: {
name: 'item.field.note',
column: 'note',
fieldType: 'text',
importable: true,
},
category: {
name: 'item.field.category',
@@ -124,19 +108,190 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'id',
importable: true,
},
active: {
name: 'item.field.active',
column: 'active',
fieldType: 'boolean',
importable: true,
filterable: false,
},
createdAt: {
created_at: {
name: 'item.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
columns: {
type: {
name: 'item.field.type',
type: 'text',
exportable: true,
},
name: {
name: 'item.field.name',
type: 'text',
exportable: true,
},
code: {
name: 'item.field.code',
type: 'text',
exportable: true,
},
sellable: {
name: 'item.field.sellable',
type: 'boolean',
exportable: true,
},
purchasable: {
name: 'item.field.purchasable',
type: 'boolean',
exportable: true,
},
sellPrice: {
name: 'item.field.cost_price',
type: 'number',
exportable: true,
},
costPrice: {
name: 'item.field.cost_account',
type: 'number',
exportable: true,
},
costAccount: {
name: 'item.field.sell_account',
type: 'text',
accessor: 'costAccount.name',
exportable: true,
},
sellAccount: {
name: 'item.field.sell_description',
type: 'text',
accessor: 'sellAccount.name',
exportable: true,
},
inventoryAccount: {
name: 'item.field.inventory_account',
type: 'text',
accessor: 'inventoryAccount.name',
exportable: true,
},
sellDescription: {
name: 'Sell description',
type: 'text',
exportable: true,
},
purchaseDescription: {
name: 'Purchase description',
type: 'text',
exportable: true,
},
quantityOnHand: {
name: 'item.field.quantity_on_hand',
type: 'number',
exportable: true,
},
note: {
name: 'item.field.note',
type: 'text',
exportable: true,
},
category: {
name: 'item.field.category',
type: 'text',
accessor: 'category.name',
exportable: true,
},
active: {
name: 'item.field.active',
fieldType: 'boolean',
exportable: true,
},
createdAt: {
name: 'item.field.created_at',
type: 'date',
exportable: true,
},
},
fields2: {
type: {
name: 'item.field.type',
fieldType: 'enumeration',
options: [
{ key: 'inventory', label: 'item.field.type.inventory' },
{ key: 'service', label: 'item.field.type.service' },
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
],
required: true,
},
name: {
name: 'item.field.name',
fieldType: 'text',
required: true,
},
code: {
name: 'item.field.code',
fieldType: 'text',
},
sellable: {
name: 'item.field.sellable',
fieldType: 'boolean',
},
purchasable: {
name: 'item.field.purchasable',
fieldType: 'boolean',
},
sellPrice: {
name: 'item.field.sell_price',
fieldType: 'number',
},
cost_price: {
name: 'item.field.cost_price',
fieldType: 'number',
},
costAccount: {
name: 'item.field.cost_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellAccount: {
name: 'item.field.sell_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
inventoryAccount: {
name: 'item.field.inventory_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
importHint: 'Matches the account name or code.',
},
sellDescription: {
name: 'Sell Description',
fieldType: 'text',
},
purchaseDescription: {
name: 'Purchase Description',
fieldType: 'text',
},
note: {
name: 'item.field.note',
fieldType: 'text',
},
category: {
name: 'item.field.category',
fieldType: 'relation',
relationModel: 'ItemCategory',
relationImportMatch: ['name'],
importHint: 'Matches the category name.',
},
active: {
name: 'item.field.active',
fieldType: 'boolean',
},
},
};

View File

@@ -4,6 +4,8 @@ export default {
sortField: 'name',
sortOrder: 'DESC',
},
importable: true,
exportable: true,
fields: {
name: {
name: 'item_category.field.name',
@@ -27,4 +29,34 @@ export default {
columnType: 'date',
},
},
columns: {
name: {
name: 'item_category.field.name',
type: 'text',
},
description: {
name: 'item_category.field.description',
type: 'text',
},
count: {
name: 'item_category.field.count',
type: 'text',
},
createdAt: {
name: 'item_category.field.created_at',
type: 'text',
},
},
fields2: {
name: {
name: 'item_category.field.name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'item_category.field.description',
column: 'description',
fieldType: 'text',
},
},
};

View File

@@ -4,54 +4,203 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
importable: true,
exportFlattenOn: 'entries',
exportable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'journalNumber',
fields: {
'date': {
date: {
name: 'manual_journal.field.date',
column: 'date',
fieldType: 'date',
},
'journal_number': {
journal_number: {
name: 'manual_journal.field.journal_number',
column: 'journal_number',
fieldType: 'text',
},
'reference': {
reference: {
name: 'manual_journal.field.reference',
column: 'reference',
fieldType: 'text',
},
'journal_type': {
journal_type: {
name: 'manual_journal.field.journal_type',
column: 'journal_type',
fieldType: 'text',
},
'amount': {
amount: {
name: 'manual_journal.field.amount',
column: 'amount',
fieldType: 'number',
},
'description': {
description: {
name: 'manual_journal.field.description',
column: 'description',
fieldType: 'text',
},
'status': {
status: {
name: 'manual_journal.field.status',
column: 'status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'published' }
{ key: 'published', label: 'published' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
created_at: {
name: 'manual_journal.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
columns: {
date: {
name: 'manual_journal.field.date',
type: 'date',
},
journalNumber: {
name: 'manual_journal.field.journal_number',
type: 'text',
},
reference: {
name: 'manual_journal.field.reference',
type: 'text',
},
journalType: {
name: 'manual_journal.field.journal_type',
type: 'text',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
currencyCode: {
name: 'manual_journal.field.currency',
type: 'text',
},
exchangeRate: {
name: 'manual_journal.field.exchange_rate',
type: 'number',
},
description: {
name: 'manual_journal.field.description',
type: 'text',
},
entries: {
name: 'Entries',
type: 'collection',
collectionOf: 'object',
columns: {
credit: {
name: 'Credit',
type: 'text',
},
debit: {
name: 'Debit',
type: 'text',
},
account: {
name: 'Account',
accessor: 'account.name',
},
contact: {
name: 'Contact',
accessor: 'contact.displayName',
},
note: {
name: 'Note',
},
},
publish: {
name: 'Publish',
type: 'boolean',
},
publishedAt: {
name: 'Published At',
},
},
createdAt: {
name: 'Created At',
},
},
fields2: {
date: {
name: 'manual_journal.field.date',
fieldType: 'date',
required: true,
},
journalNumber: {
name: 'manual_journal.field.journal_number',
fieldType: 'text',
required: true,
},
reference: {
name: 'manual_journal.field.reference',
fieldType: 'text',
},
journalType: {
name: 'manual_journal.field.journal_type',
fieldType: 'text',
},
currencyCode: {
name: 'manual_journal.field.currency',
fieldType: 'text',
},
exchange_rate: {
name: 'manual_journal.field.exchange_rate',
fieldType: 'number',
},
description: {
name: 'manual_journal.field.description',
fieldType: 'text',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 2,
required: true,
fields: {
credit: {
name: 'Credit',
fieldType: 'number',
required: true,
},
debit: {
name: 'Debit',
fieldType: 'number',
required: true,
},
accountId: {
name: 'Account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
contact: {
name: 'Contact',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
},
note: {
name: 'Note',
fieldType: 'text',
},
},
},
publish: {
name: 'Publish',
fieldType: 'boolean',
},
},
};
/**
@@ -64,6 +213,6 @@ function StatusFieldSortQuery(query, role) {
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}

View File

@@ -1,33 +1,54 @@
import { get } from 'lodash';
import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from '@/interfaces';
import {
IModelMeta,
IModelMetaField,
IModelMetaDefaultSort,
} from '@/interfaces';
const defaultModelMeta = {
fields: {},
fields2: {},
};
export default (Model) =>
class ModelSettings extends Model {
/**
*
* @returns {IModelMeta}
*/
static get meta(): IModelMeta {
throw new Error('');
}
/**
* Parsed meta merged with default emta.
* @returns {IModelMeta}
*/
static get parsedMeta(): IModelMeta {
return {
...defaultModelMeta,
...this.meta,
};
}
/**
* Retrieve specific model field meta of the given field key.
* @param {string} key
* @returns {IModelMetaField}
*/
public static getField(key: string, attribute?:string): IModelMetaField {
public static getField(key: string, attribute?: string): IModelMetaField {
const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field;
}
/**
* Retrieve the specific model meta.
* Retrieves the specific model meta.
* @param {string} key
* @returns
*/
public static getMeta(key?: string) {
return key ? get(this.meta, key): this.meta;
return key ? get(this.parsedMeta, key) : this.parsedMeta;
}
/**

View File

@@ -1,5 +1,9 @@
export default {
importable: true,
exportable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo',
fields: {
customer: {
name: 'payment_receive.field.customer',
@@ -54,4 +58,101 @@ export default {
fieldDate: 'date',
},
},
columns: {
customer: {
name: 'payment_receive.field.customer',
accessor: 'customer.displayName',
type: 'text',
},
paymentDate: {
name: 'payment_receive.field.payment_date',
type: 'date',
},
amount: {
name: 'payment_receive.field.amount',
type: 'number',
},
referenceNo: {
name: 'payment_receive.field.reference_no',
type: 'text',
},
depositAccount: {
name: 'payment_receive.field.deposit_account',
accessor: 'depositAccount.name',
type: 'text',
},
paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no',
type: 'text',
},
statement: {
name: 'payment_receive.field.statement',
type: 'text',
},
created_at: {
name: 'payment_receive.field.created_at',
type: 'date',
},
},
fields2: {
customerId: {
name: 'payment_receive.field.customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
exchangeRate: {
name: 'payment_receive.field.exchange_rate',
fieldType: 'number',
},
paymentDate: {
name: 'payment_receive.field.payment_date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'payment_receive.field.reference_no',
fieldType: 'text',
},
depositAccountId: {
name: 'payment_receive.field.deposit_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the account name or code.',
},
paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no',
fieldType: 'text',
importHint: 'The payment number should be unique.',
},
statement: {
name: 'payment_receive.field.statement',
fieldType: 'text',
},
entries: {
name: 'payment_receive.field.entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
invoiceId: {
name: 'payment_receive.field.invoice',
fieldType: 'relation',
relationModel: 'SaleInvoice',
relationImportMatch: 'invoiceNo',
required: true,
importHint: 'Matches the invoice number.',
},
paymentAmount: {
name: 'payment_receive.field.entries.payment_amount',
fieldType: 'number',
required: true,
},
},
},
},
};

View File

@@ -4,18 +4,25 @@ export default {
sortOrder: 'DESC',
sortField: 'estimate_date',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'estimateNumber',
fields: {
'amount': {
amount: {
name: 'estimate.field.amount',
column: 'amount',
fieldType: 'number',
},
'estimate_number': {
estimate_number: {
name: 'estimate.field.estimate_number',
column: 'estimate_number',
fieldType: 'text',
},
'customer': {
customer: {
name: 'estimate.field.customer',
column: 'customer_id',
fieldType: 'relation',
@@ -26,32 +33,32 @@ export default {
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'estimate_date': {
estimate_date: {
name: 'estimate.field.estimate_date',
column: 'estimate_date',
fieldType: 'date',
},
'expiration_date': {
expiration_date: {
name: 'estimate.field.expiration_date',
column: 'expiration_date',
fieldType: 'date',
},
'reference_no': {
reference_no: {
name: 'estimate.field.reference_no',
column: 'reference',
fieldType: 'text',
},
'note': {
note: {
name: 'estimate.field.note',
column: 'note',
fieldType: 'text',
},
'terms_conditions': {
terms_conditions: {
name: 'estimate.field.terms_conditions',
column: 'terms_conditions',
fieldType: 'text',
},
'status': {
status: {
name: 'estimate.field.status',
fieldType: 'enumeration',
options: [
@@ -63,12 +70,175 @@ export default {
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
created_at: {
name: 'estimate.field.created_at',
column: 'created_at',
columnType: 'date',
},
},
columns: {
customer: {
name: 'Customer',
type: 'text',
accessor: 'customer.displayName',
exportable: true,
},
estimateDate: {
name: 'Estimate Date',
type: 'date',
exportable: true,
},
expirationDate: {
name: 'Expiration Date',
type: 'date',
exportable: true,
},
estimateNumber: {
name: 'Estimate No.',
type: 'text',
exportable: true,
},
reference: {
name: 'Reference No.',
type: 'text',
exportable: true,
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
type: 'text',
},
exchangeRate: {
name: 'Exchange Rate',
type: 'number',
exportable: true,
},
currencyCode: {
name: 'Currency',
type: 'text',
exportable: true,
},
note: {
name: 'Note',
type: 'text',
exportable: true,
},
termsConditions: {
name: 'Terms & Conditions',
type: 'text',
exportable: true,
},
delivered: {
name: 'Delivered',
type: 'boolean',
exportable: true,
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
estimateDate: {
name: 'Estimate Date',
fieldType: 'date',
required: true,
},
expirationDate: {
name: 'Expiration Date',
fieldType: 'date',
required: true,
},
estimateNumber: {
name: 'Estimate No.',
fieldType: 'text',
},
reference: {
name: 'Reference No.',
fieldType: 'text',
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
currencyCode: {
name: 'Currency',
fieldType: 'text',
},
note: {
name: 'Note',
fieldType: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
fieldType: 'text',
},
delivered: {
name: 'Delivered',
type: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'invoice.field.item_name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Line Description',
fieldType: 'text',
},
},
},
},
};
function StatusFieldSortQuery(query, role) {

View File

@@ -4,6 +4,13 @@ export default {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'invoiceNo',
fields: {
customer: {
name: 'invoice.field.customer',
@@ -83,6 +90,167 @@ export default {
fieldType: 'date',
},
},
columns: {
invoiceDate: {
name: 'invoice.field.invoice_date',
type: 'date',
},
dueDate: {
name: 'invoice.field.due_date',
type: 'date',
},
referenceNo: {
name: 'invoice.field.reference_no',
type: 'text',
},
invoiceNo: {
name: 'invoice.field.invoice_no',
type: 'text',
},
customer: {
name: 'invoice.field.customer',
type: 'text',
accessor: 'customer.displayName',
},
amount: {
name: 'invoice.field.amount',
type: 'text',
accessor: 'balanceAmountFormatted',
},
exchangeRate: {
name: 'invoice.field.exchange_rate',
type: 'number',
},
currencyCode: {
name: 'invoice.field.currency',
type: 'text',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
dueAmount: {
name: 'Due Amount',
accessor: 'dueAmountFormatted',
},
invoiceMessage: {
name: 'invoice.field.invoice_message',
type: 'text',
},
termsConditions: {
name: 'invoice.field.terms_conditions',
type: 'text',
},
delivered: {
name: 'invoice.field.delivered',
type: 'boolean',
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
invoiceDate: {
name: 'invoice.field.invoice_date',
fieldType: 'date',
required: true,
},
dueDate: {
name: 'invoice.field.due_date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'invoice.field.reference_no',
fieldType: 'text',
},
invoiceNo: {
name: 'invoice.field.invoice_no',
fieldType: 'text',
},
customerId: {
name: 'invoice.field.customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'invoice.field.exchange_rate',
fieldType: 'number',
},
currencyCode: {
name: 'invoice.field.currency',
fieldType: 'text',
},
invoiceMessage: {
name: 'invoice.field.invoice_message',
fieldType: 'text',
},
termsConditions: {
name: 'invoice.field.terms_conditions',
fieldType: 'text',
},
entries: {
name: 'invoice.field.entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'invoice.field.item_name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'invoice.field.description',
fieldType: 'text',
},
},
},
delivered: {
name: 'invoice.field.delivered',
fieldType: 'boolean',
},
},
};
/**

View File

@@ -4,13 +4,20 @@ export default {
sortOrder: 'DESC',
sortField: 'created_at',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'receiptNumber',
fields: {
'amount': {
amount: {
name: 'receipt.field.amount',
column: 'amount',
fieldType: 'number',
},
'deposit_account': {
deposit_account: {
column: 'deposit_account_id',
name: 'receipt.field.deposit_account',
fieldType: 'relation',
@@ -21,7 +28,7 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'customer': {
customer: {
name: 'receipt.field.customer',
column: 'customer_id',
fieldType: 'relation',
@@ -32,38 +39,37 @@ export default {
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'receipt_date': {
receipt_date: {
name: 'receipt.field.receipt_date',
column: 'receipt_date',
fieldType: 'date',
},
'receipt_number': {
receipt_number: {
name: 'receipt.field.receipt_number',
column: 'receipt_number',
fieldType: 'text',
},
'reference_no': {
reference_no: {
name: 'receipt.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
'receipt_message': {
receipt_message: {
name: 'receipt.field.receipt_message',
column: 'receipt_message',
fieldType: 'text',
},
'statement': {
statement: {
name: 'receipt.field.statement',
column: 'statement',
fieldType: 'text',
},
'created_at': {
created_at: {
name: 'receipt.field.created_at',
column: 'created_at',
fieldType: 'date',
},
'status': {
status: {
name: 'receipt.field.status',
fieldType: 'enumeration',
options: [
@@ -74,6 +80,162 @@ export default {
sortCustomQuery: StatusFieldSortQuery,
},
},
columns: {
amount: {
name: 'receipt.field.amount',
column: 'amount',
type: 'number',
},
depositAccount: {
name: 'receipt.field.deposit_account',
type: 'text',
accessor: 'depositAccount.name',
},
customer: {
name: 'receipt.field.customer',
type: 'text',
accessor: 'customer.displayName',
},
receiptDate: {
name: 'receipt.field.receipt_date',
type: 'date',
},
receiptNumber: {
name: 'receipt.field.receipt_number',
type: 'text',
},
referenceNo: {
name: 'receipt.field.reference_no',
column: 'reference_no',
type: 'text',
exportable: true,
},
receiptMessage: {
name: 'receipt.field.receipt_message',
column: 'receipt_message',
type: 'text',
},
statement: {
name: 'receipt.field.statement',
type: 'text',
},
status: {
name: 'receipt.field.status',
type: 'enumeration',
options: [
{ key: 'draft', label: 'receipt.field.status.draft' },
{ key: 'closed', label: 'receipt.field.status.closed' },
],
exportable: true,
},
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
createdAt: {
name: 'receipt.field.created_at',
type: 'date',
},
},
fields2: {
receiptDate: {
name: 'Receipt Date',
fieldType: 'date',
required: true,
},
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
depositAccountId: {
name: 'Deposit Account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
receiptNumber: {
name: 'Receipt Number',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
closed: {
name: 'Closed',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'invoice.field.item_name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'invoice.field.description',
fieldType: 'text',
},
},
},
statement: {
name: 'Statement',
fieldType: 'text',
},
receiptMessage: {
name: 'Receipt Message',
fieldType: 'text',
},
},
};
function StatusFieldFilterQuery(query, role) {

View File

@@ -2,7 +2,7 @@ export default {
defaultFilterField: 'createdAt',
defaultSort: {
sortOrder: 'DESC',
sortField: 'createdAt',
sortField: 'created_at',
},
importable: true,
fields: {
@@ -10,33 +10,27 @@ export default {
name: 'Date',
column: 'date',
fieldType: 'date',
importable: true,
required: true,
},
payee: {
name: 'Payee',
column: 'payee',
fieldType: 'text',
importable: true,
},
description: {
name: 'Description',
column: 'description',
fieldType: 'text',
importable: true,
},
referenceNo: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
importable: true,
},
amount: {
name: 'Amount',
column: 'Amount',
fieldType: 'numeric',
required: true,
importable: true,
},
account: {
name: 'Account',
@@ -51,4 +45,28 @@ export default {
importable: false,
},
},
fields2: {
date: {
name: 'Date',
fieldType: 'date',
required: true,
},
payee: {
name: 'Payee',
fieldType: 'text',
},
description: {
name: 'Description',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
amount: {
name: 'Amount',
fieldType: 'number',
required: true,
},
},
};

View File

@@ -2,99 +2,75 @@ export default {
defaultFilterField: 'displayName',
defaultSort: {
sortOrder: 'DESC',
sortField: 'createdAt',
sortField: 'created_at',
},
importable: true,
exportable: true,
fields: {
firstName: {
first_name: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
importable: true,
},
lastName: {
last_name: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
importable: true,
},
displayName: {
display_name: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
required: true,
importable: true,
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
importable: true,
},
workPhone: {
work_phone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
importable: true,
},
personalPhone: {
personal_phone: {
name: 'vendor.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
importable: true,
},
companyName: {
company_name: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
importable: true,
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
importable: true,
},
created_at: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'vendor.field.balance',
column: 'balance',
fieldType: 'number',
},
openingBalance: {
opening_balance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
importable: true,
},
openingBalanceAt: {
opening_balance_at: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
importable: true,
},
openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
importable: true,
},
currencyCode: {
currency_code: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
importable: true,
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
importable: true,
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
importable: true,
},
status: {
name: 'vendor.field.status',
@@ -114,96 +90,298 @@ export default {
}
},
},
},
columns: {
firstName: {
name: 'vendor.field.first_name',
type: 'text',
},
lastName: {
name: 'vendor.field.last_name',
type: 'text',
},
displayName: {
name: 'vendor.field.display_name',
type: 'text',
},
email: {
name: 'vendor.field.email',
type: 'text',
},
workPhone: {
name: 'vendor.field.work_phone',
type: 'text',
},
personalPhone: {
name: 'vendor.field.personal_phone',
type: 'text',
},
companyName: {
name: 'vendor.field.company_name',
type: 'text',
},
website: {
name: 'vendor.field.website',
type: 'text',
},
balance: {
name: 'vendor.field.balance',
type: 'number',
},
openingBalance: {
name: 'vendor.field.opening_balance',
type: 'number',
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
type: 'date',
},
currencyCode: {
name: 'vendor.field.currency',
type: 'text',
},
status: {
name: 'vendor.field.status',
},
note: {
name: 'vendor.field.note',
type: 'text',
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
type: 'text',
exportable: true,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
type: 'text',
exportable: true,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
type: 'text',
exportable: true,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
type: 'text',
exportable: true,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
type: 'text',
exportable: true,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
type: 'text',
exportable: true,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
type: 'text',
exportable: true,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
type: 'text',
exportable: true,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
type: 'text',
exportable: true,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
type: 'text',
exportable: true,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
type: 'text',
exportable: true,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
type: 'text',
exportable: true,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
type: 'text',
exportable: true,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
type: 'text',
exportable: true,
},
createdAt: {
name: 'vendor.field.created_at',
type: 'date',
exportable: true,
},
},
fields2: {
firstName: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
},
lastName: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
},
displayName: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
required: true,
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
},
workPhone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
},
personalPhone: {
name: 'vendor.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
},
companyName: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
},
openingBalance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
},
openingBalanceAt: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
},
openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
},
currencyCode: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
fieldType: 'text',
importable: true,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
fieldType: 'text',
importable: true,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
fieldType: 'text',
importable: true,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
fieldType: 'text',
importable: true,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
fieldType: 'text',
importable: true,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
fieldType: 'text',
importable: true,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
fieldType: 'text',
importable: true,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
fieldType: 'text',
importable: true,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
fieldType: 'text',
importable: true,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
fieldType: 'text',
importable: true,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
fieldType: 'text',
importable: true,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
fieldType: 'text',
importable: true,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
fieldType: 'text',
importable: true,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
fieldType: 'text',
importable: true,
},
createdAt: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -12,6 +12,14 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
exportable: true,
exportFlattenOn: 'entries',
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'vendorCreditNumber',
fields: {
vendor: {
name: 'vendor_credit.field.vendor',
@@ -72,4 +80,142 @@ export default {
fieldType: 'date',
},
},
columns: {
vendorId: {
name: 'Vendor',
type: 'relation',
accessor: 'vendor.displayName',
},
exchangeRate: {
name: 'Echange Rate',
type: 'text',
},
vendorCreditNumber: {
name: 'Vendor Credit No.',
type: 'text',
},
referenceNo: {
name: 'Refernece No.',
type: 'text',
},
vendorCreditDate: {
name: 'Vendor Credit Date',
type: 'date',
},
amount: {
name: 'Amount',
accessor: 'formattedAmount',
},
creditRemaining: {
name: 'Credits Remaining',
accessor: 'formattedCreditsRemaining',
},
refundedAmount: {
name: 'Refunded Amount',
accessor: 'refundedAmount',
},
invoicedAmount: {
name: 'Invoiced Amount',
accessor: 'formattedInvoicedAmount',
},
note: {
name: 'Note',
type: 'text',
},
open: {
name: 'Open',
type: 'boolean',
},
entries: {
name: 'Entries',
type: 'collection',
collectionOf: 'object',
columns: {
itemName: {
name: 'Item Name',
accessor: 'item.name',
},
rate: {
name: 'Item Rate',
accessor: 'rateFormatted',
},
quantity: {
name: 'Item Quantity',
accessor: 'quantityFormatted',
},
description: {
name: 'Item Description',
},
amount: {
name: 'Item Amount',
accessor: 'totalFormatted',
},
},
},
},
fields2: {
vendorId: {
name: 'Vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Echange Rate',
fieldType: 'text',
},
vendorCreditNumber: {
name: 'Vendor Credit No.',
fieldType: 'text',
},
referenceNo: {
name: 'Refernece No.',
fieldType: 'text',
},
vendorCreditDate: {
name: 'Vendor Credit Date',
fieldType: 'date',
required: true,
},
note: {
name: 'Note',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
itemId: {
name: 'Item Name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
importHint: 'Matches the item name or code.',
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
},
},
},
};

View File

@@ -8,6 +8,34 @@ export default {
sortField: 'name',
sortOrder: 'DESC',
},
columns: {
date: {
name: 'warehouse_transfer.field.date',
type: 'date',
exportable: true,
},
transaction_number: {
name: 'warehouse_transfer.field.transaction_number',
type: 'text',
exportable: true,
},
status: {
name: 'warehouse_transfer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'in-transit', label: 'In Transit' },
{ key: 'transferred', label: 'Transferred' },
],
sortable: false,
},
created_at: {
name: 'warehouse_transfer.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
fields: {
date: {
name: 'warehouse_transfer.field.date',

View File

@@ -1,4 +1,5 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import 'newrelic';
import './before';
import '@/config';

View File

@@ -0,0 +1,31 @@
import { Inject, Service } from 'typedi';
import { AccountsApplication } from './AccountsApplication';
import { Exportable } from '../Export/Exportable';
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
@Service()
export class AccountsExportable extends Exportable {
@Inject()
private accountsApplication: AccountsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IAccountsFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
inactiveMode: false,
...query,
structure: IAccountsStructureType.Flat,
pageSize: 12000,
page: 1,
} as IAccountsFilter;
return this.accountsApplication
.getAccounts(tenantId, parsedQuery)
.then((output) => output.accounts);
}
}

View File

@@ -97,9 +97,11 @@ export class CommandAccountValidators {
query.whereNot('id', notAccountId);
}
});
if (account.length > 0) {
throw new ServiceError(ERRORS.ACCOUNT_CODE_NOT_UNIQUE);
throw new ServiceError(
ERRORS.ACCOUNT_CODE_NOT_UNIQUE,
'Account code is not unique.'
);
}
}
@@ -124,7 +126,10 @@ export class CommandAccountValidators {
}
});
if (foundAccount) {
throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE);
throw new ServiceError(
ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
'Account name is not unique.'
);
}
}

View File

@@ -1,4 +1,4 @@
import { Service, Inject, Container } from 'typedi';
import { Service, Inject } from 'typedi';
import {
IRegisterDTO,
ISystemUser,
@@ -9,6 +9,9 @@ import { AuthSigninService } from './AuthSignin';
import { AuthSignupService } from './AuthSignup';
import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
import { AuthSignupConfirmService } from './AuthSignupConfirm';
import { SystemUser } from '@/system/models';
import { AuthSignupConfirmResend } from './AuthSignupResend';
@Service()
export default class AuthenticationApplication {
@@ -18,6 +21,12 @@ export default class AuthenticationApplication {
@Inject()
private authSignupService: AuthSignupService;
@Inject()
private authSignupConfirmService: AuthSignupConfirmService;
@Inject()
private authSignUpConfirmResendService: AuthSignupConfirmResend;
@Inject()
private authResetPasswordService: AuthSendResetPassword;
@@ -44,6 +53,28 @@ export default class AuthenticationApplication {
return this.authSignupService.signUp(signupDTO);
}
/**
* Verfying the provided user's email after signin-up.
* @param {string} email
* @param {string} token
* @returns {Promise<SystemUser>}
*/
public async signUpConfirm(
email: string,
token: string
): Promise<SystemUser> {
return this.authSignupConfirmService.signUpConfirm(email, token);
}
/**
* Resends the confirmation email of the given system user.
* @param {number} userId - System user id.
* @returns {Promise<void>}
*/
public async signUpConfirmResend(userId: number) {
return this.authSignUpConfirmResendService.signUpConfirmResend(userId);
}
/**
* Generates and retrieve password reset token for the given user email.
* @param {string} email

View File

@@ -1,5 +1,6 @@
import { isEmpty, omit } from 'lodash';
import { defaultTo, isEmpty, omit } from 'lodash';
import moment from 'moment';
import crypto from 'crypto';
import { ServiceError } from '@/exceptions';
import {
IAuthSignedUpEventPayload,
@@ -42,6 +43,13 @@ export class AuthSignupService {
const hashedPassword = await hashPassword(signupDTO.password);
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
const verifiedEnabed = defaultTo(config.signupConfirmation.enabled, false);
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
const verified = !verifiedEnabed;
const inviteAcceptedAt = moment().format('YYYY-MM-DD');
// Triggers signin up event.
await this.eventPublisher.emitAsync(events.auth.signingUp, {
signupDTO,
@@ -50,10 +58,12 @@ export class AuthSignupService {
const tenant = await this.tenantsManager.createTenant();
const registeredUser = await systemUserRepository.create({
...omit(signupDTO, 'country'),
verifyToken,
verified,
active: true,
password: hashedPassword,
tenantId: tenant.id,
inviteAcceptedAt: moment().format('YYYY-MM-DD'),
inviteAcceptedAt,
});
// Triggers signed up event.
await this.eventPublisher.emitAsync(events.auth.signUp, {

View File

@@ -0,0 +1,57 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { SystemUser } from '@/system/models';
import { ERRORS } from './_constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
IAuthSignUpVerifiedEventPayload,
IAuthSignUpVerifingEventPayload,
} from '@/interfaces';
@Service()
export class AuthSignupConfirmService {
@Inject()
private eventPublisher: EventPublisher;
/**
* Verifies the provided user's email after signing-up.
* @throws {ServiceErrors}
* @param {IRegisterDTO} signupDTO
* @returns {Promise<ISystemUser>}
*/
public async signUpConfirm(
email: string,
verifyToken: string
): Promise<SystemUser> {
const foundUser = await SystemUser.query().findOne({ email, verifyToken });
if (!foundUser) {
throw new ServiceError(ERRORS.SIGNUP_CONFIRM_TOKEN_INVALID);
}
const userId = foundUser.id;
// Triggers `signUpConfirming` event.
await this.eventPublisher.emitAsync(events.auth.signUpConfirming, {
email,
verifyToken,
userId,
} as IAuthSignUpVerifingEventPayload);
const updatedUser = await SystemUser.query().patchAndFetchById(
foundUser.id,
{
verified: true,
verifyToken: '',
}
);
// Triggers `signUpConfirmed` event.
await this.eventPublisher.emitAsync(events.auth.signUpConfirmed, {
email,
verifyToken,
userId,
} as IAuthSignUpVerifiedEventPayload);
return updatedUser as SystemUser;
}
}

View File

@@ -0,0 +1,34 @@
import { Inject, Service } from 'typedi';
import { ServiceError } from '@/exceptions';
import { SystemUser } from '@/system/models';
import { ERRORS } from './_constants';
@Service()
export class AuthSignupConfirmResend {
@Inject('agenda')
private agenda: any;
/**
* Resends the email confirmation of the given user.
* @param {number} userId - User ID.
* @returns {Promise<void>}
*/
public async signUpConfirmResend(userId: number) {
const user = await SystemUser.query().findById(userId).throwIfNotFound();
// Throw error if the user is already verified.
if (user.verified) {
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
}
// Throw error if the verification token is not exist.
if (!user.verifyToken) {
throw new ServiceError(ERRORS.USER_ALREADY_VERIFIED);
}
const payload = {
email: user.email,
token: user.verifyToken,
fullName: user.firstName,
};
await this.agenda.now('send-signup-verify-mail', payload);
}
}

View File

@@ -33,4 +33,33 @@ export default class AuthenticationMailMesssages {
})
.send();
}
/**
* Sends signup verification mail.
* @param {string} email - Email address
* @param {string} fullName - User name.
* @param {string} token - Verification token.
* @returns {Promise<void>}
*/
public async sendSignupVerificationMail(
email: string,
fullName: string,
token: string
) {
const verifyUrl = `${config.baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
await new Mail()
.setSubject('Bigcapital - Verify your email')
.setView('mail/SignupVerifyEmail.html')
.setTo(email)
.setAttachments([
{
filename: 'bigcapital.png',
path: `${global.__views_dir}/images/bigcapital.png`,
cid: 'bigcapital_logo',
},
])
.setData({ verifyUrl, fullName })
.send();
}
}

View File

@@ -9,4 +9,6 @@ export const ERRORS = {
EMAIL_EXISTS: 'EMAIL_EXISTS',
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
SIGNUP_CONFIRM_TOKEN_INVALID: 'SIGNUP_CONFIRM_TOKEN_INVALID',
USER_ALREADY_VERIFIED: 'USER_ALREADY_VERIFIED',
};

View File

@@ -0,0 +1,30 @@
import { IAuthSignedUpEventPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { Inject } from 'typedi';
export class SendVerfiyMailOnSignUp {
@Inject('agenda')
private agenda: any;
/**
* Attaches events with handles.
*/
public attach(bus) {
bus.subscribe(events.auth.signUp, this.handleSendVerifyMailOnSignup);
}
/**
*
* @param {ITaxRateEditedPayload} payload -
*/
private handleSendVerifyMailOnSignup = async ({
user,
}: IAuthSignedUpEventPayload) => {
const payload = {
email: user.email,
token: user.verifyToken,
fullName: user.firstName,
};
await this.agenda.now('send-signup-verify-mail', payload);
};
}

View File

@@ -0,0 +1,35 @@
import { Container } from 'typedi';
import AuthenticationMailMesssages from '@/services/Authentication/AuthenticationMailMessages';
export class SendVerifyMailJob {
/**
* Constructor method.
* @param {Agenda} agenda
*/
constructor(agenda) {
agenda.define(
'send-signup-verify-mail',
{ priority: 'high' },
this.handler.bind(this)
);
}
/**
* Handle send welcome mail job.
* @param {Job} job
* @param {Function} done
*/
public async handler(job, done: Function): Promise<void> {
const { data } = job.attrs;
const { email, fullName, token } = data;
const authService = Container.get(AuthenticationMailMesssages);
try {
await authService.sendSignupVerificationMail(email, fullName, token);
done();
} catch (error) {
console.log(error);
done(error);
}
}
}

View File

@@ -6,7 +6,7 @@ import { ERRORS } from './constants';
@Service()
export default class CashflowDeleteAccount {
@Inject()
tenancy: HasTenancyService;
private tenancy: HasTenancyService;
/**
* Validate the account has no associated cashflow transactions.

View File

@@ -79,27 +79,25 @@ export interface ICashflowTransactionTypeMeta {
}
export const BankTransactionsSampleData = [
[
{
Amount: '6,410.19',
Date: '2024-03-26',
Payee: 'MacGyver and Sons',
'Reference No.': 'REF-1',
Description: 'Commodi quo labore.',
},
{
Amount: '8,914.17',
Date: '2024-01-05',
Payee: 'Eichmann - Bergnaum',
'Reference No.': 'REF-1',
Description: 'Quia enim et.',
},
{
Amount: '6,200.88',
Date: '2024-02-17',
Payee: 'Luettgen, Mraz and Legros',
'Reference No.': 'REF-1',
Description: 'Occaecati consequuntur cum impedit illo.',
},
],
{
Amount: '6,410.19',
Date: '2024-03-26',
Payee: 'MacGyver and Sons',
'Reference No.': 'REF-1',
Description: 'Commodi quo labore.',
},
{
Amount: '8,914.17',
Date: '2024-01-05',
Payee: 'Eichmann - Bergnaum',
'Reference No.': 'REF-1',
Description: 'Quia enim et.',
},
{
Amount: '6,200.88',
Date: '2024-02-17',
Payee: 'Luettgen, Mraz and Legros',
'Reference No.': 'REF-1',
Description: 'Occaecati consequuntur cum impedit illo.',
},
];

View File

@@ -50,10 +50,7 @@ export class CustomersApplication {
* @param {ISystemUser} authorizedUser
* @returns {Promise<ICustomer>}
*/
public createCustomer = (
tenantId: number,
customerDTO: ICustomerNewDTO,
) => {
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
return this.createCustomerService.createCustomer(tenantId, customerDTO);
};

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { CustomersApplication } from './CustomersApplication';
import { Exportable } from '@/services/Export/Exportable';
@Service()
export class CustomersExportable extends Exportable {
@Inject()
private customersApplication: CustomersApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IItemsFilter) {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
} as IItemsFilter;
return this.customersApplication
.getCustomers(tenantId, parsedQuery)
.then((output) => output.customers);
}
}

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import {
ISystemUser,
IVendorEditDTO,
@@ -42,13 +43,9 @@ export class VendorsApplication {
public createVendor = (
tenantId: number,
vendorDTO: IVendorNewDTO,
authorizedUser: ISystemUser
trx?: Knex.Transaction
) => {
return this.createVendorService.createVendor(
tenantId,
vendorDTO,
authorizedUser
);
return this.createVendorService.createVendor(tenantId, vendorDTO, trx);
};
/**

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { IItemsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import { VendorsApplication } from './VendorsApplication';
@Service()
export class VendorsExportable extends Exportable {
@Inject()
private vendorsApplication: VendorsApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IItemsFilter) {
const parsedQuery = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
page: 1,
...query,
pageSize: 12,
} as IItemsFilter;
return this.vendorsApplication
.getVendors(tenantId, parsedQuery)
.then((output) => output.vendors);
}
}

View File

@@ -12,7 +12,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Doloribus autem optio temporibus dolores mollitia sit.",
"Billing Address 1": "862 Jessika Well",
"Billing Address 2": "1091 Dorthy Mount",
@@ -42,7 +42,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.",
"Billing Address 1": "532 Simonis Spring",
"Billing Address 2": "3122 Nicolas Inlet",
@@ -72,7 +72,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Vero quibusdam rem fugit aperiam est modi.",
"Billing Address 1": "214 Sauer Villages",
"Billing Address 2": "30687 Kacey Square",
@@ -102,7 +102,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Quis cumque molestias rerum.",
"Billing Address 1": "22590 Cathy Harbor",
"Billing Address 2": "24493 Brycen Brooks",

View File

@@ -4,7 +4,6 @@ import {
ICreditNoteCreatedPayload,
ICreditNoteCreatingPayload,
ICreditNoteNewDTO,
ISystemUser,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
@@ -34,7 +33,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
public newCreditNote = async (
tenantId: number,
creditNoteDTO: ICreditNoteNewDTO,
authorizedUser: ISystemUser
trx?: Knex.Transaction
) => {
const { CreditNote, Contact } = this.tenancy.models(tenantId);
@@ -66,28 +65,32 @@ export default class CreateCreditNote extends BaseCreditNotes {
customer.currencyCode
);
// Creates a new credit card transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
tenantId,
creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
tenantId,
creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
// Upsert the credit note graph.
const creditNote = await CreditNote.query(trx).upsertGraph({
...creditNoteModel,
});
// Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
tenantId,
creditNoteDTO,
creditNote,
creditNoteId: creditNote.id,
trx,
} as ICreditNoteCreatedPayload);
// Upsert the credit note graph.
const creditNote = await CreditNote.query(trx).upsertGraph({
...creditNoteModel,
});
// Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
tenantId,
creditNoteDTO,
creditNote,
creditNoteId: creditNote.id,
trx,
} as ICreditNoteCreatedPayload);
return creditNote;
});
return creditNote;
},
trx
);
};
}

View File

@@ -0,0 +1,30 @@
import { Inject, Service } from 'typedi';
import { ICreditNotesQueryDTO } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable';
import ListCreditNotes from './ListCreditNotes';
@Service()
export class CreditNotesExportable extends Exportable {
@Inject()
private getCreditNotes: ListCreditNotes;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId -
* @param {IVendorCreditsQueryDTO} query -
* @returns {}
*/
public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as ICreditNotesQueryDTO;
return this.getCreditNotes
.getCreditNotesList(tenantId, parsedQuery)
.then((output) => output.creditNotes);
}
}

View File

@@ -0,0 +1,44 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ICreditNoteNewDTO } from '@/interfaces';
import { Importable } from '../Import/Importable';
import CreateCreditNote from './CreateCreditNote';
@Service()
export class CreditNotesImportable extends Importable {
@Inject()
private createCreditNoteImportable: CreateCreditNote;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: ICreditNoteNewDTO,
trx?: Knex.Transaction
) {
return this.createCreditNoteImportable.newCreditNote(
tenantId,
createAccountDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return [];
}
}

View File

@@ -45,7 +45,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
);
const { results, pagination } = await CreditNote.query()
.onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})

View File

@@ -88,7 +88,8 @@ export class CreateExpense {
public newExpense = async (
tenantId: number,
expenseDTO: IExpenseCreateDTO,
authorizedUser: ISystemUser
authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<IExpense> => {
const { Expense } = await this.tenancy.models(tenantId);
@@ -103,28 +104,32 @@ export class CreateExpense {
);
// Writes the expense transaction with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onExpenseCreating` event.
await this.eventPublisher.emitAsync(events.expenses.onCreating, {
trx,
tenantId,
expenseDTO,
} as IExpenseCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onExpenseCreating` event.
await this.eventPublisher.emitAsync(events.expenses.onCreating, {
trx,
tenantId,
expenseDTO,
} as IExpenseCreatingPayload);
// Creates a new expense transaction graph.
const expense: IExpense = await Expense.query(trx).upsertGraph(
expenseObj
);
// Triggers `onExpenseCreated` event.
await this.eventPublisher.emitAsync(events.expenses.onCreated, {
tenantId,
expenseId: expense.id,
authorizedUser,
expense,
trx,
} as IExpenseCreatedPayload);
// Creates a new expense transaction graph.
const expense: IExpense = await Expense.query(trx).upsertGraph(
expenseObj
);
// Triggers `onExpenseCreated` event.
await this.eventPublisher.emitAsync(events.expenses.onCreated, {
tenantId,
expenseId: expense.id,
authorizedUser,
expense,
trx,
} as IExpenseCreatedPayload);
return expense;
});
return expense;
},
trx
);
};
}

View File

@@ -0,0 +1,29 @@
import { Inject, Service } from 'typedi';
import { Exportable } from '../Export/Exportable';
import { IExpensesFilter } from '@/interfaces';
import { ExpensesApplication } from './ExpensesApplication';
@Service()
export class ExpensesExportable extends Exportable {
@Inject()
private expensesApplication: ExpensesApplication;
/**
* Retrieves the accounts data to exportable sheet.
* @param {number} tenantId
* @returns
*/
public exportable(tenantId: number, query: IExpensesFilter) {
const parsedQuery = {
sortOrder: 'desc',
columnSortBy: 'created_at',
...query,
page: 1,
pageSize: 12000,
} as IExpensesFilter;
return this.expensesApplication
.getExpenses(tenantId, parsedQuery)
.then((output) => output.expenses);
}
}

View File

@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IExpenseCreateDTO } from '@/interfaces';
import { Importable } from '../Import/Importable';
import { CreateExpense } from './CRUD/CreateExpense';
import { ExpensesSampleData } from './constants';
@Service()
export class ExpensesImportable extends Importable {
@Inject()
private createExpenseService: CreateExpense;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: IExpenseCreateDTO,
trx?: Knex.Transaction
) {
return this.createExpenseService.newExpense(
tenantId,
createAccountDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return ExpensesSampleData;
}
}

View File

@@ -36,3 +36,43 @@ export const ERRORS = {
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST',
};
export const ExpensesSampleData = [
{
'Payment Date': '2024-03-01',
'Reference No.': 'REF-1',
'Payment Account': 'Petty Cash',
Description: 'Vel et dolorem architecto veniam.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description': 'Voluptates voluptas corporis vel.',
Publish: 'T',
},
{
'Payment Date': '2024-03-02',
'Reference No.': 'REF-2',
'Payment Account': 'Petty Cash',
Description: 'Id est molestias.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description': 'Eos voluptatem cumque et voluptate reiciendis.',
Publish: 'T',
},
{
'Payment Date': '2024-03-03',
'Reference No.': 'REF-3',
'Payment Account': 'Petty Cash',
Description: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description':
'Hic alias rerum sed commodi dolores sint animi perferendis.',
Publish: 'T',
},
];

View File

@@ -0,0 +1,17 @@
import { Inject, Service } from 'typedi';
import { ExportResourceService } from './ExportService';
@Service()
export class ExportApplication {
@Inject()
private exportResource: ExportResourceService;
/**
* Exports the given resource to csv, xlsx or pdf format.
* @param {string} reosurce
* @param {string} format
*/
public export(tenantId: number, resource: string, format: string) {
return this.exportResource.export(tenantId, resource, format);
}
}

View File

@@ -0,0 +1,49 @@
import { camelCase, upperFirst } from 'lodash';
import { Exportable } from './Exportable';
export class ExportableRegistry {
private static instance: ExportableRegistry;
private exportables: Record<string, Exportable>;
/**
* Constructor method.
*/
constructor() {
this.exportables = {};
}
/**
* Gets singleton instance of registry.
* @returns {ExportableRegistry}
*/
public static getInstance(): ExportableRegistry {
if (!ExportableRegistry.instance) {
ExportableRegistry.instance = new ExportableRegistry();
}
return ExportableRegistry.instance;
}
/**
* Registers the given importable service.
* @param {string} resource
* @param {Exportable} importable
*/
public registerExportable(resource: string, importable: Exportable): void {
const _resource = this.sanitizeResourceName(resource);
this.exportables[_resource] = importable;
}
/**
* Retrieves the importable service instance of the given resource name.
* @param {string} name
* @returns {Exportable}
*/
public getExportable(name: string): Exportable {
const _name = this.sanitizeResourceName(name);
return this.exportables[_name];
}
private sanitizeResourceName(resource: string) {
return upperFirst(camelCase(resource));
}
}

View File

@@ -0,0 +1,72 @@
import Container, { Service } from 'typedi';
import { AccountsExportable } from '../Accounts/AccountsExportable';
import { ExportableRegistry } from './ExportRegistery';
import { ItemsExportable } from '../Items/ItemsExportable';
import { CustomersExportable } from '../Contacts/Customers/CustomersExportable';
import { VendorsExportable } from '../Contacts/Vendors/VendorsExportable';
import { ExpensesExportable } from '../Expenses/ExpensesExportable';
import { SaleInvoicesExportable } from '../Sales/Invoices/SaleInvoicesExportable';
import { SaleEstimatesExportable } from '../Sales/Estimates/SaleEstimatesExportable';
import { SaleReceiptsExportable } from '../Sales/Receipts/SaleReceiptsExportable';
import { BillsExportable } from '../Purchases/Bills/BillsExportable';
import { PaymentsReceivedExportable } from '../Sales/PaymentReceives/PaymentsReceivedExportable';
import { BillPaymentExportable } from '../Purchases/BillPayments/BillPaymentExportable';
import { ManualJournalsExportable } from '../ManualJournals/ManualJournalExportable';
import { CreditNotesExportable } from '../CreditNotes/CreditNotesExportable';
import { VendorCreditsExportable } from '../Purchases/VendorCredits/VendorCreditsExportable';
import { ItemCategoriesExportable } from '../ItemCategories/ItemCategoriesExportable';
@Service()
export class ExportableResources {
private static registry: ExportableRegistry;
/**
* Consttuctor method.
*/
constructor() {
this.boot();
}
/**
* Importable instances.
*/
private importables = [
{ resource: 'Account', exportable: AccountsExportable },
{ resource: 'Item', exportable: ItemsExportable },
{ resource: 'ItemCategory', exportable: ItemCategoriesExportable },
{ resource: 'Customer', exportable: CustomersExportable },
{ resource: 'Vendor', exportable: VendorsExportable },
{ resource: 'Expense', exportable: ExpensesExportable },
{ resource: 'SaleInvoice', exportable: SaleInvoicesExportable },
{ resource: 'SaleEstimate', exportable: SaleEstimatesExportable },
{ resource: 'SaleReceipt', exportable: SaleReceiptsExportable },
{ resource: 'Bill', exportable: BillsExportable },
{ resource: 'PaymentReceive', exportable: PaymentsReceivedExportable },
{ resource: 'BillPayment', exportable: BillPaymentExportable },
{ resource: 'ManualJournal', exportable: ManualJournalsExportable },
{ resource: 'CreditNote', exportable: CreditNotesExportable },
{ resource: 'VendorCredit', exportable: VendorCreditsExportable },
];
/**
*
*/
public get registry() {
return ExportableResources.registry;
}
/**
* Boots all the registered importables.
*/
public boot() {
if (!ExportableResources.registry) {
const instance = ExportableRegistry.getInstance();
this.importables.forEach((importable) => {
const importableInstance = Container.get(importable.exportable);
instance.registerExportable(importable.resource, importableInstance);
});
ExportableResources.registry = instance;
}
}
}

View File

@@ -0,0 +1,161 @@
import { Inject, Service } from 'typedi';
import xlsx from 'xlsx';
import * as R from 'ramda';
import { get } from 'lodash';
import { sanitizeResourceName } from '../Import/_utils';
import ResourceService from '../Resource/ResourceService';
import { ExportableResources } from './ExportResources';
import { ServiceError } from '@/exceptions';
import { Errors } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils';
@Service()
export class ExportResourceService {
@Inject()
private resourceService: ResourceService;
@Inject()
private exportableResources: ExportableResources;
/**
* Exports the given resource data through csv, xlsx or pdf.
* @param {number} tenantId - Tenant id.
* @param {string} resourceName - Resource name.
* @param {string} format - File format.
*/
public async export(tenantId: number, resourceName: string, format: string = 'csv') {
const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.getResourceMeta(tenantId, resource);
this.validateResourceMeta(resourceMeta);
const data = await this.getExportableData(tenantId, resource);
const transformed = this.transformExportedData(tenantId, resource, data);
const exportableColumns = this.getExportableColumns(resourceMeta);
const workbook = this.createWorkbook(transformed, exportableColumns);
return this.exportWorkbook(workbook, format);
}
/**
* Retrieves metadata for a specific resource.
* @param {number} tenantId - The tenant identifier.
* @param {string} resource - The name of the resource.
* @returns The metadata of the resource.
*/
private getResourceMeta(tenantId: number, resource: string) {
return this.resourceService.getResourceMeta(tenantId, resource);
}
/**
* Validates if the resource metadata is exportable.
* @param {any} resourceMeta - The metadata of the resource.
* @throws {ServiceError} If the resource is not exportable or lacks columns.
*/
private validateResourceMeta(resourceMeta: any) {
if (!resourceMeta.exportable || !resourceMeta.columns) {
throw new ServiceError(Errors.RESOURCE_NOT_EXPORTABLE);
}
}
/**
* Transforms the exported data based on the resource metadata.
* If the resource metadata specifies a flattening attribute (`exportFlattenOn`),
* the data will be flattened based on this attribute using the `flatDataCollections` utility function.
*
* @param {number} tenantId - The tenant identifier.
* @param {string} resource - The name of the resource.
* @param {Array<Record<string, any>>} data - The original data to be transformed.
* @returns {Array<Record<string, any>>} - The transformed data.
*/
private transformExportedData(
tenantId: number,
resource: string,
data: Array<Record<string, any>>
): Array<Record<string, any>> {
const resourceMeta = this.getResourceMeta(tenantId, resource);
return R.when<Array<Record<string, any>>, Array<Record<string, any>>>(
R.always(Boolean(resourceMeta.exportFlattenOn)),
(data) => flatDataCollections(data, resourceMeta.exportFlattenOn),
data
);
}
/**
* Fetches exportable data for a given resource.
* @param {number} tenantId - The tenant identifier.
* @param {string} resource - The name of the resource.
* @returns A promise that resolves to the exportable data.
*/
private async getExportableData(tenantId: number, resource: string) {
const exportable =
this.exportableResources.registry.getExportable(resource);
return exportable.exportable(tenantId, {});
}
/**
* Extracts columns that are marked as exportable from the resource metadata.
* @param {IModelMeta} resourceMeta - The metadata of the resource.
* @returns An array of exportable columns.
*/
private getExportableColumns(resourceMeta: IModelMeta) {
const processColumns = (
columns: { [key: string]: IModelMetaColumn },
parent = ''
) => {
return Object.entries(columns)
.filter(([_, value]) => value.exportable !== false)
.flatMap(([key, value]) => {
if (value.type === 'collection' && value.collectionOf === 'object') {
return processColumns(value.columns, key);
} else {
const group = parent;
return [
{
name: value.name,
type: value.type || 'text',
accessor: value.accessor || key,
group,
},
];
}
});
};
return processColumns(resourceMeta.columns);
}
/**
* Creates a workbook from the provided data and columns.
* @param {any[]} data - The data to be included in the workbook.
* @param {any[]} exportableColumns - The columns to be included in the workbook.
* @returns The created workbook.
*/
private createWorkbook(data: any[], exportableColumns: any[]) {
const workbook = xlsx.utils.book_new();
const worksheetData = data.map((item) =>
exportableColumns.map((col) => get(item, getDataAccessor(col)))
);
worksheetData.unshift(exportableColumns.map((col) => col.name));
const worksheet = xlsx.utils.aoa_to_sheet(worksheetData);
xlsx.utils.book_append_sheet(workbook, worksheet, 'Exported Data');
return workbook;
}
/**
* Exports the workbook in the specified format.
* @param {any} workbook - The workbook to be exported.
* @param {string} format - The format to export the workbook in.
* @returns The exported workbook data.
*/
private exportWorkbook(workbook: any, format: string) {
if (format.toLowerCase() === 'csv') {
return xlsx.write(workbook, { type: 'buffer', bookType: 'csv' });
} else if (format.toLowerCase() === 'xlsx') {
return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' });
}
}
}

View File

@@ -0,0 +1,22 @@
export class Exportable {
/**
*
* @param tenantId
* @returns
*/
public async exportable(
tenantId: number,
query: Record<string, any>
): Promise<Array<Record<string, any>>> {
return [];
}
/**
*
* @param data
* @returns
*/
public transform(data: Record<string, any>) {
return data;
}
}

View File

@@ -0,0 +1,3 @@
export enum Errors {
RESOURCE_NOT_EXPORTABLE = 'RESOURCE_NOT_EXPORTABLE',
}

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